GapFill Flowee
This commit is contained in:
@@ -22,6 +22,8 @@ export interface TenantSettingsResponse {
|
||||
archival_mode: ArchivalMode
|
||||
conservatore_id: string
|
||||
conservatore_endpoint: string | null
|
||||
/** Slug del tenant sul sistema del conservatore (es. 'pechub' per Aeterna) */
|
||||
conservatore_tenant_slug: string | null
|
||||
conservatore_username_configured: boolean
|
||||
conservatore_password_configured: boolean
|
||||
archival_notes: string | null
|
||||
@@ -34,6 +36,8 @@ export interface TenantSettingsUpdate {
|
||||
conservatore_id?: string
|
||||
/** URL endpoint API del conservatore (obbligatorio in produzione) */
|
||||
conservatore_endpoint?: string
|
||||
/** Slug del tenant sul sistema del conservatore (es. 'pechub' per Aeterna) */
|
||||
conservatore_tenant_slug?: string
|
||||
/** Username in chiaro – viene cifrata lato server. Stringa vuota = cancella */
|
||||
conservatore_username?: string
|
||||
/** Password in chiaro – viene cifrata lato server. Stringa vuota = cancella */
|
||||
@@ -41,6 +45,13 @@ export interface TenantSettingsUpdate {
|
||||
archival_notes?: string
|
||||
}
|
||||
|
||||
export interface ConservatoreTestResult {
|
||||
success: boolean
|
||||
message: string
|
||||
latency_ms: number | null
|
||||
provider_info: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
// ─── Tipi indicizzazione full-text ─────────────────────────────────────────
|
||||
|
||||
export interface IndexingStats {
|
||||
@@ -76,20 +87,35 @@ export interface IndexingJobStatus {
|
||||
|
||||
export const settingsApi = {
|
||||
/**
|
||||
* Recupera le impostazioni del tenant corrente.
|
||||
* Recupera le impostazioni del tenant.
|
||||
* Se non esistono, il backend le crea con i valori di default (mock).
|
||||
* @param tenantId - (solo super_admin) UUID del tenant target
|
||||
*/
|
||||
get: async (): Promise<TenantSettingsResponse> => {
|
||||
const { data } = await apiClient.get<TenantSettingsResponse>('/settings')
|
||||
get: async (tenantId?: string): Promise<TenantSettingsResponse> => {
|
||||
const params = tenantId ? { tenant_id: tenantId } : undefined
|
||||
const { data } = await apiClient.get<TenantSettingsResponse>('/settings', { params })
|
||||
return data
|
||||
},
|
||||
|
||||
/**
|
||||
* Aggiorna le impostazioni del tenant.
|
||||
* Solo i campi forniti vengono modificati (semantica PATCH).
|
||||
* @param tenantId - (solo super_admin) UUID del tenant target
|
||||
*/
|
||||
update: async (payload: TenantSettingsUpdate): Promise<TenantSettingsResponse> => {
|
||||
const { data } = await apiClient.put<TenantSettingsResponse>('/settings', payload)
|
||||
update: async (payload: TenantSettingsUpdate, tenantId?: string): Promise<TenantSettingsResponse> => {
|
||||
const params = tenantId ? { tenant_id: tenantId } : undefined
|
||||
const { data } = await apiClient.put<TenantSettingsResponse>('/settings', payload, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
/**
|
||||
* Testa la connessione al conservatore configurato.
|
||||
* Effettua una chiamata reale (login + verifica identità) senza modificare dati.
|
||||
* @param tenantId - (solo super_admin) UUID del tenant target
|
||||
*/
|
||||
testConservatore: async (tenantId?: string): Promise<ConservatoreTestResult> => {
|
||||
const params = tenantId ? { tenant_id: tenantId } : undefined
|
||||
const { data } = await apiClient.post<ConservatoreTestResult>('/settings/test-conservatore', undefined, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
CheckCircle,
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Label } from '@/components/ui/Label'
|
||||
@@ -174,6 +175,14 @@ export function MailboxesPage() {
|
||||
{mailbox.provider}
|
||||
</span>
|
||||
)}
|
||||
{/* Badge REM europea (Feature N8) */}
|
||||
{mailbox.protocol_type === 'rem_eu' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium text-blue-700 bg-blue-100">
|
||||
<Globe className="h-3 w-3" />
|
||||
REM EU
|
||||
{mailbox.rem_provider && ` · ${mailbox.rem_provider}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{mailbox.email_address}</p>
|
||||
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
|
||||
@@ -278,6 +287,7 @@ function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFo
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<MailboxCreateRequest>({
|
||||
defaultValues: editingMailbox
|
||||
@@ -285,6 +295,8 @@ function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFo
|
||||
email_address: editingMailbox.email_address,
|
||||
display_name: editingMailbox.display_name || '',
|
||||
provider: editingMailbox.provider || '',
|
||||
protocol_type: editingMailbox.protocol_type || 'pec_it',
|
||||
rem_provider: editingMailbox.rem_provider || '',
|
||||
imap_host: editingMailbox.imap_host,
|
||||
imap_port: editingMailbox.imap_port,
|
||||
imap_user: editingMailbox.email_address,
|
||||
@@ -295,6 +307,7 @@ function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFo
|
||||
smtp_use_tls: editingMailbox.smtp_use_tls,
|
||||
}
|
||||
: {
|
||||
protocol_type: 'pec_it',
|
||||
imap_port: 993,
|
||||
smtp_port: 465,
|
||||
imap_use_ssl: true,
|
||||
@@ -302,6 +315,10 @@ function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFo
|
||||
},
|
||||
})
|
||||
|
||||
// Osserva il campo protocol_type per mostrare/nascondere rem_provider
|
||||
const selectedProtocol = useWatch({ control, name: 'protocol_type' })
|
||||
const isRemEu = selectedProtocol === 'rem_eu'
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: mailboxesApi.create,
|
||||
onSuccess: () => {
|
||||
@@ -363,6 +380,41 @@ function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Protocollo (Feature N8 – REM europea) */}
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="text-sm font-semibold flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
Protocollo
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Tipo protocollo</Label>
|
||||
<select
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
{...register('protocol_type')}
|
||||
>
|
||||
<option value="pec_it">PEC italiana (default)</option>
|
||||
<option value="rem_eu">REM europea (ETSI EN 319 532-4)</option>
|
||||
</select>
|
||||
</div>
|
||||
{isRemEu && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Provider REM europeo</Label>
|
||||
<Input
|
||||
placeholder="es. docutel, anodet, de-mail"
|
||||
{...register('rem_provider')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isRemEu && (
|
||||
<p className="text-xs text-blue-700 bg-blue-50 rounded p-2">
|
||||
Casella configurata per REM europea (ETSI EN 319 532-4). I messaggi in arrivo
|
||||
con header X-REM-* verranno classificati automaticamente con il parser REM.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Separatore IMAP */}
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="text-sm font-semibold">Configurazione IMAP (ricezione)</h4>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
@@ -309,8 +309,22 @@ function TaxonomyWidget({ messageId, messageLabels }: { messageId: string; messa
|
||||
queryFn: () => labelsApi.list(),
|
||||
})
|
||||
|
||||
// Le label tassonomiche del messaggio sono quelle con parent_id != null
|
||||
const taxonomyLabels = messageLabels.filter((l) => l.parent_id !== null)
|
||||
// Le label tassonomiche del messaggio: nodi figli (parent_id != null) PIU' i nodi
|
||||
// radice che hanno almeno un figlio nell'albero (identificati come parent_id di
|
||||
// qualche altra label). Questo permette di visualizzare anche gli Ambiti (livello 0)
|
||||
// quando sono assegnati direttamente a un messaggio.
|
||||
const taxonomyIdSet = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
allLabels.forEach((l: LabelResponse) => {
|
||||
if (l.parent_id !== null) {
|
||||
ids.add(l.id) // nodo figlio -> tassonomico
|
||||
ids.add(l.parent_id) // suo genitore radice -> anch'esso tassonomico
|
||||
}
|
||||
})
|
||||
return ids
|
||||
}, [allLabels])
|
||||
|
||||
const taxonomyLabels = messageLabels.filter((l) => taxonomyIdSet.has(l.id))
|
||||
|
||||
// Costruisce il percorso completo di una label: "Ambito > Processo > Classificazione"
|
||||
function buildPath(labelId: string): string {
|
||||
|
||||
@@ -37,10 +37,13 @@ import {
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
import { usersApi } from '@/api/users.api'
|
||||
import { tenantsApi } from '@/api/tenants.api'
|
||||
import type { TenantResponse } from '@/types/api.types'
|
||||
import {
|
||||
settingsApi,
|
||||
type TenantSettingsResponse,
|
||||
type ArchivalMode,
|
||||
type ConservatoreTestResult,
|
||||
type IndexingStats,
|
||||
type IndexingJobStatus,
|
||||
type ReindexMode,
|
||||
@@ -973,32 +976,53 @@ export function SettingsPage() {
|
||||
const [loadingArchival, setLoadingArchival] = useState(false)
|
||||
const [archivalExpanded, setArchivalExpanded] = useState(false)
|
||||
|
||||
// Selector tenant per super_admin
|
||||
const [tenantList, setTenantList] = useState<TenantResponse[]>([])
|
||||
const [selectedTenantId, setSelectedTenantId] = useState<string | undefined>(undefined)
|
||||
|
||||
// Form archiviazione
|
||||
const [archivalMode, setArchivalMode] = useState<ArchivalMode>('mock')
|
||||
const [conservatoreId, setConservatoreId] = useState('')
|
||||
const [conservatoreEndpoint, setConservatoreEndpoint] = useState('')
|
||||
const [conservatoreTenantSlug, setConservatoreTenantSlug] = useState('')
|
||||
const [conservatoreUsername, setConservatoreUsername] = useState('')
|
||||
const [conservatorePassword, setConservatorePassword] = useState('')
|
||||
const [archivalNotes, setArchivalNotes] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [savingArchival, setSavingArchival] = useState(false)
|
||||
const [showProductionConfirm, setShowProductionConfirm] = useState(false)
|
||||
const [testingConnection, setTestingConnection] = useState(false)
|
||||
const [testResult, setTestResult] = useState<ConservatoreTestResult | null>(null)
|
||||
|
||||
/* ── Carica lista tenant (solo super_admin) ── */
|
||||
useEffect(() => {
|
||||
if (isSuperAdmin) {
|
||||
tenantsApi.list().then(setTenantList).catch(() => {
|
||||
// Silenzioso: la lista e' un extra di comodita'
|
||||
})
|
||||
}
|
||||
}, [isSuperAdmin])
|
||||
|
||||
/* ── Carica impostazioni archiviazione ── */
|
||||
useEffect(() => {
|
||||
if (isAdmin) {
|
||||
loadArchivalSettings()
|
||||
loadArchivalSettings(selectedTenantId)
|
||||
}
|
||||
}, [isAdmin])
|
||||
}, [isAdmin, selectedTenantId])
|
||||
|
||||
const loadArchivalSettings = async () => {
|
||||
const loadArchivalSettings = async (tenantId?: string) => {
|
||||
setLoadingArchival(true)
|
||||
// Resetta credenziali ogni volta che si cambia tenant
|
||||
setConservatoreUsername('')
|
||||
setConservatorePassword('')
|
||||
setTestResult(null)
|
||||
try {
|
||||
const data = await settingsApi.get()
|
||||
const data = await settingsApi.get(tenantId)
|
||||
setArchivalSettings(data)
|
||||
setArchivalMode(data.archival_mode)
|
||||
setConservatoreId(data.conservatore_id)
|
||||
setConservatoreEndpoint(data.conservatore_endpoint ?? '')
|
||||
setConservatoreTenantSlug(data.conservatore_tenant_slug ?? '')
|
||||
setArchivalNotes(data.archival_notes ?? '')
|
||||
} catch {
|
||||
toast.error('Errore durante il caricamento delle impostazioni di archiviazione')
|
||||
@@ -1050,6 +1074,27 @@ export function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Test connessione conservatore ── */
|
||||
const handleTestConnection = async () => {
|
||||
setTestingConnection(true)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const result = await settingsApi.testConservatore(selectedTenantId)
|
||||
setTestResult(result)
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { response?: { data?: { detail?: string } } })
|
||||
?.response?.data?.detail
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: msg ?? 'Errore durante il test di connessione',
|
||||
latency_ms: null,
|
||||
provider_info: null,
|
||||
})
|
||||
} finally {
|
||||
setTestingConnection(false)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Cambio modalita' archiviazione ── */
|
||||
const handleModeToggle = (newMode: ArchivalMode) => {
|
||||
if (newMode === 'production' && archivalMode === 'mock') {
|
||||
@@ -1073,21 +1118,24 @@ export function SettingsPage() {
|
||||
archival_mode: archivalMode,
|
||||
conservatore_id: conservatoreId || undefined,
|
||||
conservatore_endpoint: conservatoreEndpoint || undefined,
|
||||
conservatore_tenant_slug: conservatoreTenantSlug || undefined,
|
||||
archival_notes: archivalNotes || undefined,
|
||||
}
|
||||
|
||||
if (conservatoreUsername) payload.conservatore_username = conservatoreUsername
|
||||
if (conservatorePassword) payload.conservatore_password = conservatorePassword
|
||||
|
||||
const updated = await settingsApi.update(payload)
|
||||
const updated = await settingsApi.update(payload, selectedTenantId)
|
||||
setArchivalSettings(updated)
|
||||
setArchivalMode(updated.archival_mode)
|
||||
setConservatoreId(updated.conservatore_id)
|
||||
setConservatoreEndpoint(updated.conservatore_endpoint ?? '')
|
||||
setConservatoreTenantSlug(updated.conservatore_tenant_slug ?? '')
|
||||
setArchivalNotes(updated.archival_notes ?? '')
|
||||
setConservatoreUsername('')
|
||||
setConservatorePassword('')
|
||||
setShowProductionConfirm(false)
|
||||
setTestResult(null)
|
||||
|
||||
toast.success(
|
||||
updated.archival_mode === 'production'
|
||||
@@ -1231,6 +1279,41 @@ export function SettingsPage() {
|
||||
|
||||
{archivalExpanded && (
|
||||
<div className="mt-5 space-y-5">
|
||||
|
||||
{/* ── Selector tenant (solo super_admin) ── */}
|
||||
{isSuperAdmin && tenantList.length > 0 && (
|
||||
<div className="rounded-lg bg-purple-50 border border-purple-200 p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-3.5 w-3.5 text-purple-600" />
|
||||
<span className="text-xs font-semibold text-purple-800 uppercase tracking-wide">
|
||||
Gestione per tenant (Super Admin)
|
||||
</span>
|
||||
</div>
|
||||
<select
|
||||
className="w-full rounded-md border border-purple-300 bg-white px-3 py-2 text-sm text-gray-800 shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
value={selectedTenantId ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setSelectedTenantId(val || undefined)
|
||||
setShowProductionConfirm(false)
|
||||
}}
|
||||
>
|
||||
<option value="">— Il tuo tenant (super_admin) —</option>
|
||||
{tenantList.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name} ({t.slug}){!t.is_active ? ' — SOSPESO' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedTenantId && (
|
||||
<p className="text-xs text-purple-700">
|
||||
Stai modificando le impostazioni del tenant selezionato.
|
||||
Le modifiche vengono salvate separatamente per ogni tenant.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingArchival ? (
|
||||
<p className="text-sm text-gray-500 py-4 text-center">
|
||||
Caricamento impostazioni...
|
||||
@@ -1334,7 +1417,21 @@ export function SettingsPage() {
|
||||
id="conservatore_endpoint"
|
||||
value={conservatoreEndpoint}
|
||||
onChange={(e) => setConservatoreEndpoint(e.target.value)}
|
||||
placeholder="https://conservatore.provider.it/api/v1"
|
||||
placeholder="https://api.aeterna.idrainformatica.it"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="conservatore_tenant_slug">
|
||||
Tenant Slug
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">(es. Aeterna: slug organizzazione)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="conservatore_tenant_slug"
|
||||
value={conservatoreTenantSlug}
|
||||
onChange={(e) => setConservatoreTenantSlug(e.target.value)}
|
||||
placeholder="es. pechub"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1398,6 +1495,7 @@ export function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Riepilogo configurazione salvata ── */}
|
||||
{archivalSettings && (
|
||||
<div className={`rounded-lg p-3 text-xs border ${
|
||||
archivalSettings.archival_mode === 'production'
|
||||
@@ -1411,6 +1509,9 @@ export function SettingsPage() {
|
||||
{archivalSettings.conservatore_endpoint && (
|
||||
<li>Endpoint: <strong className="font-mono text-xs break-all">{archivalSettings.conservatore_endpoint}</strong></li>
|
||||
)}
|
||||
{archivalSettings.conservatore_tenant_slug && (
|
||||
<li>Tenant Slug: <strong>{archivalSettings.conservatore_tenant_slug}</strong></li>
|
||||
)}
|
||||
<li>Credenziali: {
|
||||
archivalSettings.conservatore_username_configured && archivalSettings.conservatore_password_configured
|
||||
? <strong className="text-green-700">Configurate</strong>
|
||||
@@ -1420,7 +1521,51 @@ export function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-1">
|
||||
{/* ── Risultato test connessione ── */}
|
||||
{testResult && (
|
||||
<div className={`rounded-lg p-3 text-xs border flex items-start gap-2 ${
|
||||
testResult.success
|
||||
? 'bg-green-50 border-green-200 text-green-800'
|
||||
: 'bg-red-50 border-red-200 text-red-800'
|
||||
}`}>
|
||||
{testResult.success
|
||||
? <CheckCircle className="h-4 w-4 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
: <AlertTriangle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
}
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="font-medium">{testResult.success ? 'Connessione riuscita' : 'Connessione fallita'}</p>
|
||||
<p>{testResult.message}</p>
|
||||
{testResult.latency_ms !== null && (
|
||||
<p className="text-gray-500">Latenza: {testResult.latency_ms} ms</p>
|
||||
)}
|
||||
{testResult.provider_info && testResult.success && (
|
||||
<div className="mt-1 text-gray-600 space-y-0.5">
|
||||
{Object.entries(testResult.provider_info).map(([k, v]) => (
|
||||
<div key={k}>
|
||||
<span className="text-gray-400">{k}: </span>
|
||||
<span>{String(v)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Azioni ── */}
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testingConnection || archivalMode !== 'production'}
|
||||
className="flex items-center gap-1.5 text-blue-700 border-blue-300 hover:bg-blue-50 disabled:opacity-50"
|
||||
>
|
||||
{testingConnection
|
||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||
: <RefreshCw className="h-4 w-4" />
|
||||
}
|
||||
{testingConnection ? 'Test in corso...' : 'Testa connessione'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveArchival}
|
||||
disabled={savingArchival}
|
||||
|
||||
@@ -102,6 +102,8 @@ export interface UserUpdateRequest {
|
||||
|
||||
export type MailboxStatus = 'active' | 'paused' | 'error' | 'deleted'
|
||||
|
||||
export type MailboxProtocol = 'pec_it' | 'rem_eu'
|
||||
|
||||
export interface MailboxResponse {
|
||||
id: string
|
||||
tenant_id: string
|
||||
@@ -114,6 +116,9 @@ export interface MailboxResponse {
|
||||
smtp_host: string
|
||||
smtp_port: number
|
||||
smtp_use_tls: boolean
|
||||
// Protocollo (Feature N8 – REM europea)
|
||||
protocol_type: MailboxProtocol
|
||||
rem_provider: string | null
|
||||
status: MailboxStatus
|
||||
last_sync_at: string | null
|
||||
last_sync_uid: number | null
|
||||
@@ -135,6 +140,9 @@ export interface MailboxCreateRequest {
|
||||
email_address: string
|
||||
display_name?: string
|
||||
provider?: string
|
||||
// Protocollo (Feature N8 – REM europea)
|
||||
protocol_type?: MailboxProtocol
|
||||
rem_provider?: string
|
||||
imap_host: string
|
||||
imap_port: number
|
||||
imap_user: string
|
||||
@@ -151,6 +159,9 @@ export interface MailboxUpdateRequest {
|
||||
display_name?: string
|
||||
provider?: string
|
||||
status?: 'active' | 'paused'
|
||||
// Protocollo (Feature N8)
|
||||
protocol_type?: MailboxProtocol
|
||||
rem_provider?: string | null
|
||||
imap_host?: string
|
||||
imap_port?: number
|
||||
imap_user?: string
|
||||
|
||||
Reference in New Issue
Block a user