Fix supplier view and add public water reading flow
This commit is contained in:
parent
016177bce0
commit
887540c5fe
|
|
@ -299,7 +299,20 @@ public function mount(): void
|
|||
{
|
||||
$user = Auth::user();
|
||||
if ($user instanceof User) {
|
||||
$requestedStabileId = request()->integer('stabile_id');
|
||||
if ($requestedStabileId > 0) {
|
||||
StabileContext::setActiveStabileId($user, $requestedStabileId);
|
||||
}
|
||||
|
||||
$this->stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
|
||||
if (! $this->stabileId) {
|
||||
$fallbackStabileId = StabileContext::accessibleStabili($user)->first()?->id;
|
||||
if ($fallbackStabileId) {
|
||||
StabileContext::setActiveStabileId($user, (int) $fallbackStabileId);
|
||||
$this->stabileId = (int) $fallbackStabileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->mountInteractsWithTable();
|
||||
|
|
@ -355,7 +368,7 @@ protected function getTableQuery(): Builder
|
|||
return LegacyCondominNominativo::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
if (! $this->hasLegacyNominativiForStabile($codStabile) || $this->hasLegacyDuplicateUnits($codStabile)) {
|
||||
if (! $this->hasLegacyNominativiForStabile($codStabile)) {
|
||||
return $this->buildDomainFallbackQuery((int) $activeStabileId);
|
||||
}
|
||||
|
||||
|
|
@ -398,20 +411,6 @@ protected function hasLegacyNominativiForStabile(string $codStabile): bool
|
|||
->exists();
|
||||
}
|
||||
|
||||
protected function hasLegacyDuplicateUnits(string $codStabile): bool
|
||||
{
|
||||
return DB::connection('gescon_import')
|
||||
->table('condomin')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->whereNotNull('interno')
|
||||
->where('interno', '<>', '')
|
||||
->select('scala', 'interno')
|
||||
->groupBy('scala', 'interno')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->limit(1)
|
||||
->exists();
|
||||
}
|
||||
|
||||
protected function buildDomainFallbackQuery(int $stabileId): Builder
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\StabileServizio;
|
||||
use App\Models\StabileServizioLettura;
|
||||
use App\Models\StabileServizioTariffa;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\User;
|
||||
use App\Models\VoceSpesa;
|
||||
use App\Support\AnnoGestioneContext;
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use UnitEnum;
|
||||
|
||||
class ServiziStabileArchivio extends Page implements HasTable
|
||||
|
|
@ -69,6 +71,121 @@ public function setAcquaTab(string $tab): void
|
|||
$this->acquaTab = in_array($tab, $allowed, true) ? $tab : 'dashboard';
|
||||
}
|
||||
|
||||
public function prepareAcquaReadingCampaign(): void
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$servizio = $this->resolveActiveAcquaServizio();
|
||||
|
||||
if (! $stabileId || ! $servizio) {
|
||||
Notification::make()->title('Servizio acqua attivo non disponibile per lo stabile selezionato.')->warning()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$units = UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('scala')
|
||||
->orderBy('interno')
|
||||
->orderBy('id')
|
||||
->get(['id', 'codice_unita', 'scala', 'interno']);
|
||||
|
||||
if ($units->isEmpty()) {
|
||||
Notification::make()->title('Nessuna unità immobiliare disponibile per avviare la campagna letture.')->warning()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$year = $this->resolveActiveAnnoGestione();
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach ($units as $unit) {
|
||||
$completed = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->where('unita_immobiliare_id', (int) $unit->id)
|
||||
->whereYear('created_at', $year)
|
||||
->whereNotNull('lettura_fine')
|
||||
->exists();
|
||||
|
||||
if ($completed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$previous = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->where('unita_immobiliare_id', (int) $unit->id)
|
||||
->whereNotNull('lettura_fine')
|
||||
->orderByDesc('periodo_al')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$requestRow = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->where('unita_immobiliare_id', (int) $unit->id)
|
||||
->whereYear('created_at', $year)
|
||||
->whereNull('lettura_fine')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$requestReference = 'PUBREQ:' . (int) $servizio->id . ':' . (int) $unit->id . ':' . $year;
|
||||
$requestPayload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'stabile_servizio_id' => (int) $servizio->id,
|
||||
'unita_immobiliare_id' => (int) $unit->id,
|
||||
'fornitore_id' => $servizio->fornitore_id,
|
||||
'periodo_dal' => $previous?->periodo_al,
|
||||
'periodo_al' => null,
|
||||
'tipologia_lettura' => 'richiesta_autolettura',
|
||||
'canale_acquisizione' => 'portale_pubblico',
|
||||
'riferimento_acquisizione' => $requestReference,
|
||||
'workflow_stato' => 'richiesta_inviata',
|
||||
'richiesta_lettura_inviata_at' => now(),
|
||||
'deadline_lettura_at' => now()->addDays(7),
|
||||
'lettura_precedente_valore' => $previous?->lettura_fine,
|
||||
'lettura_inizio' => $previous?->lettura_fine,
|
||||
'raw' => [
|
||||
'source' => 'campagna_letture_acqua',
|
||||
'year' => $year,
|
||||
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')),
|
||||
],
|
||||
];
|
||||
|
||||
if ($requestRow) {
|
||||
$requestRow->fill($requestPayload);
|
||||
$requestRow->save();
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
StabileServizioLettura::query()->create($requestPayload);
|
||||
$created++;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Campagna letture acqua preparata')
|
||||
->body("Richieste create: {$created} · richieste aggiornate: {$updated}")
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function markAcquaReadingReminder(int $readingId): void
|
||||
{
|
||||
$reading = StabileServizioLettura::query()->find($readingId);
|
||||
if (! $reading) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reading->fill([
|
||||
'sollecito_inviato_at' => now(),
|
||||
'workflow_stato' => 'sollecito_inviato',
|
||||
]);
|
||||
$reading->save();
|
||||
|
||||
Notification::make()->title('Sollecito registrato')->success()->send();
|
||||
}
|
||||
|
||||
public function ensureAcquaAceaContract(): void
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
|
|
@ -478,6 +595,34 @@ private function resolveActiveAnnoGestione(): int
|
|||
return AnnoGestioneContext::resolveActiveAnno($user instanceof User ? $user : null);
|
||||
}
|
||||
|
||||
private function resolveActiveAcquaServizio(): ?StabileServizio
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
if (! $stabileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StabileServizio::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('tipo', 'acqua')
|
||||
->orderByDesc('attivo')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
private function buildPublicAcquaReadingUrl(int $servizioId, int $unitaId, ?int $requestId = null): string
|
||||
{
|
||||
return URL::temporarySignedRoute(
|
||||
'public.water-reading.show',
|
||||
now()->addDays(30),
|
||||
array_filter([
|
||||
'servizio' => $servizioId,
|
||||
'unita' => $unitaId,
|
||||
'request' => $requestId,
|
||||
], fn ($value) => $value !== null)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function resolveLegacyCodiciStabile(): array
|
||||
{
|
||||
|
|
@ -1201,6 +1346,107 @@ public function getAcquaLettureCondominiProperty(): array
|
|||
->all();
|
||||
}
|
||||
|
||||
/** @return array{totale:int,completate:int,pendenti:int,sollecitate:int} */
|
||||
public function getAcquaCampagnaStatsProperty(): array
|
||||
{
|
||||
$rows = $this->acquaCampagnaRows;
|
||||
|
||||
return [
|
||||
'totale' => count($rows),
|
||||
'completate' => count(array_filter($rows, fn (array $row): bool => $row['status'] === 'fatta')),
|
||||
'pendenti' => count(array_filter($rows, fn (array $row): bool => in_array($row['status'], ['richiesta', 'non_richiesta'], true))),
|
||||
'sollecitate' => count(array_filter($rows, fn (array $row): bool => $row['status'] === 'sollecito')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int,array<string,mixed>> */
|
||||
public function getAcquaCampagnaRowsProperty(): array
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$servizio = $this->resolveActiveAcquaServizio();
|
||||
if (! $stabileId || ! $servizio) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$year = $this->resolveActiveAnnoGestione();
|
||||
|
||||
$units = UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('scala')
|
||||
->orderBy('interno')
|
||||
->orderBy('id')
|
||||
->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||
|
||||
$nominativi = DB::table('unita_immobiliare_nominativi')
|
||||
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id')
|
||||
->get(['unita_immobiliare_id', 'nominativo']);
|
||||
|
||||
$nominativiByUnit = [];
|
||||
foreach ($nominativi as $row) {
|
||||
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
||||
if ($unitId > 0 && ! isset($nominativiByUnit[$unitId])) {
|
||||
$nominativiByUnit[$unitId] = trim((string) ($row->nominativo ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$readingRows = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$currentYearByUnit = [];
|
||||
$latestRealByUnit = [];
|
||||
foreach ($readingRows as $row) {
|
||||
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
||||
if ($unitId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowYear = optional($row->created_at)->year ?: optional($row->periodo_al)->year;
|
||||
if ((int) $rowYear === $year && ! isset($currentYearByUnit[$unitId])) {
|
||||
$currentYearByUnit[$unitId] = $row;
|
||||
}
|
||||
|
||||
if ($row->lettura_fine !== null && ! isset($latestRealByUnit[$unitId])) {
|
||||
$latestRealByUnit[$unitId] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $units->map(function (UnitaImmobiliare $unit) use ($currentYearByUnit, $latestRealByUnit, $nominativiByUnit, $servizio): array {
|
||||
$current = $currentYearByUnit[(int) $unit->id] ?? null;
|
||||
$latestReal = $latestRealByUnit[(int) $unit->id] ?? null;
|
||||
|
||||
$status = 'non_richiesta';
|
||||
if ($current && $current->lettura_fine !== null) {
|
||||
$status = 'fatta';
|
||||
} elseif ($current && $current->sollecito_inviato_at) {
|
||||
$status = 'sollecito';
|
||||
} elseif ($current && $current->richiesta_lettura_inviata_at) {
|
||||
$status = 'richiesta';
|
||||
}
|
||||
|
||||
return [
|
||||
'unit_id' => (int) $unit->id,
|
||||
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')) ?: ('UI #' . (int) $unit->id),
|
||||
'nominativo' => $nominativiByUnit[(int) $unit->id] ?? (trim((string) ($unit->denominazione ?? '')) ?: 'Nominativo da completare'),
|
||||
'previous_reading' => $latestReal?->lettura_fine,
|
||||
'request_id' => $current?->id,
|
||||
'status' => $status,
|
||||
'requested_at' => optional($current?->richiesta_lettura_inviata_at)->format('d/m/Y H:i'),
|
||||
'deadline_at' => optional($current?->deadline_lettura_at)->format('d/m/Y'),
|
||||
'completed_at' => optional($current?->created_at)->format('d/m/Y H:i'),
|
||||
'latest_value' => $current?->lettura_fine,
|
||||
'public_url' => $this->buildPublicAcquaReadingUrl((int) $servizio->id, (int) $unit->id, $current?->id ? (int) $current->id : null),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/** @return array<int, array<string,mixed>> */
|
||||
public function getAcquaTariffeRowsProperty(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\PublicAccess;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\StabileServizio;
|
||||
use App\Models\StabileServizioLettura;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class WaterReadingRequestController extends Controller
|
||||
{
|
||||
public function show(Request $request): View
|
||||
{
|
||||
$servizioId = (int) $request->integer('servizio');
|
||||
$unitaId = (int) $request->integer('unita');
|
||||
$requestId = (int) $request->integer('request');
|
||||
|
||||
$servizio = StabileServizio::query()
|
||||
->where('id', $servizioId)
|
||||
->where('tipo', 'acqua')
|
||||
->firstOrFail();
|
||||
|
||||
$unita = UnitaImmobiliare::query()
|
||||
->with('stabile:id,denominazione,codice_stabile')
|
||||
->where('id', $unitaId)
|
||||
->where('stabile_id', (int) $servizio->stabile_id)
|
||||
->firstOrFail();
|
||||
|
||||
$pendingRequest = $requestId > 0
|
||||
? StabileServizioLettura::query()
|
||||
->where('id', $requestId)
|
||||
->where('stabile_servizio_id', $servizioId)
|
||||
->where('unita_immobiliare_id', $unitaId)
|
||||
->first()
|
||||
: null;
|
||||
|
||||
$previous = StabileServizioLettura::query()
|
||||
->where('stabile_servizio_id', $servizioId)
|
||||
->where('unita_immobiliare_id', $unitaId)
|
||||
->whereNotNull('lettura_fine')
|
||||
->when($pendingRequest, fn ($query) => $query->where('id', '!=', (int) $pendingRequest->id))
|
||||
->orderByDesc('periodo_al')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$submitUrl = URL::temporarySignedRoute('public.water-reading.store', now()->addDays(30), array_filter([
|
||||
'servizio' => $servizioId,
|
||||
'unita' => $unitaId,
|
||||
'request' => $pendingRequest?->id,
|
||||
]));
|
||||
|
||||
return view('public.water-reading.show', [
|
||||
'servizio' => $servizio,
|
||||
'unita' => $unita,
|
||||
'pendingRequest' => $pendingRequest,
|
||||
'previous' => $previous,
|
||||
'submitUrl' => $submitUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$servizioId = (int) $request->integer('servizio');
|
||||
$unitaId = (int) $request->integer('unita');
|
||||
$requestId = (int) $request->integer('request');
|
||||
|
||||
$servizio = StabileServizio::query()
|
||||
->where('id', $servizioId)
|
||||
->where('tipo', 'acqua')
|
||||
->firstOrFail();
|
||||
|
||||
$unita = UnitaImmobiliare::query()
|
||||
->where('id', $unitaId)
|
||||
->where('stabile_id', (int) $servizio->stabile_id)
|
||||
->firstOrFail();
|
||||
|
||||
$validated = $request->validate([
|
||||
'lettura_attuale' => ['required', 'numeric', 'min:0'],
|
||||
'data_lettura' => ['nullable', 'date'],
|
||||
'rilevatore_nome' => ['required', 'string', 'max:255'],
|
||||
'rilevatore_contatto' => ['nullable', 'string', 'max:255'],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
'foto_1' => ['nullable', 'image', 'max:8192'],
|
||||
'foto_2' => ['nullable', 'image', 'max:8192'],
|
||||
]);
|
||||
|
||||
$pendingRequest = $requestId > 0
|
||||
? StabileServizioLettura::query()
|
||||
->where('id', $requestId)
|
||||
->where('stabile_servizio_id', $servizioId)
|
||||
->where('unita_immobiliare_id', $unitaId)
|
||||
->first()
|
||||
: null;
|
||||
|
||||
$previous = StabileServizioLettura::query()
|
||||
->where('stabile_servizio_id', $servizioId)
|
||||
->where('unita_immobiliare_id', $unitaId)
|
||||
->whereNotNull('lettura_fine')
|
||||
->when($pendingRequest, fn ($query) => $query->where('id', '!=', (int) $pendingRequest->id))
|
||||
->orderByDesc('periodo_al')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$letturaInizio = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
|
||||
$letturaFine = (float) $validated['lettura_attuale'];
|
||||
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
|
||||
|
||||
if ($consumo !== null && $consumo < 0) {
|
||||
return back()->withErrors([
|
||||
'lettura_attuale' => 'La lettura attuale non puo essere inferiore a quella precedente.',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
$photoPaths = [];
|
||||
foreach (['foto_1', 'foto_2'] as $photoField) {
|
||||
if ($request->hasFile($photoField)) {
|
||||
$photoPaths[] = $request->file($photoField)?->store('letture-acqua-public', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => (int) $servizio->stabile_id,
|
||||
'stabile_servizio_id' => (int) $servizio->id,
|
||||
'unita_immobiliare_id' => (int) $unita->id,
|
||||
'fornitore_id' => $servizio->fornitore_id,
|
||||
'periodo_dal' => $previous?->periodo_al,
|
||||
'periodo_al' => isset($validated['data_lettura']) ? Carbon::parse((string) $validated['data_lettura'])->toDateString() : now()->toDateString(),
|
||||
'tipologia_lettura' => 'autolettura_link_pubblico',
|
||||
'canale_acquisizione' => 'portale_pubblico',
|
||||
'workflow_stato' => 'ricevuta',
|
||||
'riferimento_acquisizione' => $pendingRequest?->riferimento_acquisizione ?: ('PUBREAD:' . (int) $servizio->id . ':' . (int) $unita->id . ':' . now()->format('YmdHis')),
|
||||
'rilevatore_tipo' => 'link_pubblico',
|
||||
'rilevatore_nome' => trim((string) $validated['rilevatore_nome']),
|
||||
'lettura_precedente_valore' => $letturaInizio,
|
||||
'lettura_inizio' => $letturaInizio,
|
||||
'lettura_fine' => $letturaFine,
|
||||
'lettura_foto_path' => $photoPaths[0] ?? null,
|
||||
'lettura_foto_metadata' => [
|
||||
'paths' => $photoPaths,
|
||||
'contact' => trim((string) ($validated['rilevatore_contatto'] ?? '')),
|
||||
'note' => trim((string) ($validated['note'] ?? '')),
|
||||
],
|
||||
'consumo_valore' => $consumo,
|
||||
'consumo_unita' => 'mc',
|
||||
'raw' => [
|
||||
'source' => 'public_water_reading_link',
|
||||
'contact' => trim((string) ($validated['rilevatore_contatto'] ?? '')),
|
||||
'note' => trim((string) ($validated['note'] ?? '')),
|
||||
'photo_paths' => $photoPaths,
|
||||
'request_id' => $pendingRequest?->id,
|
||||
],
|
||||
];
|
||||
|
||||
if ($pendingRequest) {
|
||||
$pendingRequest->fill($payload);
|
||||
$pendingRequest->save();
|
||||
} else {
|
||||
StabileServizioLettura::query()->create($payload);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Lettura acqua inviata correttamente.');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
$legacyOpsRows = $this->acquaLegacyOperazioniRows;
|
||||
$acquaContratto = $this->acquaContrattoStabile;
|
||||
$acquaRipartoNominativi = $this->acquaRipartoNominativiLegacy;
|
||||
$campagnaStats = $this->acquaCampagnaStats;
|
||||
$campagnaRows = $this->acquaCampagnaRows;
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
|
|
@ -236,6 +238,90 @@ class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'servizi' ? 'bo
|
|||
|
||||
@if($acquaTab === 'letture')
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-700/50 dark:bg-emerald-900/10">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-emerald-900 dark:text-emerald-200">Campagna letture acqua per unità</div>
|
||||
<div class="mt-1 text-xs text-emerald-800 dark:text-emerald-300">Primo slice operativo: genera richieste per le unità dello stabile, dividi fatte e non fatte, e usa un link pubblico firmato per inviare lettura attuale più massimo due foto.</div>
|
||||
</div>
|
||||
<x-filament::button size="sm" color="success" wire:click="prepareAcquaReadingCampaign">
|
||||
Prepara / aggiorna campagna letture
|
||||
</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-4">
|
||||
<div class="rounded-lg border bg-white p-3 text-sm dark:bg-gray-900">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500">Unità</div>
|
||||
<div class="mt-2 text-xl font-semibold">{{ $campagnaStats['totale'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3 text-sm dark:bg-gray-900">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500">Fatte</div>
|
||||
<div class="mt-2 text-xl font-semibold text-emerald-700">{{ $campagnaStats['completate'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3 text-sm dark:bg-gray-900">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500">Pendenti</div>
|
||||
<div class="mt-2 text-xl font-semibold text-amber-700">{{ $campagnaStats['pendenti'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3 text-sm dark:bg-gray-900">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500">Sollecitate</div>
|
||||
<div class="mt-2 text-xl font-semibold text-sky-700">{{ $campagnaStats['sollecitate'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead>
|
||||
<tr class="border-b border-emerald-200 dark:border-emerald-700/50">
|
||||
<th class="px-2 py-2 text-left">Unità</th>
|
||||
<th class="px-2 py-2 text-left">Nominativo</th>
|
||||
<th class="px-2 py-2 text-right">Lettura precedente</th>
|
||||
<th class="px-2 py-2 text-left">Stato</th>
|
||||
<th class="px-2 py-2 text-left">Richiesta</th>
|
||||
<th class="px-2 py-2 text-left">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($campagnaRows as $row)
|
||||
<tr class="border-b border-emerald-100 dark:border-emerald-800/30">
|
||||
<td class="px-2 py-2">{{ $row['unit_label'] }}</td>
|
||||
<td class="px-2 py-2">{{ $row['nominativo'] }}</td>
|
||||
<td class="px-2 py-2 text-right">{{ $row['previous_reading'] !== null ? number_format((float) $row['previous_reading'], 3, ',', '.') : '—' }}</td>
|
||||
<td class="px-2 py-2">
|
||||
@if($row['status'] === 'fatta')
|
||||
<span class="rounded bg-emerald-100 px-1.5 py-0.5 font-semibold text-emerald-700">Fatta</span>
|
||||
@elseif($row['status'] === 'sollecito')
|
||||
<span class="rounded bg-sky-100 px-1.5 py-0.5 font-semibold text-sky-700">Sollecito</span>
|
||||
@elseif($row['status'] === 'richiesta')
|
||||
<span class="rounded bg-amber-100 px-1.5 py-0.5 font-semibold text-amber-700">Richiesta inviata</span>
|
||||
@else
|
||||
<span class="rounded bg-slate-100 px-1.5 py-0.5 font-semibold text-slate-700">Da inviare</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-2 py-2">
|
||||
{{ $row['requested_at'] ?: '—' }}
|
||||
@if($row['deadline_at'])
|
||||
<div class="text-[10px] text-gray-500">Scadenza {{ $row['deadline_at'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-2 py-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ $row['public_url'] }}" target="_blank" class="inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-emerald-800 ring-1 ring-inset ring-emerald-300 hover:bg-emerald-100">Apri link</a>
|
||||
@if($row['request_id'] && $row['status'] !== 'fatta')
|
||||
<button type="button" wire:click="markAcquaReadingReminder({{ (int) $row['request_id'] }})" class="inline-flex items-center rounded-md bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-700 hover:bg-slate-200">Sollecito</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-2 py-4 text-center text-emerald-700 dark:text-emerald-300">Nessuna unità disponibile per la campagna letture.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-700/50 dark:bg-slate-900/20">
|
||||
<div class="mb-2 text-sm font-semibold text-slate-900 dark:text-slate-100">Base nominativi riparto ACQUA (legacy) per inserimento letture manuali</div>
|
||||
<div class="mb-2 text-xs text-slate-600 dark:text-slate-300">
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
<a href="{{ \App\Filament\Pages\Fornitore\LavorazioniOperative::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Lavorazioni</a>
|
||||
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Collaboratori</a>
|
||||
<a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Rubrica clienti</a>
|
||||
<a href="{{ \App\Filament\Pages\Fornitore\ImpostazioniArchivio::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Impostazioni</a>
|
||||
<a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Prodotti</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -596,51 +597,50 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($attachmentPreview)
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" x-data="{ zoom: 1, pdfPage: 1, iframeKey: 0, refreshPdf() { this.iframeKey++; }, zoomIn() { this.zoom = Math.min(this.zoom + 0.2, 4); }, zoomOut() { this.zoom = Math.max(this.zoom - 0.2, 0.5); }, resetZoom() { this.zoom = 1; }, nextPage() { this.pdfPage = this.pdfPage + 1; this.refreshPdf(); }, prevPage() { this.pdfPage = Math.max(this.pdfPage - 1, 1); this.refreshPdf(); }, printPdf() { const frame = this.$refs.pdfFrame; if (frame?.contentWindow) { frame.contentWindow.focus(); frame.contentWindow.print(); } else { window.open(@js($attachmentPreview['url']), '_blank'); } } }">
|
||||
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">{{ $attachmentPreview['name'] ?? 'Allegato' }}</div>
|
||||
@if(!empty($attachmentPreview['description']))
|
||||
<div class="text-xs text-gray-500">{{ $attachmentPreview['description'] }}</div>
|
||||
@endif
|
||||
@if($attachmentPreview)
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" x-data="{ zoom: 1, pdfPage: 1, iframeKey: 0, refreshPdf() { this.iframeKey++; }, zoomIn() { this.zoom = Math.min(this.zoom + 0.2, 4); }, zoomOut() { this.zoom = Math.max(this.zoom - 0.2, 0.5); }, resetZoom() { this.zoom = 1; }, nextPage() { this.pdfPage = this.pdfPage + 1; this.refreshPdf(); }, prevPage() { this.pdfPage = Math.max(this.pdfPage - 1, 1); this.refreshPdf(); }, printPdf() { const frame = this.$refs.pdfFrame; if (frame?.contentWindow) { frame.contentWindow.focus(); frame.contentWindow.print(); } else { window.open(@js($attachmentPreview['url']), '_blank'); } } }">
|
||||
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">{{ $attachmentPreview['name'] ?? 'Allegato' }}</div>
|
||||
@if(!empty($attachmentPreview['description']))
|
||||
<div class="text-xs text-gray-500">{{ $attachmentPreview['description'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($attachmentPreview['is_image'] ?? false)
|
||||
<button type="button" x-on:click="zoomOut()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">-</button>
|
||||
<button type="button" x-on:click="resetZoom()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">100%</button>
|
||||
<button type="button" x-on:click="zoomIn()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">+</button>
|
||||
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||
<button type="button" x-on:click="prevPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina precedente</button>
|
||||
<span class="text-xs text-slate-500">Pagina <span x-text="pdfPage"></span></span>
|
||||
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
|
||||
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
|
||||
@endif
|
||||
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
|
||||
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
|
||||
@if($attachmentPreview['is_image'] ?? false)
|
||||
<button type="button" x-on:click="zoomOut()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">-</button>
|
||||
<button type="button" x-on:click="resetZoom()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">100%</button>
|
||||
<button type="button" x-on:click="zoomIn()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">+</button>
|
||||
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||
<button type="button" x-on:click="prevPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina precedente</button>
|
||||
<span class="text-xs text-slate-500">Pagina <span x-text="pdfPage"></span></span>
|
||||
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
|
||||
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
|
||||
@endif
|
||||
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
|
||||
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
|
||||
@if($attachmentPreview['is_image'] ?? false)
|
||||
<div class="h-full overflow-auto rounded-lg bg-slate-50 p-4">
|
||||
<div class="flex min-h-full min-w-full items-start justify-center">
|
||||
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="max-w-none rounded border bg-white shadow" x-bind:style="`width: ${zoom * 100}%; height: auto;`" />
|
||||
<div class="h-full overflow-auto rounded-lg bg-slate-50 p-4">
|
||||
<div class="flex min-h-full min-w-full items-start justify-center">
|
||||
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="max-w-none rounded border bg-white shadow" x-bind:style="`width: ${zoom * 100}%; height: auto;`" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||
<iframe x-ref="pdfFrame" x-bind:key="iframeKey" x-bind:src="@js($attachmentPreview['url']) + '#toolbar=1&navpanes=0&scrollbar=1&view=FitH&zoom=page-width&page=' + pdfPage" class="h-[84vh] w-full rounded border"></iframe>
|
||||
@else
|
||||
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
|
||||
Anteprima non disponibile per questo formato. Scarica o apri il file:
|
||||
<a href="{{ $attachmentPreview['url'] }}" class="ml-1 text-primary-600 hover:underline">Apri file</a>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||
<iframe x-ref="pdfFrame" x-bind:key="iframeKey" x-bind:src="@js($attachmentPreview['url']) + '#toolbar=1&navpanes=0&scrollbar=1&view=FitH&zoom=page-width&page=' + pdfPage" class="h-[84vh] w-full rounded border"></iframe>
|
||||
@else
|
||||
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
|
||||
Anteprima non disponibile per questo formato. Scarica o apri il file:
|
||||
<a href="{{ $attachmentPreview['url'] }}" class="ml-1 text-primary-600 hover:underline">Apri file</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -123,7 +123,7 @@ class="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-5
|
|||
<div class="mt-1 text-xs text-indigo-700">Tag: {{ $match->tags }}</div>
|
||||
@endif
|
||||
@if($match->note)
|
||||
<div class="mt-1 text-xs text-gray-500">{{ \\Illuminate\\Support\\Str::limit((string) $match->note, 120) }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500">{{ \Illuminate\Support\Str::limit((string) $match->note, 120) }}</div>
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
|
|
|
|||
84
resources/views/public/water-reading/show.blade.php
Normal file
84
resources/views/public/water-reading/show.blade.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Lettura acqua - {{ $unita->stabile->denominazione ?? 'NetGescon' }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||
<div class="mx-auto max-w-3xl space-y-4 p-4 md:p-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Invio lettura acqua</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">Compila la lettura attuale del contatore per l'unità indicata. Puoi allegare massimo due foto.</p>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="rounded-lg border border-emerald-300 bg-emerald-50 p-3 text-sm text-emerald-700">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="rounded-lg border border-red-300 bg-red-50 p-3 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg bg-white p-4 shadow">
|
||||
<div class="grid gap-3 md:grid-cols-2 text-sm">
|
||||
<div><span class="font-medium">Stabile:</span> {{ $unita->stabile->codice_stabile ?? '' }} - {{ $unita->stabile->denominazione ?? '-' }}</div>
|
||||
<div><span class="font-medium">Unità:</span> {{ $unita->codice_unita ?: ('UI #' . (int) $unita->id) }}</div>
|
||||
<div><span class="font-medium">Servizio:</span> {{ $servizio->nome ?: 'Acqua' }}</div>
|
||||
<div><span class="font-medium">Matricola:</span> {{ $servizio->contatore_matricola ?: '—' }}</div>
|
||||
<div><span class="font-medium">Lettura precedente:</span> {{ $previous?->lettura_fine !== null ? number_format((float) $previous->lettura_fine, 3, ',', '.') : '—' }}</div>
|
||||
<div><span class="font-medium">Ultima data nota:</span> {{ optional($previous?->periodo_al)->format('d/m/Y') ?: '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ $submitUrl }}" enctype="multipart/form-data" class="space-y-4 rounded-lg bg-white p-4 shadow">
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Chi sta inviando la lettura</span>
|
||||
<input type="text" name="rilevatore_nome" value="{{ old('rilevatore_nome') }}" class="w-full rounded-md border-slate-300" required>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Contatto</span>
|
||||
<input type="text" name="rilevatore_contatto" value="{{ old('rilevatore_contatto') }}" class="w-full rounded-md border-slate-300" placeholder="Telefono o email">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Lettura attuale</span>
|
||||
<input type="number" step="0.001" min="0" name="lettura_attuale" value="{{ old('lettura_attuale') }}" class="w-full rounded-md border-slate-300" required>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Data lettura</span>
|
||||
<input type="date" name="data_lettura" value="{{ old('data_lettura', now()->toDateString()) }}" class="w-full rounded-md border-slate-300">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Foto contatore 1</span>
|
||||
<input type="file" name="foto_1" accept="image/*" capture="environment" class="w-full rounded-md border-slate-300">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Foto contatore 2</span>
|
||||
<input type="file" name="foto_2" accept="image/*" capture="environment" class="w-full rounded-md border-slate-300">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Note</span>
|
||||
<textarea name="note" rows="3" class="w-full rounded-md border-slate-300">{{ old('note') }}</textarea>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white">Invia lettura</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -45,6 +45,11 @@
|
|||
return redirect('/admin-filament');
|
||||
});
|
||||
|
||||
Route::middleware('signed')->prefix('public/letture-acqua')->name('public.water-reading.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\PublicAccess\WaterReadingRequestController::class, 'show'])->name('show');
|
||||
Route::post('/', [\App\Http\Controllers\PublicAccess\WaterReadingRequestController::class, 'store'])->name('store');
|
||||
});
|
||||
|
||||
// --- Authenticated Routes ---
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user