feat(docs): picker modal per scegliere documenti dal repository company

Chiude la promessa UX del super_admin (testo editor: 'I documenti già in regola nel
repository della Company saranno riutilizzati automaticamente'). Nel benef, oltre al
classico upload dal PC, e ora possibile pescare documenti dal repository Gepafin
della company, ereditando filename/scadenza e status live (VALID/DUE/EXPIRED).

Nuovi componenti:
- CompanyDocumentPicker.js (195 righe): Dialog PrimeReact con filtri tipo/stato/testo,
  DataTable con radio selection, semaforo tag VALID/DUE/EXPIRED, mostra scadenza
  formattata IT, pulsante conferma disabilitato finche nulla e selezionato.

Servizio:
- RendicontazioneService.linkDocumentFromRepository(remDocId, companyDocId, cb, err)
  chiama il nuovo endpoint microservizio POST .../document/{id}/link-from-repository.

Integrazione PraticaRendicontazioneEdit sezione 4 Documenti:
- 2 state + 2 handler nuovi: repoPicker {visible, docCode}, openRepositoryPicker,
  closeRepositoryPicker, handleRepositoryPick (ensureDocRecord -> link -> toast).
- UI riga documento richiesto ora ha 2 pulsanti quando vuoto:
    [pi-upload] Carica dal PC     [pi-folder-open] Scegli dal repository
- Quando linked: accanto al FileUploadCell compare Tag semaforo con lo status del
  sorgente (VALID=verde/DUE=giallo/EXPIRED=rosso) + pulsante cambia (ri-apre picker).
- CompanyDocumentPicker montato a fondo pagina, riceve practice.company_id +
  currentSourceId per evidenziare la scelta gia fatta.

Webpack compila pulito (solo warning no-unused-vars preesistenti non miei).
Test E2E backend gia verdi nel commit backend 7c8de6a.
This commit is contained in:
BFLOWS
2026-04-20 18:55:49 +02:00
parent 9d23601ba3
commit 2b6b4dbada
3 changed files with 294 additions and 12 deletions

View File

@@ -21,6 +21,7 @@ import { Column } from 'primereact/column';
import RendicontazioneService from '../service/rendicontazioneService';
import FileUploadCell from '../components/FileUploadCell';
import FilePreviewDialog from '../components/FilePreviewDialog';
import CompanyDocumentPicker from '../components/CompanyDocumentPicker';
// ---------- costanti ----------
const IVA_REGIME_LABELS = {
@@ -143,6 +144,44 @@ const PraticaRendicontazioneEdit = () => {
);
};
// --- CompanyDocument picker state + handlers ---
const [repoPicker, setRepoPicker] = useState({ visible: false, docCode: null });
const openRepositoryPicker = (docCode) => setRepoPicker({ visible: true, docCode });
const closeRepositoryPicker = () => setRepoPicker({ visible: false, docCode: null });
// quando l'utente sceglie un doc dal picker: ensure record -> link-from-repository -> update state
const handleRepositoryPick = (companyDoc) => {
const docCode = repoPicker.docCode;
if (!docCode || !companyDoc) return;
ensureDocRecord(docCode, (remDocId) => {
RendicontazioneService.linkDocumentFromRepository(
remDocId, companyDoc.id,
(resp) => {
const d = resp?.data || {};
setPractice(p => p ? {
...p,
documents: p.documents.map(x => x.doc_code === docCode ? {
...x,
filename: d.filename ?? companyDoc.fileName,
expires_at: d.expires_at ?? null,
source_company_document_id: d.source_company_document_id ?? companyDoc.id,
source_status: d.source_status ?? companyDoc.status,
size_bytes: null,
} : x)
} : p);
const sev = (d.source_status === 'EXPIRED') ? 'warn' : 'success';
toast.current?.show({
severity: sev,
summary: __('Documento collegato dal repository', 'gepafin'),
detail: companyDoc.fileName + ' · ' + (d.source_status || companyDoc.status)
});
},
(err) => toast.current?.show({ severity: 'error', summary: __('Errore link repository', 'gepafin'), detail: err?.detail })
);
});
};
// ---------- load ----------
const load = useCallback(() => {
setLoading(true);
@@ -617,19 +656,45 @@ const PraticaRendicontazioneEdit = () => {
<div><small className="text-color-secondary"><code>{dr.code}</code></small></div>
</div>
<div style={{ flex: 2, minWidth: '260px' }}>
{existing && existing.id ? (
<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 ? (
<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.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)} />
)}
</div>
) : !readOnly ? (
<Button type="button" icon="pi pi-upload" size="small" outlined
label={__('Carica', 'gepafin')}
onClick={() => ensureDocRecord(dr.code, () => {/* reload not needed, setPractice already updated */})} />
<div className="flex gap-2 align-items-center flex-wrap">
<Button type="button" icon="pi pi-upload" size="small" outlined
label={__('Carica dal PC', 'gepafin')}
onClick={() => ensureDocRecord(dr.code, () => {/* reload not needed */})} />
<Button type="button" icon="pi pi-folder-open" size="small" outlined severity="secondary"
label={__('Scegli dal repository', 'gepafin')}
onClick={() => openRepositoryPicker(dr.code)} />
</div>
) : (
<span className="text-color-secondary">{__('Nessun file', 'gepafin')}</span>
)}
@@ -908,6 +973,14 @@ const PraticaRendicontazioneEdit = () => {
</form>
)}
</Dialog>
<CompanyDocumentPicker
visible={repoPicker.visible}
companyId={practice?.company_id}
currentSourceId={(practice?.documents?.find(d => d.doc_code === repoPicker.docCode) || {}).source_company_document_id || null}
onHide={closeRepositoryPicker}
onSelect={handleRepositoryPick}
/>
<FilePreviewDialog
visible={previewDialog.visible}
onHide={closePreview}