feat(docs-picker): dedup per categoria + distinzione admin/azienda + riga doc 3-stati
Dopo test browser ampio con Carlo sono emersi 3 cose importanti:
1. LOGICA: il BE Spring Gepafin non implementa sostituzione automatica
dei documenti stessa categoria — uploadFileForCompany fa saveAll puro,
nessun softdelete del precedente, nessun UNIQUE constraint su
(company_id, document_category_id). In sandbox risultavano 2 DURC
VALID attivi simultaneamente. Mitigazione picker: dedup client-side
per categoria, preferenza VALID > DUE, a parita id desc.
2. CATEGORIE: il BE ha 3 macro-tipi (CompanyDocumentTypeEnum):
COMPANY_DOCUMENT (azienda — DURC/Visura/Bilancio),
PERSONAL_DOCUMENT (amministratore/legale rappresentante — CI/CF/antiric),
APPLICATION_DOCUMENT (legato a specifica application).
Carlo aveva intuito giusto: admin vs azienda e la divisione personal vs
company. Il picker ora fa 2 chiamate al BE (default retituisce
COMPANY+APPLICATION, poi filter esplicito documentType=PERSONAL_DOCUMENT)
e unisce i risultati con dedup.
3. UX RIGA DOCUMENTO: il layout che avevo fatto (FileUploadCell + Tag
esterno + icon button) rompeva il flex causa nesting, tag e refresh
andavano a capo. Separati 3 casi semantici puliti:
- CASO A repository: riga custom con [icona pdf] filename [tag stato]
[button Cambia] [button Rimuovi] — tutto orizzontale
- CASO B upload PC: FileUploadCell standard (preview/download/refresh/delete)
- CASO C vuoto: 2 pulsanti Carica dal PC / Scegli dal repository
CAMBIAMENTI CompanyDocumentPicker.js (+197 -52):
- dedupByCategory(docs) con ranking STATUS+id
- doppia chiamata getCompanyDocuments (default + PERSONAL)
- nuova colonna Origine con badge colorato (blu=Azienda, viola=Admin)
- 3 pulsanti manuali per tab Origine (SelectButton PrimeReact aveva
issues styling col tema Gepafin: label bianche invisibili sui non-selected)
- ORIGIN_CFG con 3 varianti per COMPANY/PERSONAL/APPLICATION
CAMBIAMENTI PraticaRendicontazioneEdit.js (+55 -17):
- riga doc riscritta con 3 branch distinti per stato
- pulsanti outlined con label esplicite 'Cambia' / 'Rimuovi' (icon-only
text button avevano scopribilita bassa)
- handler Rimuovi collegato a deleteEntityFile esistente + toast feedback
Test browser verificato con Carlo: dedup ok (2 DURC in DB → 1 nel
picker), tabs Azienda/Amministratore leggibili, label pulsanti chiare,
flusso Cambia/Rimuovi funziona.
This commit is contained in:
@@ -13,45 +13,55 @@ import { Message } from 'primereact/message';
|
||||
import CompanyDocumentsService from '../../../service/company-documents-service';
|
||||
|
||||
/**
|
||||
* Modal per selezionare un documento dal repository della company.
|
||||
* Modal per scegliere un documento dal repository della company.
|
||||
*
|
||||
* Mostra tutti i company_document della company (BE Gepafin /companyDocument/company/{id}),
|
||||
* filtri per categoria/stato/ricerca, selezione single-row via DataTable, semaforo stato.
|
||||
* Carica tramite 2 chiamate al BE Spring:
|
||||
* - default (no filter) -> COMPANY_DOCUMENT + APPLICATION_DOCUMENT
|
||||
* - documentType=PERSONAL -> PERSONAL_DOCUMENT (amministratore/legale rappresentante)
|
||||
*
|
||||
* Unione dei risultati e deduplicazione per document_category_id scegliendo
|
||||
* il piu recente attivo (VALID > DUE, a parita id desc). Questo mitiga il
|
||||
* fatto che il BE Gepafin non implementa sostituzione automatica su upload
|
||||
* stessa categoria (es. 2 DURC attivi in DB).
|
||||
*
|
||||
* Props:
|
||||
* visible boolean
|
||||
* companyId number
|
||||
* onHide () => void
|
||||
* onSelect (doc) => void — doc: {id, fileName, status, expirationDate, category, ...}
|
||||
* currentSourceId number | null — id gia selezionato, per evidenziarlo
|
||||
*
|
||||
* Nota: il BE Spring filtra fuori i doc EXPIRED — non compariranno qui.
|
||||
* Il gate submit comunque blocca via JOIN live lato microservizio.
|
||||
* visible, companyId, onHide, onSelect(doc), currentSourceId
|
||||
*/
|
||||
const STATUS_CFG = {
|
||||
VALID: { severity: 'success', label: 'Valido', icon: 'pi pi-check-circle', color: 'var(--green-500)' },
|
||||
DUE: { severity: 'warning', label: 'In scadenza', icon: 'pi pi-exclamation-triangle', color: 'var(--yellow-500)' },
|
||||
EXPIRED: { severity: 'danger', label: 'Scaduto', icon: 'pi pi-times-circle', color: 'var(--red-500)' },
|
||||
VALID: { severity: 'success', label: 'Valido', icon: 'pi pi-check-circle' },
|
||||
DUE: { severity: 'warning', label: 'In scadenza', icon: 'pi pi-exclamation-triangle' },
|
||||
EXPIRED: { severity: 'danger', label: 'Scaduto', icon: 'pi pi-times-circle' },
|
||||
};
|
||||
|
||||
const ORIGIN_CFG = {
|
||||
COMPANY_DOCUMENT: { label: 'Azienda', icon: 'pi pi-building', color: 'var(--blue-500)', bg: 'var(--blue-50)' },
|
||||
PERSONAL_DOCUMENT: { label: 'Amministratore', icon: 'pi pi-user', color: 'var(--purple-500)', bg: 'var(--purple-50)' },
|
||||
APPLICATION_DOCUMENT: { label: 'Applicazione', icon: 'pi pi-file', color: 'var(--teal-500)', bg: 'var(--teal-50)' },
|
||||
};
|
||||
|
||||
const formatDate = (d) => {
|
||||
if (!d) return '—';
|
||||
try {
|
||||
const dt = typeof d === 'string' ? new Date(d) : d;
|
||||
return dt.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
} catch { return String(d); }
|
||||
try { return new Date(d).toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
|
||||
catch { return String(d); }
|
||||
};
|
||||
|
||||
const daysUntil = (d) => {
|
||||
if (!d) return null;
|
||||
try {
|
||||
const dt = typeof d === 'string' ? new Date(d) : d;
|
||||
const ms = dt.getTime() - Date.now();
|
||||
return Math.ceil(ms / (1000 * 60 * 60 * 24));
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
const daysUntil = (d) => { if (!d) return null; try { return Math.ceil((new Date(d).getTime() - Date.now()) / 86400000); } catch { return null; } };
|
||||
const getCategoryName = (d) => (d.category && d.category.categoryName) || d.type || '—';
|
||||
const getOriginType = (d) => d.type || 'COMPANY_DOCUMENT';
|
||||
|
||||
// dedup: per ogni (type + category), mantieni il piu recente attivo. VALID > DUE, poi id desc
|
||||
const dedupByCategory = (docs) => {
|
||||
const STATUS_RANK = { VALID: 0, DUE: 1, EXPIRED: 2 };
|
||||
const groups = new Map();
|
||||
for (const d of docs) {
|
||||
const key = `${d.type || 'X'}::${getCategoryName(d)}`;
|
||||
const rank = (STATUS_RANK[d.status] ?? 99) * 1e12 - (d.id || 0);
|
||||
const prev = groups.get(key);
|
||||
if (!prev || rank < prev._rank) {
|
||||
groups.set(key, { ...d, _rank: rank });
|
||||
}
|
||||
}
|
||||
return Array.from(groups.values()).map(({ _rank, ...rest }) => rest);
|
||||
};
|
||||
|
||||
const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSourceId = null }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -62,32 +72,35 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
const [search, setSearch] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState(null);
|
||||
const [statusFilter, setStatusFilter] = useState(null);
|
||||
const [originFilter, setOriginFilter] = useState('ALL'); // ALL | COMPANY_DOCUMENT | PERSONAL_DOCUMENT
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !companyId) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSelectedId(currentSourceId);
|
||||
setSearch(''); setCategoryFilter(null); setStatusFilter(null); setOriginFilter('ALL');
|
||||
|
||||
// doppia chiamata: company/application + personal
|
||||
let collected = [];
|
||||
let pending = 2;
|
||||
const done = () => { if (--pending === 0) { setDocs(dedupByCategory(collected)); setLoading(false); } };
|
||||
|
||||
CompanyDocumentsService.getCompanyDocuments(
|
||||
companyId,
|
||||
(resp) => {
|
||||
const data = resp?.status === 'SUCCESS' ? (resp.data || []) : [];
|
||||
setDocs(data);
|
||||
setLoading(false);
|
||||
},
|
||||
() => {
|
||||
setError(__('Impossibile caricare i documenti del repository. Riprova tra qualche istante.', 'gepafin'));
|
||||
setLoading(false);
|
||||
}
|
||||
(resp) => { if (resp?.status === 'SUCCESS') collected = collected.concat(resp.data || []); done(); },
|
||||
() => { setError(__('Impossibile caricare i documenti del repository.', 'gepafin')); done(); }
|
||||
);
|
||||
CompanyDocumentsService.getCompanyDocuments(
|
||||
companyId,
|
||||
(resp) => { if (resp?.status === 'SUCCESS') collected = collected.concat(resp.data || []); done(); },
|
||||
() => done(),
|
||||
[['documentType', 'PERSONAL_DOCUMENT']]
|
||||
);
|
||||
setSearch(''); setCategoryFilter(null); setStatusFilter(null);
|
||||
}, [visible, companyId, currentSourceId]);
|
||||
|
||||
const categoryOptions = useMemo(() => {
|
||||
const uniq = Array.from(new Set(docs.map(getCategoryName).filter(Boolean))).sort();
|
||||
return uniq.map(t => ({ label: t, value: t }));
|
||||
}, [docs]);
|
||||
|
||||
// opzioni filtri derivate
|
||||
const categoryOptions = useMemo(() => Array.from(new Set(docs.map(getCategoryName).filter(Boolean))).sort().map(t => ({ label: t, value: t })), [docs]);
|
||||
const statusOptions = [
|
||||
{ label: __('Validi', 'gepafin'), value: 'VALID' },
|
||||
{ label: __('In scadenza', 'gepafin'), value: 'DUE' },
|
||||
@@ -96,6 +109,7 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
const filteredDocs = useMemo(() => {
|
||||
const s = (search || '').trim().toLowerCase();
|
||||
return docs.filter(d => {
|
||||
if (originFilter !== 'ALL' && getOriginType(d) !== originFilter) return false;
|
||||
if (categoryFilter && getCategoryName(d) !== categoryFilter) return false;
|
||||
if (statusFilter && d.status !== statusFilter) return false;
|
||||
if (s) {
|
||||
@@ -104,7 +118,7 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [docs, search, categoryFilter, statusFilter]);
|
||||
}, [docs, search, categoryFilter, statusFilter, originFilter]);
|
||||
|
||||
// templates colonne
|
||||
const nameTpl = (row) => (
|
||||
@@ -114,42 +128,42 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
<strong style={{ fontSize: '0.95rem' }}>{row.fileName || row.name || `Doc #${row.id}`}</strong>
|
||||
</div>
|
||||
{row.name && row.name !== row.fileName && (
|
||||
<div style={{ marginLeft: '1.6rem' }}>
|
||||
<small className="text-color-secondary">{row.name}</small>
|
||||
</div>
|
||||
<div style={{ marginLeft: '1.6rem' }}><small className="text-color-secondary">{row.name}</small></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const originTpl = (row) => {
|
||||
const cfg = ORIGIN_CFG[getOriginType(row)] || ORIGIN_CFG.COMPANY_DOCUMENT;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
|
||||
background: cfg.bg, color: cfg.color, padding: '0.2rem 0.6rem',
|
||||
borderRadius: '12px', fontSize: '0.8rem', fontWeight: 500
|
||||
}}>
|
||||
<i className={cfg.icon} style={{ fontSize: '0.8rem' }} />
|
||||
{cfg.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
const categoryTpl = (row) => (
|
||||
<Tag value={getCategoryName(row)} severity="secondary"
|
||||
style={{ fontFamily: 'monospace', fontSize: '0.8rem' }} />
|
||||
);
|
||||
|
||||
const statusTpl = (row) => {
|
||||
const cfg = STATUS_CFG[row.status] || { severity: 'secondary', label: row.status || '—', icon: 'pi pi-question' };
|
||||
const days = daysUntil(row.expirationDate);
|
||||
return (
|
||||
<div>
|
||||
<Tag severity={cfg.severity} icon={cfg.icon} value={cfg.label}
|
||||
style={{ fontWeight: 600 }} />
|
||||
<Tag severity={cfg.severity} icon={cfg.icon} value={cfg.label} style={{ fontWeight: 600 }} />
|
||||
{row.status === 'DUE' && days != null && days >= 0 && (
|
||||
<div><small className="text-color-secondary">scade tra {days} {days === 1 ? 'giorno' : 'giorni'}</small></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const expiryTpl = (row) => <div style={{ fontFamily: 'monospace', fontSize: '0.9rem' }}>{formatDate(row.expirationDate)}</div>;
|
||||
|
||||
const expiryTpl = (row) => (
|
||||
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem' }}>
|
||||
{formatDate(row.expirationDate)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const chosenDoc = useMemo(
|
||||
() => docs.find(d => d.id === selectedId) || null,
|
||||
[docs, selectedId]
|
||||
);
|
||||
const chosenDoc = useMemo(() => docs.find(d => d.id === selectedId) || null, [docs, selectedId]);
|
||||
|
||||
const footer = (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: '1rem' }}>
|
||||
@@ -168,8 +182,7 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" text onClick={onHide} />
|
||||
<Button label={__('Seleziona', 'gepafin')} icon="pi pi-check"
|
||||
disabled={!chosenDoc}
|
||||
<Button label={__('Seleziona', 'gepafin')} icon="pi pi-check" disabled={!chosenDoc}
|
||||
onClick={() => { if (chosenDoc) { onSelect(chosenDoc); onHide(); } }} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,30 +201,41 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog header={dialogHeader}
|
||||
visible={visible} onHide={onHide}
|
||||
style={{ width: '960px', maxWidth: '95vw' }}
|
||||
<Dialog header={dialogHeader} visible={visible} onHide={onHide}
|
||||
style={{ width: '1000px', maxWidth: '95vw' }}
|
||||
modal dismissableMask footer={footer}
|
||||
contentStyle={{ paddingTop: '1rem' }}>
|
||||
|
||||
{/* info banner */}
|
||||
<Message severity="info" style={{ width: '100%', marginBottom: '1.5rem' }}
|
||||
content={
|
||||
<small>{__('Seleziona un documento già caricato in fase di domanda. La scadenza viene controllata automaticamente — i documenti scaduti vengono esclusi dal repository.', 'gepafin')}</small>
|
||||
<small>{__('Seleziona un documento gia caricato. Se esistono piu versioni per la stessa categoria, viene mostrata solo la piu recente valida. I documenti scaduti sono esclusi.', 'gepafin')}</small>
|
||||
} />
|
||||
|
||||
{/* FILTRI — search full width, poi 2 dropdown affiancati con CSS grid rigido */}
|
||||
{/* tab origine — 3 pulsanti manuali (SelectButton di PrimeReact ha issues di styling col tema Gepafin) */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
||||
<Button type="button" size="small"
|
||||
label={__('Tutti', 'gepafin')}
|
||||
outlined={originFilter !== 'ALL'}
|
||||
onClick={() => setOriginFilter('ALL')}
|
||||
style={{ flex: 1 }} />
|
||||
<Button type="button" size="small" icon="pi pi-building"
|
||||
label={__('Azienda', 'gepafin')}
|
||||
outlined={originFilter !== 'COMPANY_DOCUMENT'}
|
||||
onClick={() => setOriginFilter('COMPANY_DOCUMENT')}
|
||||
style={{ flex: 1 }} />
|
||||
<Button type="button" size="small" icon="pi pi-user"
|
||||
label={__('Amministratore', 'gepafin')}
|
||||
outlined={originFilter !== 'PERSONAL_DOCUMENT'}
|
||||
onClick={() => setOriginFilter('PERSONAL_DOCUMENT')}
|
||||
style={{ flex: 1 }} />
|
||||
</div>
|
||||
|
||||
{/* filtri secondari */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '0.75rem', marginBottom: '1rem' }}>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<i className="pi pi-search" style={{
|
||||
position: 'absolute',
|
||||
left: '1rem',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
color: 'var(--text-color-secondary)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1
|
||||
}} />
|
||||
<i className="pi pi-search" style={{ position: 'absolute', left: '1rem', top: '50%',
|
||||
transform: 'translateY(-50%)', color: 'var(--text-color-secondary)',
|
||||
pointerEvents: 'none', zIndex: 1 }} />
|
||||
<InputText placeholder={__('Cerca per nome o categoria...', 'gepafin')}
|
||||
value={search} onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '100%', paddingLeft: '2.75rem' }} />
|
||||
@@ -226,13 +250,8 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABELLA */}
|
||||
{loading ? (
|
||||
<div>
|
||||
<Skeleton height="2.5rem" className="mb-2" />
|
||||
<Skeleton height="2.5rem" className="mb-2" />
|
||||
<Skeleton height="2.5rem" />
|
||||
</div>
|
||||
<div><Skeleton height="2.5rem" className="mb-2" /><Skeleton height="2.5rem" className="mb-2" /><Skeleton height="2.5rem" /></div>
|
||||
) : error ? (
|
||||
<Message severity="error" text={error} style={{ width: '100%' }} />
|
||||
) : (
|
||||
@@ -247,15 +266,15 @@ const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSo
|
||||
paginator rows={10}
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport"
|
||||
currentPageReportTemplate="{first}-{last} di {totalRecords}"
|
||||
selectionMode="single"
|
||||
selection={chosenDoc}
|
||||
selectionMode="single" selection={chosenDoc}
|
||||
onSelectionChange={(e) => e.value && setSelectedId(e.value.id)}
|
||||
rowClassName={(row) => row.id === selectedId ? 'p-highlight' : ''}
|
||||
style={{ cursor: 'pointer' }}>
|
||||
<Column header={__('Documento', 'gepafin')} body={nameTpl} style={{ minWidth: '280px' }} />
|
||||
<Column header={__('Categoria', 'gepafin')} body={categoryTpl} style={{ width: '180px' }} />
|
||||
<Column header={__('Stato', 'gepafin')} body={statusTpl} style={{ width: '170px' }} />
|
||||
<Column header={__('Scadenza', 'gepafin')} body={expiryTpl} style={{ width: '120px' }} />
|
||||
<Column header={__('Documento', 'gepafin')} body={nameTpl} style={{ minWidth: '240px' }} />
|
||||
<Column header={__('Origine', 'gepafin')} body={originTpl} style={{ width: '150px' }} />
|
||||
<Column header={__('Categoria', 'gepafin')} body={categoryTpl} style={{ width: '170px' }} />
|
||||
<Column header={__('Stato', 'gepafin')} body={statusTpl} style={{ width: '160px' }} />
|
||||
<Column header={__('Scadenza', 'gepafin')} body={expiryTpl} style={{ width: '110px' }} />
|
||||
</DataTable>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
@@ -656,41 +656,57 @@ const PraticaRendicontazioneEdit = () => {
|
||||
<div><small className="text-color-secondary"><code>{dr.code}</code></small></div>
|
||||
</div>
|
||||
<div style={{ flex: 2, minWidth: '260px' }}>
|
||||
{existing && existing.id && existing.filename ? (
|
||||
<div className="flex align-items-center gap-2 flex-wrap">
|
||||
<FileUploadCell
|
||||
entityType="document" entityId={existing.id}
|
||||
filename={existing.filename} sizeBytes={existing.size_bytes}
|
||||
readOnly={readOnly}
|
||||
onPreview={() => openPreview('document', existing.id, dr.label, existing.filename)}
|
||||
onChange={(meta) => updateDocFile(dr.code, existing.id, meta)}
|
||||
toastRef={toast}
|
||||
{existing && existing.id && existing.filename && existing.source_company_document_id ? (
|
||||
// CASO A: documento linkato dal repository company — layout custom
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'nowrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1, minWidth: 0 }}>
|
||||
<i className="pi pi-file-pdf" style={{ color: 'var(--primary-color)', fontSize: '1.1rem', flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
||||
{existing.filename}
|
||||
</span>
|
||||
</div>
|
||||
<Tag
|
||||
severity={existing.source_status === 'VALID' ? 'success'
|
||||
: existing.source_status === 'DUE' ? 'warning'
|
||||
: existing.source_status === 'EXPIRED' ? 'danger' : 'info'}
|
||||
icon={existing.source_status === 'VALID' ? 'pi pi-check-circle'
|
||||
: existing.source_status === 'DUE' ? 'pi pi-exclamation-triangle'
|
||||
: existing.source_status === 'EXPIRED' ? 'pi pi-times-circle' : 'pi pi-link'}
|
||||
value={existing.source_status === 'EXPIRED' ? __('Scaduto', 'gepafin')
|
||||
: existing.source_status === 'DUE' ? __('In scadenza', 'gepafin')
|
||||
: __('Dal repository', 'gepafin')}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
{existing.source_company_document_id && (
|
||||
<Tag
|
||||
severity={existing.source_status === 'VALID' ? 'success'
|
||||
: existing.source_status === 'DUE' ? 'warning'
|
||||
: existing.source_status === 'EXPIRED' ? 'danger' : 'info'}
|
||||
icon={existing.source_status === 'VALID' ? 'pi pi-check-circle'
|
||||
: existing.source_status === 'DUE' ? 'pi pi-exclamation-triangle'
|
||||
: existing.source_status === 'EXPIRED' ? 'pi pi-times-circle' : 'pi pi-link'}
|
||||
value={existing.source_status === 'EXPIRED' ? __('Scaduto', 'gepafin')
|
||||
: existing.source_status === 'DUE' ? __('In scadenza', 'gepafin')
|
||||
: existing.source_status === 'VALID' ? __('Dal repository', 'gepafin')
|
||||
: __('Dal repository', 'gepafin')}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button type="button" icon="pi pi-folder-open" size="small" text
|
||||
tooltip={__('Cambia: scegli dal repository', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
||||
onClick={() => openRepositoryPicker(dr.code)} />
|
||||
<>
|
||||
<Button type="button" icon="pi pi-pencil" size="small" outlined
|
||||
label={__('Cambia', 'gepafin')}
|
||||
onClick={() => openRepositoryPicker(dr.code)} />
|
||||
<Button type="button" icon="pi pi-trash" size="small" outlined severity="danger"
|
||||
label={__('Rimuovi', 'gepafin')}
|
||||
onClick={() => RendicontazioneService.deleteEntityFile('document', existing.id,
|
||||
() => updateDocFile(dr.code, existing.id, null),
|
||||
(err) => toast.current?.show({ severity: 'error', summary: __('Errore rimozione', 'gepafin'), detail: err?.detail }))
|
||||
} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : existing && existing.id && existing.filename ? (
|
||||
// CASO B: file caricato dal PC — usa FileUploadCell standard
|
||||
<FileUploadCell
|
||||
entityType="document" entityId={existing.id}
|
||||
filename={existing.filename} sizeBytes={existing.size_bytes}
|
||||
readOnly={readOnly}
|
||||
onPreview={() => openPreview('document', existing.id, dr.label, existing.filename)}
|
||||
onChange={(meta) => updateDocFile(dr.code, existing.id, meta)}
|
||||
toastRef={toast}
|
||||
/>
|
||||
) : !readOnly ? (
|
||||
<div className="flex gap-2 align-items-center flex-wrap">
|
||||
// CASO C: doc vuoto — 2 pulsanti
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Button type="button" icon="pi pi-upload" size="small" outlined
|
||||
label={__('Carica dal PC', 'gepafin')}
|
||||
onClick={() => ensureDocRecord(dr.code, () => {/* reload not needed */})} />
|
||||
onClick={() => ensureDocRecord(dr.code, () => {})} />
|
||||
<Button type="button" icon="pi pi-folder-open" size="small" outlined severity="secondary"
|
||||
label={__('Scegli dal repository', 'gepafin')}
|
||||
onClick={() => openRepositoryPicker(dr.code)} />
|
||||
|
||||
Reference in New Issue
Block a user