gestione documentale avanzata 1 parte

This commit is contained in:
2026-05-28 09:34:28 +02:00
parent 3471befb1a
commit f2b0833b90
34482 changed files with 4312269 additions and 546 deletions
@@ -20,6 +20,12 @@
<a href="#applicazione" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cog mr-2"></i> Applicazione
</a>
<a href="#repository" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cloud mr-2"></i> Repository Remoti
</a>
<a href="#documenti" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-file mr-2"></i> Repository Documenti
</a>
<a href="#logo" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-image mr-2"></i> Logo
</a>
@@ -124,6 +130,188 @@
</div>
</div>
{{-- === REPOSITORY REMOTI (Opzione B) === --}}
<div class="tab-pane" id="repository">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> Repository Remoti</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#repoModal" onclick="resetRepoForm()">
<i class="fas fa-plus mr-1"></i> Nuovo Repository
</button>
</div>
</div>
<div class="card-body">
<p class="text-muted">Configura repository remoti (WebDAV, Google Drive) per archiviare documenti. Ogni repository appare come root separata nell'albero delle cartelle in Gestione Documenti.</p>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if($repositories->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm" id="repoTable">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>Nome</th>
<th>Tipo</th>
<th>Stato</th>
<th style="width:180px;">Azioni</th>
</tr>
</thead>
<tbody id="repoSortable">
@foreach($repositories as $repo)
<tr data-id="{{ $repo->id }}">
<td class="text-center handle" style="cursor:grab;"><i class="fas fa-grip-vertical text-muted"></i></td>
<td><strong>{{ $repo->nome }}</strong></td>
<td>
<span class="badge badge-{{ $repo->tipo === 'google_drive' ? 'danger' : 'secondary' }}">
<i class="fas {{ $repo->tipo === 'google_drive' ? 'fa-google-drive' : 'fa-server' }} mr-1"></i>
{{ \App\Models\StorageRepository::etichettaTipo($repo->tipo) }}
</span>
</td>
<td>
@if($repo->is_active)
<span class="badge badge-success">Attivo</span>
@else
<span class="badge badge-secondary">Disattivo</span>
@endif
</td>
<td>
<button type="button" class="btn btn-xs btn-info" onclick="editRepo({{ $repo->id }})" title="Modifica">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-xs btn-success test-repo-btn" onclick="testRepo({{ $repo->id }}, this)" title="Test connessione">
<i class="fas fa-plug"></i>
</button>
<button type="button" class="btn btn-xs btn-danger" onclick="deleteRepo({{ $repo->id }}, '{{ addslashes($repo->nome) }}')" title="Elimina">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center text-muted py-4">
<i class="fas fa-cloud fa-3x mb-3"></i>
<p class="mb-0">Nessun repository remoto configurato.</p>
<p class="mb-0">Clicca "Nuovo Repository" per aggiungere un server WebDAV o Google Drive.</p>
</div>
@endif
</div>
</div>
@if($repositories->count() > 0)
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i> Come funziona</h3>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Ogni repository configurato apparirà come <strong>root separata</strong> nella sidebar <strong>Gestione Documenti</strong>, sotto "Repository Remoti".</li>
<li>Puoi navigare i file e le cartelle di ogni repository remoto direttamente dall'interfaccia documenti.</li>
<li><strong>WebDAV</strong>: supporta server che parlano il protocollo WebDAV (es. NextCloud, ownCloud, Synology).</li>
<li><strong>Google Drive</strong>: richiede OAuth 2.0 con client ID, client secret e refresh token configurati nella <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>.</li>
<li>Usa il pulsante <i class="fas fa-plug text-success"></i> <strong>Test</strong> per verificare la connettività dopo la configurazione.</li>
</ul>
</div>
</div>
@endif
</div>
{{-- === REPOSITORY DOCUMENTI === --}}
<div class="tab-pane" id="documenti">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Repository Documenti</h3>
</div>
<div class="card-body">
<p class="text-muted">Configura il repository filesystem dove vengono salvati i documenti caricati.</p>
@php
$currentDisk = $appSettings->documenti_storage_disk ?? 'local';
$currentPath = $appSettings->documenti_storage_path ?? 'documenti';
$absPath = storage_path('app/' . ($currentDisk === 'local' ? 'private/' : '') . $currentPath);
@endphp
<form method="POST" action="{{ route('impostazioni.app-settings.save') }}">
@csrf
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_disk">Disco di archiviazione</label>
<select name="documenti_storage_disk" id="documenti_storage_disk" class="form-control">
<option value="local" {{ $currentDisk === 'local' ? 'selected' : '' }}>Locale (storage/app/private)</option>
<option value="public" {{ $currentDisk === 'public' ? 'selected' : '' }}>Pubblico (storage/app/public)</option>
</select>
<small class="text-muted">Il disco predefinito per salvare i documenti</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_path">Percorso relativo</label>
<input type="text" name="documenti_storage_path" id="documenti_storage_path" class="form-control"
value="{{ $currentPath }}" placeholder="es. documenti">
<small class="text-muted">Sottocartella all'interno del disco</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
<strong>Percorso assoluto calcolato:</strong>
<code>{{ $absPath }}</code>
<br>
<small class="text-muted">I nuovi documenti verranno salvati in questa directory.</small>
</div>
@if(session('path_changed'))
@php
$migrateCount = session('migration_count', 0);
$oldD = session('old_storage_disk', 'local');
$oldP = session('old_storage_path', 'documenti');
$newD = session('new_storage_disk', 'local');
$newP = session('new_storage_path', 'documenti');
@endphp
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Percorso modificato!</strong>
<p class="mb-2">La nuova directory <code>{{ $newD }}://{{ $newP }}</code> è stata creata.
@if($migrateCount > 0)
Ci sono <strong>{{ $migrateCount }}</strong> file nella vecchia posizione <code>{{ $oldD }}://{{ $oldP }}</code>.</p>
<form method="POST" action="{{ route('impostazioni.migrate-percorso') }}" class="d-inline" onsubmit="return confirm('Spostare {{ $migrateCount }} file? Questa operazione potrebbe richiedere alcuni secondi.');">
@csrf
<input type="hidden" name="old_storage_disk" value="{{ $oldD }}">
<input type="hidden" name="old_storage_path" value="{{ $oldP }}">
<input type="hidden" name="new_storage_disk" value="{{ $newD }}">
<input type="hidden" name="new_storage_path" value="{{ $newP }}">
<button type="submit" class="btn btn-warning">
<i class="fas fa-exchange-alt mr-1"></i> Sposta {{ $migrateCount }} file nel nuovo percorso
</button>
<a href="/impostazioni#documenti" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Ignora
</a>
</form>
@else
<p class="mb-0">Nessun file da spostare.</p>
@endif
</div>
@endif
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Impostazioni
</button>
</form>
</div>
</div>
</div>
{{-- === LOGO === --}}
<div class="tab-pane" id="logo">
<div class="card card-primary">
@@ -1434,5 +1622,252 @@ if (window.location.hash === '#email') {
setTimeout(initSignatureEditor, 300);
});
}
// ===== REPOSITORY REMOTI =====
let repositories = @json($repositories);
function resetRepoForm() {
document.getElementById('repoForm').action = '{{ route('storage-repositories.store') }}';
document.getElementById('repoMethod').value = 'POST';
document.getElementById('repoModalTitle').textContent = 'Nuovo Repository Remoto';
document.getElementById('repoForm').reset();
document.getElementById('repoId').value = '';
document.getElementById('repo_is_active').checked = true;
toggleRepoConfigFields();
}
function toggleRepoConfigFields() {
const tipo = document.getElementById('repo_tipo').value;
document.getElementById('webdavFields').style.display = tipo === 'webdav' ? '' : 'none';
document.getElementById('googleDriveFields').style.display = tipo === 'google_drive' ? '' : 'none';
}
function editRepo(id) {
const repo = repositories.find(r => r.id === id);
if (!repo) return;
document.getElementById('repoForm').action = '{{ route('storage-repositories.update', '__ID__') }}'.replace('__ID__', id);
document.getElementById('repoMethod').value = 'PUT';
document.getElementById('repoModalTitle').textContent = 'Modifica Repository - ' + repo.nome;
document.getElementById('repoId').value = repo.id;
document.getElementById('repo_nome').value = repo.nome;
document.getElementById('repo_tipo').value = repo.tipo;
document.getElementById('repo_is_active').checked = repo.is_active;
const config = repo.config || {};
document.getElementById('webdav_base_uri').value = config.base_uri || '';
document.getElementById('webdav_username').value = config.username || '';
document.getElementById('webdav_password').value = '';
document.getElementById('webdav_password').placeholder = config.password ? '......' : '';
document.getElementById('webdav_root').value = config.root || '/';
document.getElementById('webdav_auth_type').value = config.auth_type || 'basic';
document.getElementById('gd_client_id').value = config.client_id || '';
document.getElementById('gd_client_secret').value = '';
document.getElementById('gd_client_secret').placeholder = config.client_secret ? '......' : '';
document.getElementById('gd_refresh_token').value = '';
document.getElementById('gd_refresh_token').placeholder = config.refresh_token ? '......' : '';
document.getElementById('gd_root_folder_id').value = config.root_folder_id || 'root';
toggleRepoConfigFields();
$('#repoModal').modal('show');
}
function deleteRepo(id, nome) {
if (!confirm('Eliminare il repository "' + nome + '"?')) return;
var url = '{{ route('storage-repositories.destroy', '__ID__') }}'.replace('__ID__', id);
fetch(url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'X-HTTP-Method-Override': 'DELETE'
}
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
location.reload();
} else {
alert('❌ ' + (data.message || 'Errore'));
}
}).catch(function() {
alert('❌ Errore di rete');
});
}
function testRepo(id, btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
$.ajax({
url: '{{ route('storage-repositories.test', '__ID__') }}'.replace('__ID__', id),
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
timeout: 30000,
success: function(response) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
alert(response.success ? 'Connessione OK: ' + response.message : 'Errore: ' + response.message);
},
error: function(xhr) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
const msg = xhr.responseJSON?.message || 'Errore di connessione';
alert('Errore: ' + msg);
}
});
}
$(document).ready(function() {
if (document.getElementById('repoSortable')) {
var sortableRepo = Sortable.create(document.getElementById('repoSortable'), {
handle: '.handle',
animation: 150,
onEnd: function(evt) {
var order = [];
$('#repoSortable tr').each(function() {
order.push($(this).data('id'));
});
fetch('{{ route('storage-repositories.reorder') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json',
},
body: JSON.stringify({ order: order })
});
}
});
}
});
</script>
{{-- MODAL: Repository Remoto --}}
<div class="modal fade" id="repoModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="repoModalTitle"><i class="fas fa-cloud mr-2"></i>Nuovo Repository Remoto</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="repoForm" method="POST" action="{{ route('storage-repositories.store') }}">
@csrf
<input type="hidden" name="_method" id="repoMethod" value="POST">
<input type="hidden" name="id" id="repoId" value="">
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="repo_nome">Nome *</label>
<input type="text" name="nome" id="repo_nome" class="form-control" placeholder="es. Server Parrocchia" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="repo_tipo">Tipo *</label>
<select name="tipo" id="repo_tipo" class="form-control" required onchange="toggleRepoConfigFields()">
<option value="webdav">WebDAV</option>
<option value="google_drive">Google Drive</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="repo_is_active" name="is_active" value="1" checked>
<label class="custom-control-label" for="repo_is_active">Repository attivo</label>
</div>
</div>
<hr>
<h6>Configurazione</h6>
{{-- WebDAV Fields --}}
<div id="webdavFields">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="webdav_base_uri">URL Base WebDAV *</label>
<input type="url" name="config[base_uri]" id="webdav_base_uri" class="form-control" placeholder="es. https://nextcloud.parrocchia.it/remote.php/dav/files/utente/">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_root">Root path</label>
<input type="text" name="config[root]" id="webdav_root" class="form-control" value="/" placeholder="/">
<small class="text-muted">Percorso base nel server</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="webdav_username">Username</label>
<input type="text" name="config[username]" id="webdav_username" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_password">Password</label>
<input type="password" name="config[password]" id="webdav_password" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_auth_type">Auth Type</label>
<select name="config[auth_type]" id="webdav_auth_type" class="form-control">
<option value="basic">Basic</option>
<option value="digest">Digest</option>
<option value="ntlm">NTLM</option>
</select>
</div>
</div>
</div>
</div>
{{-- Google Drive Fields --}}
<div id="googleDriveFields" style="display:none;">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_id">Client ID *</label>
<input type="text" name="config[client_id]" id="gd_client_id" class="form-control" placeholder="Google Client ID">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_secret">Client Secret *</label>
<input type="text" name="config[client_secret]" id="gd_client_secret" class="form-control" placeholder="Google Client Secret">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_refresh_token">Refresh Token *</label>
<input type="text" name="config[refresh_token]" id="gd_refresh_token" class="form-control" placeholder="OAuth Refresh Token">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_root_folder_id">Root Folder ID</label>
<input type="text" name="config[root_folder_id]" id="gd_root_folder_id" class="form-control" value="root" placeholder="root">
<small class="text-muted">ID della cartella Google Drive, default: root</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
Per ottenere le credenziali OAuth: <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
&rarr; API &amp; Services &rarr; Credentials &rarr; Crea OAuth Client ID (Desktop App).
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Repository
</button>
</div>
</form>
</div>
</div>
</div>
@endsection