mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
Versamento su API AgID
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* API client per le impostazioni del tenant.
|
||||
*
|
||||
* Endpoint:
|
||||
* GET /api/v1/settings → legge configurazione (admin)
|
||||
* PUT /api/v1/settings → aggiorna configurazione (admin)
|
||||
*/
|
||||
|
||||
import { apiClient } from './client'
|
||||
|
||||
// ─── Tipi ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export type ArchivalMode = 'mock' | 'production'
|
||||
|
||||
export interface TenantSettingsResponse {
|
||||
id: string
|
||||
tenant_id: string
|
||||
archival_mode: ArchivalMode
|
||||
conservatore_id: string
|
||||
conservatore_endpoint: string | null
|
||||
conservatore_username_configured: boolean
|
||||
conservatore_password_configured: boolean
|
||||
archival_notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface TenantSettingsUpdate {
|
||||
archival_mode?: ArchivalMode
|
||||
conservatore_id?: string
|
||||
/** URL endpoint API del conservatore (obbligatorio in produzione) */
|
||||
conservatore_endpoint?: string
|
||||
/** Username in chiaro – viene cifrata lato server. Stringa vuota = cancella */
|
||||
conservatore_username?: string
|
||||
/** Password in chiaro – viene cifrata lato server. Stringa vuota = cancella */
|
||||
conservatore_password?: string
|
||||
archival_notes?: string
|
||||
}
|
||||
|
||||
// ─── Client ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const settingsApi = {
|
||||
/**
|
||||
* Recupera le impostazioni del tenant corrente.
|
||||
* Se non esistono, il backend le crea con i valori di default (mock).
|
||||
*/
|
||||
get: async (): Promise<TenantSettingsResponse> => {
|
||||
const { data } = await apiClient.get<TenantSettingsResponse>('/settings')
|
||||
return data
|
||||
},
|
||||
|
||||
/**
|
||||
* Aggiorna le impostazioni del tenant.
|
||||
* Solo i campi forniti vengono modificati (semantica PATCH).
|
||||
*/
|
||||
update: async (payload: TenantSettingsUpdate): Promise<TenantSettingsResponse> => {
|
||||
const { data } = await apiClient.put<TenantSettingsResponse>('/settings', payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
@@ -1,17 +1,33 @@
|
||||
/**
|
||||
* SettingsPage – impostazioni profilo dell'utente corrente.
|
||||
* SettingsPage – impostazioni profilo utente e configurazione tenant (admin).
|
||||
*
|
||||
* Sezioni:
|
||||
* - Informazioni profilo (nome visualizzato, email, ruolo)
|
||||
* - Modifica nome
|
||||
* - Cambio password
|
||||
* - [Solo admin] Archiviazione Sostitutiva – toggle mock/produzione + credenziali conservatore
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Settings, User, Lock, Save } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Settings,
|
||||
User,
|
||||
Lock,
|
||||
Save,
|
||||
Archive,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FlaskConical,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
import { usersApi } from '@/api/users.api'
|
||||
import { settingsApi, type TenantSettingsResponse, type ArchivalMode } from '@/api/settings.api'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Label } from '@/components/ui/Label'
|
||||
@@ -33,11 +49,31 @@ function roleLabel(role: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Badge modalità archiviazione ────────────────────────────────────────────
|
||||
|
||||
function ArchivalModeBadge({ mode }: { mode: ArchivalMode }) {
|
||||
if (mode === 'production') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200">
|
||||
<Zap className="h-3 w-3" />
|
||||
Produzione
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-100 text-amber-800 border border-amber-200">
|
||||
<FlaskConical className="h-3 w-3" />
|
||||
Mock (simulato)
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Pagina ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function SettingsPage() {
|
||||
const { user } = useAuth()
|
||||
const loadUser = useAuthStore((s) => s.loadUser)
|
||||
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'
|
||||
|
||||
/* ── Stato modifica nome ── */
|
||||
const [fullName, setFullName] = useState(user?.full_name ?? '')
|
||||
@@ -48,6 +84,49 @@ export function SettingsPage() {
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [savingPwd, setSavingPwd] = useState(false)
|
||||
|
||||
/* ── Stato impostazioni archiviazione (admin) ── */
|
||||
const [archivalSettings, setArchivalSettings] = useState<TenantSettingsResponse | null>(null)
|
||||
const [loadingArchival, setLoadingArchival] = useState(false)
|
||||
const [archivalExpanded, setArchivalExpanded] = useState(false)
|
||||
|
||||
// Form archiviazione
|
||||
const [archivalMode, setArchivalMode] = useState<ArchivalMode>('mock')
|
||||
const [conservatoreId, setConservatoreId] = useState('')
|
||||
const [conservatoreEndpoint, setConservatoreEndpoint] = useState('')
|
||||
const [conservatoreUsername, setConservatoreUsername] = useState('')
|
||||
const [conservatorePassword, setConservatorePassword] = useState('')
|
||||
const [archivalNotes, setArchivalNotes] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [savingArchival, setSavingArchival] = useState(false)
|
||||
|
||||
// Conferma passaggio a produzione
|
||||
const [showProductionConfirm, setShowProductionConfirm] = useState(false)
|
||||
|
||||
/* ── Carica impostazioni archiviazione ── */
|
||||
useEffect(() => {
|
||||
if (isAdmin) {
|
||||
loadArchivalSettings()
|
||||
}
|
||||
}, [isAdmin])
|
||||
|
||||
const loadArchivalSettings = async () => {
|
||||
setLoadingArchival(true)
|
||||
try {
|
||||
const data = await settingsApi.get()
|
||||
setArchivalSettings(data)
|
||||
// Popola form
|
||||
setArchivalMode(data.archival_mode)
|
||||
setConservatoreId(data.conservatore_id)
|
||||
setConservatoreEndpoint(data.conservatore_endpoint ?? '')
|
||||
setArchivalNotes(data.archival_notes ?? '')
|
||||
// Username/password non vengono mai restituiti in chiaro
|
||||
} catch {
|
||||
toast.error('Errore durante il caricamento delle impostazioni di archiviazione')
|
||||
} finally {
|
||||
setLoadingArchival(false)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Salva nome ── */
|
||||
const handleSaveName = async () => {
|
||||
if (!user) return
|
||||
@@ -91,6 +170,67 @@ export function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Cambio modalità archiviazione ── */
|
||||
const handleModeToggle = (newMode: ArchivalMode) => {
|
||||
if (newMode === 'production' && archivalMode === 'mock') {
|
||||
// Chiedi conferma prima di passare a produzione
|
||||
setShowProductionConfirm(true)
|
||||
} else {
|
||||
setArchivalMode(newMode)
|
||||
setShowProductionConfirm(false)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Salva impostazioni archiviazione ── */
|
||||
const handleSaveArchival = async () => {
|
||||
// Validazione client-side per modalità produzione
|
||||
if (archivalMode === 'production' && !conservatoreEndpoint.trim()) {
|
||||
toast.error('La modalità produzione richiede un URL endpoint del conservatore')
|
||||
return
|
||||
}
|
||||
|
||||
setSavingArchival(true)
|
||||
try {
|
||||
const payload: Parameters<typeof settingsApi.update>[0] = {
|
||||
archival_mode: archivalMode,
|
||||
conservatore_id: conservatoreId || undefined,
|
||||
conservatore_endpoint: conservatoreEndpoint || undefined,
|
||||
archival_notes: archivalNotes || undefined,
|
||||
}
|
||||
|
||||
// Includi credenziali solo se l'utente ha inserito qualcosa
|
||||
if (conservatoreUsername) {
|
||||
payload.conservatore_username = conservatoreUsername
|
||||
}
|
||||
if (conservatorePassword) {
|
||||
payload.conservatore_password = conservatorePassword
|
||||
}
|
||||
|
||||
const updated = await settingsApi.update(payload)
|
||||
setArchivalSettings(updated)
|
||||
setArchivalMode(updated.archival_mode)
|
||||
setConservatoreId(updated.conservatore_id)
|
||||
setConservatoreEndpoint(updated.conservatore_endpoint ?? '')
|
||||
setArchivalNotes(updated.archival_notes ?? '')
|
||||
// Reset credenziali (non rimostrare in chiaro)
|
||||
setConservatoreUsername('')
|
||||
setConservatorePassword('')
|
||||
setShowProductionConfirm(false)
|
||||
|
||||
toast.success(
|
||||
updated.archival_mode === 'production'
|
||||
? '✅ Archiviazione attivata in modalità PRODUZIONE'
|
||||
: '🧪 Archiviazione impostata in modalità mock'
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { response?: { data?: { detail?: string } } })
|
||||
?.response?.data?.detail
|
||||
toast.error(msg ?? 'Errore durante il salvataggio delle impostazioni')
|
||||
} finally {
|
||||
setSavingArchival(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
@@ -193,6 +333,319 @@ export function SettingsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ── Card: Archiviazione Sostitutiva (solo admin) ── */}
|
||||
{isAdmin && (
|
||||
<Card className="p-5">
|
||||
{/* Header collassabile */}
|
||||
<button
|
||||
className="w-full flex items-center justify-between text-left"
|
||||
onClick={() => setArchivalExpanded((v) => !v)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Archive className="h-4 w-4 text-gray-500" />
|
||||
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">
|
||||
Archiviazione Sostitutiva
|
||||
</h2>
|
||||
{archivalSettings && (
|
||||
<ArchivalModeBadge mode={archivalSettings.archival_mode} />
|
||||
)}
|
||||
</div>
|
||||
{archivalExpanded
|
||||
? <ChevronUp className="h-4 w-4 text-gray-400" />
|
||||
: <ChevronDown className="h-4 w-4 text-gray-400" />
|
||||
}
|
||||
</button>
|
||||
|
||||
{archivalExpanded && (
|
||||
<div className="mt-5 space-y-5">
|
||||
|
||||
{loadingArchival ? (
|
||||
<p className="text-sm text-gray-500 py-4 text-center">
|
||||
Caricamento impostazioni…
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* ── Toggle modalità ── */}
|
||||
<div className="space-y-2">
|
||||
<Label>Modalità conservatore</Label>
|
||||
<p className="text-xs text-gray-500">
|
||||
In modalità <strong>mock</strong> le operazioni di versamento vengono
|
||||
simulate localmente senza inviare dati a sistemi esterni. Attiva la
|
||||
modalità <strong>produzione</strong> solo dopo aver configurato l'endpoint
|
||||
e le credenziali del conservatore AgID.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3 mt-2">
|
||||
{/* Bottone Mock */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleModeToggle('mock')}
|
||||
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
||||
archivalMode === 'mock'
|
||||
? 'border-amber-400 bg-amber-50 text-amber-900'
|
||||
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FlaskConical className={`h-6 w-6 ${archivalMode === 'mock' ? 'text-amber-600' : 'text-gray-400'}`} />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold">Mock</p>
|
||||
<p className="text-xs opacity-75">Simulazione locale</p>
|
||||
</div>
|
||||
{archivalMode === 'mock' && (
|
||||
<CheckCircle className="h-4 w-4 text-amber-600" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Bottone Produzione */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleModeToggle('production')}
|
||||
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
||||
archivalMode === 'production'
|
||||
? 'border-green-500 bg-green-50 text-green-900'
|
||||
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Zap className={`h-6 w-6 ${archivalMode === 'production' ? 'text-green-600' : 'text-gray-400'}`} />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold">Produzione</p>
|
||||
<p className="text-xs opacity-75">Conservatore reale AgID</p>
|
||||
</div>
|
||||
{archivalMode === 'production' && (
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Banner conferma passaggio a produzione */}
|
||||
{showProductionConfirm && archivalMode === 'mock' && (
|
||||
<div className="mt-3 p-3 rounded-lg bg-amber-50 border border-amber-200 flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-800">
|
||||
Stai per attivare la modalità produzione
|
||||
</p>
|
||||
<p className="text-xs text-amber-700 mt-0.5">
|
||||
I versamenti verranno inviati al conservatore AgID reale.
|
||||
Assicurati che l'endpoint e le credenziali siano corretti.
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
className="text-xs font-medium text-amber-800 underline hover:no-underline"
|
||||
onClick={() => {
|
||||
setArchivalMode('production')
|
||||
setShowProductionConfirm(false)
|
||||
}}
|
||||
>
|
||||
Confermo, attiva produzione
|
||||
</button>
|
||||
<span className="text-amber-600">·</span>
|
||||
<button
|
||||
className="text-xs font-medium text-gray-600 underline hover:no-underline"
|
||||
onClick={() => setShowProductionConfirm(false)}
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr className="border-gray-100" />
|
||||
|
||||
{/* ── Identificativo conservatore ── */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="conservatore_id">Identificativo conservatore</Label>
|
||||
<Input
|
||||
id="conservatore_id"
|
||||
value={conservatoreId}
|
||||
onChange={(e) => setConservatoreId(e.target.value)}
|
||||
placeholder="es. aruba-cons, docuvision, namirial"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Codice identificativo del provider di conservazione (usato nei log dei versamenti).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Endpoint (mostrato sempre, obbligatorio in produzione) ── */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="conservatore_endpoint">
|
||||
URL endpoint API conservatore
|
||||
{archivalMode === 'production' && (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="conservatore_endpoint"
|
||||
value={conservatoreEndpoint}
|
||||
onChange={(e) => setConservatoreEndpoint(e.target.value)}
|
||||
placeholder="https://conservatore.provider.it/api/v1"
|
||||
className={
|
||||
archivalMode === 'production' && !conservatoreEndpoint
|
||||
? 'border-red-300 focus:ring-red-400'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
{archivalMode === 'mock' && (
|
||||
<p className="text-xs text-gray-400">
|
||||
Non utilizzato in modalità mock – puoi configurarlo in anticipo.
|
||||
</p>
|
||||
)}
|
||||
{archivalMode === 'production' && !conservatoreEndpoint && (
|
||||
<p className="text-xs text-red-500">
|
||||
Obbligatorio per la modalità produzione
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Credenziali (mostrate sempre, più evidenti in produzione) ── */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
Credenziali conservatore
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Vengono salvate cifrate (AES-256-GCM).
|
||||
{archivalSettings?.conservatore_username_configured && (
|
||||
<span className="ml-1 text-green-600 font-medium">
|
||||
✓ Username configurata
|
||||
</span>
|
||||
)}
|
||||
{archivalSettings?.conservatore_password_configured && (
|
||||
<span className="ml-2 text-green-600 font-medium">
|
||||
✓ Password configurata
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="conservatore_username">Username</Label>
|
||||
<Input
|
||||
id="conservatore_username"
|
||||
value={conservatoreUsername}
|
||||
onChange={(e) => setConservatoreUsername(e.target.value)}
|
||||
placeholder={
|
||||
archivalSettings?.conservatore_username_configured
|
||||
? '(credenziale già salvata – lascia vuoto per mantenerla)'
|
||||
: 'Inserisci username conservatore'
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="conservatore_password">Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="conservatore_password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={conservatorePassword}
|
||||
onChange={(e) => setConservatorePassword(e.target.value)}
|
||||
placeholder={
|
||||
archivalSettings?.conservatore_password_configured
|
||||
? '(credenziale già salvata – lascia vuoto per mantenerla)'
|
||||
: 'Inserisci password conservatore'
|
||||
}
|
||||
autoComplete="new-password"
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
>
|
||||
{showPassword
|
||||
? <EyeOff className="h-4 w-4" />
|
||||
: <Eye className="h-4 w-4" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Note operative ── */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="archival_notes">Note operative (opzionale)</Label>
|
||||
<textarea
|
||||
id="archival_notes"
|
||||
value={archivalNotes}
|
||||
onChange={(e) => setArchivalNotes(e.target.value)}
|
||||
placeholder="es. Contratto n. 123/2026 – scadenza 31/12/2027"
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Riepilogo stato corrente ── */}
|
||||
{archivalSettings && (
|
||||
<div className={`rounded-lg p-3 text-xs border ${
|
||||
archivalSettings.archival_mode === 'production'
|
||||
? 'bg-green-50 border-green-200 text-green-800'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-600'
|
||||
}`}>
|
||||
<p className="font-medium mb-1">
|
||||
Configurazione attuale (salvata)
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
<li>
|
||||
Modalità:{' '}
|
||||
<strong>
|
||||
{archivalSettings.archival_mode === 'production'
|
||||
? 'Produzione'
|
||||
: 'Mock (simulazione)'}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
Conservatore ID: <strong>{archivalSettings.conservatore_id}</strong>
|
||||
</li>
|
||||
{archivalSettings.conservatore_endpoint && (
|
||||
<li>
|
||||
Endpoint:{' '}
|
||||
<strong className="font-mono text-xs break-all">
|
||||
{archivalSettings.conservatore_endpoint}
|
||||
</strong>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
Credenziali:{' '}
|
||||
{archivalSettings.conservatore_username_configured &&
|
||||
archivalSettings.conservatore_password_configured
|
||||
? <strong className="text-green-700">Configurate ✓</strong>
|
||||
: <span className="text-gray-500">Non configurate</span>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Pulsante salva ── */}
|
||||
<div className="flex justify-end pt-1">
|
||||
<Button
|
||||
onClick={handleSaveArchival}
|
||||
disabled={savingArchival}
|
||||
className={
|
||||
archivalMode === 'production'
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1.5" />
|
||||
{savingArchival
|
||||
? 'Salvataggio…'
|
||||
: archivalMode === 'production'
|
||||
? 'Salva e attiva produzione'
|
||||
: 'Salva impostazioni'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user