mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Visualizzazione ricevute
This commit is contained in:
@@ -104,4 +104,24 @@ export const messagesApi = {
|
||||
|
||||
getReceipts: (id: string) =>
|
||||
apiClient.get<MessageResponse[]>(`/messages/${id}/receipts`).then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Scarica il pacchetto ZIP completo della PEC (postacert.eml, daticert.xml,
|
||||
* ricevute di accettazione/consegna per le mail outbound).
|
||||
*/
|
||||
downloadPackage: async (messageId: string, subject?: string | null): Promise<void> => {
|
||||
const response = await apiClient.get(
|
||||
`/messages/${messageId}/download-package`,
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
const blobUrl = window.URL.createObjectURL(new Blob([response.data], { type: 'application/zip' }))
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = blobUrl
|
||||
const safeSubject = (subject || 'pec').replace(/[/\\]/g, '_').slice(0, 50)
|
||||
anchor.setAttribute('download', `pec_${safeSubject}.zip`)
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChevronDown, ChevronRight, Mail } from 'lucide-react'
|
||||
import { ChevronDown, ChevronRight, Mail, ExternalLink } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PecTypeBadge, PecStateBadge } from '@/components/PecBadge/PecBadge'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import type { MessageResponse } from '@/types/api.types'
|
||||
@@ -12,9 +13,11 @@ interface ReceiptTreeProps {
|
||||
/**
|
||||
* Visualizza la gerarchia delle ricevute PEC collegate a un messaggio.
|
||||
* Mostra in ordine cronologico: accettazione → consegna (o anomalia).
|
||||
* Le ricevute sono cliccabili e navigano al dettaglio del messaggio ricevuta.
|
||||
*/
|
||||
export function ReceiptTree({ message, receipts }: ReceiptTreeProps) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (receipts.length === 0) {
|
||||
if (message.direction === 'outbound') {
|
||||
@@ -66,6 +69,7 @@ export function ReceiptTree({ message, receipts }: ReceiptTreeProps) {
|
||||
date={receipt.received_at || receipt.created_at}
|
||||
type={receipt.pec_type}
|
||||
messageId={receipt.id}
|
||||
onClick={() => navigate(`/messages/${receipt.id}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -81,25 +85,41 @@ interface ReceiptNodeProps {
|
||||
type?: MessageResponse['pec_type']
|
||||
messageId?: string
|
||||
isRoot?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function ReceiptNode({ label, date, state, type, isRoot }: ReceiptNodeProps) {
|
||||
return (
|
||||
function ReceiptNode({ label, date, state, type, isRoot, onClick }: ReceiptNodeProps) {
|
||||
const isClickable = !!onClick
|
||||
|
||||
const content = (
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Indicatore timeline */}
|
||||
<div className="mt-1 flex flex-col items-center">
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full border-2 ${
|
||||
isRoot ? 'border-primary bg-primary/20' : 'border-muted-foreground bg-muted'
|
||||
isRoot
|
||||
? 'border-primary bg-primary/20'
|
||||
: isClickable
|
||||
? 'border-blue-500 bg-blue-100'
|
||||
: 'border-muted-foreground bg-muted'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium truncate">{label}</span>
|
||||
<span
|
||||
className={`text-sm font-medium truncate ${
|
||||
isClickable ? 'text-blue-600 group-hover:underline' : ''
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{state && !isRoot && <PecStateBadge state={state} />}
|
||||
{type && type !== 'posta_certificata' && <PecTypeBadge type={type} />}
|
||||
{isClickable && (
|
||||
<ExternalLink className="h-3.5 w-3.5 text-blue-400 group-hover:text-blue-600 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
{date && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{formatDate(date)}</p>
|
||||
@@ -107,4 +127,19 @@ function ReceiptNode({ label, date, state, type, isRoot }: ReceiptNodeProps) {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (isClickable) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="w-full text-left group rounded-md px-2 py-1 -mx-2 hover:bg-muted/50 transition-colors cursor-pointer"
|
||||
title="Apri dettaglio ricevuta"
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return <div>{content}</div>
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
MailX,
|
||||
PackageOpen,
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@@ -33,6 +34,7 @@ export function MessageDetailPage() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||
const [isDownloadingPackage, setIsDownloadingPackage] = useState(false)
|
||||
|
||||
// Carica messaggio
|
||||
const {
|
||||
@@ -136,6 +138,20 @@ export function MessageDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Download pacchetto completo ZIP
|
||||
const handleDownloadPackage = async () => {
|
||||
if (!message) return
|
||||
setIsDownloadingPackage(true)
|
||||
try {
|
||||
await messagesApi.downloadPackage(message.id, message.subject)
|
||||
toast.success('Pacchetto scaricato')
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error))
|
||||
} finally {
|
||||
setIsDownloadingPackage(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Imposta tag del messaggio
|
||||
const setLabelsMutation = useMutation({
|
||||
mutationFn: (labelIds: string[]) =>
|
||||
@@ -313,6 +329,22 @@ export function MessageDetailPage() {
|
||||
Rispondi
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Scarica pacchetto completo ZIP (sempre visibile) */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDownloadPackage}
|
||||
disabled={isDownloadingPackage}
|
||||
title="Scarica pacchetto completo (ZIP con tutti i file originali)"
|
||||
>
|
||||
{isDownloadingPackage ? (
|
||||
<div className="h-4 w-4 mr-1 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<PackageOpen className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
Scarica
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user