diff --git a/src/modules/rendicontazione/components/CompanyDocumentPicker.js b/src/modules/rendicontazione/components/CompanyDocumentPicker.js new file mode 100644 index 0000000..3382aa0 --- /dev/null +++ b/src/modules/rendicontazione/components/CompanyDocumentPicker.js @@ -0,0 +1,195 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { __ } from '@wordpress/i18n'; +import { Dialog } from 'primereact/dialog'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { Button } from 'primereact/button'; +import { RadioButton } from 'primereact/radiobutton'; +import { Tag } from 'primereact/tag'; +import { InputText } from 'primereact/inputtext'; +import { Dropdown } from 'primereact/dropdown'; +import { Skeleton } from 'primereact/skeleton'; + +import CompanyDocumentsService from '../../../service/company-documents-service'; + +/** + * Modal per selezionare un documento dal repository della company. + * + * Mostra tutti i company_document della company (BE Gepafin /companyDocument/company/{id}), + * permette filtri per tipo, stato e testo libero, selezione radio, conferma. + * Alla conferma chiama onSelect(companyDocument). + * + * Props: + * visible boolean + * companyId number + * onHide () => void + * onSelect (doc) => void — doc: {id, fileName, type, status, expirationDate, ...} + * currentSourceId number | null — id gia selezionato, per evidenziarlo + */ +const STATUS_CFG = { + VALID: { severity: 'success', label: __('Valido', 'gepafin'), icon: 'pi pi-check-circle' }, + DUE: { severity: 'warning', label: __('In scadenza', 'gepafin'), icon: 'pi pi-exclamation-triangle' }, + EXPIRED: { severity: 'danger', label: __('Scaduto', 'gepafin'), icon: 'pi pi-times-circle' }, +}; + +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); } +}; + +const CompanyDocumentPicker = ({ visible, companyId, onHide, onSelect, currentSourceId = null }) => { + const [loading, setLoading] = useState(false); + const [docs, setDocs] = useState([]); + const [error, setError] = useState(null); + + const [selectedId, setSelectedId] = useState(null); + const [search, setSearch] = useState(''); + const [typeFilter, setTypeFilter] = useState(null); + const [statusFilter, setStatusFilter] = useState(null); + + // load al mount + useEffect(() => { + if (!visible || !companyId) return; + setLoading(true); + setError(null); + setSelectedId(currentSourceId); + CompanyDocumentsService.getCompanyDocuments( + companyId, + (resp) => { + const data = resp?.status === 'SUCCESS' ? (resp.data || []) : []; + setDocs(data); + setLoading(false); + }, + () => { + setError(__('Impossibile caricare i documenti del repository', 'gepafin')); + setLoading(false); + } + ); + // reset filtri all'apertura + setSearch(''); setTypeFilter(null); setStatusFilter(null); + }, [visible, companyId, currentSourceId]); + + // opzioni filtri derivate dai dati + const typeOptions = useMemo(() => { + const uniq = Array.from(new Set(docs.map(d => d.type).filter(Boolean))).sort(); + return uniq.map(t => ({ label: t, value: t })); + }, [docs]); + + const statusOptions = [ + { label: __('Tutti', 'gepafin'), value: null }, + { label: __('Validi', 'gepafin'), value: 'VALID' }, + { label: __('In scadenza', 'gepafin'), value: 'DUE' }, + { label: __('Scaduti', 'gepafin'), value: 'EXPIRED' }, + ]; + + const filteredDocs = useMemo(() => { + const s = (search || '').trim().toLowerCase(); + return docs.filter(d => { + if (typeFilter && d.type !== typeFilter) return false; + if (statusFilter && d.status !== statusFilter) return false; + if (s) { + const hay = [d.fileName, d.name, d.type].filter(Boolean).join(' ').toLowerCase(); + if (!hay.includes(s)) return false; + } + return true; + }); + }, [docs, search, typeFilter, statusFilter]); + + // templates colonne + const selectionTpl = (row) => ( + setSelectedId(row.id)} /> + ); + + const nameTpl = (row) => ( +
+ {row.fileName || row.name || `Doc #${row.id}`} + {row.name && row.name !== row.fileName && ( +
{row.name}
+ )} +
+ ); + + const typeTpl = (row) => {row.type || '—'}; + + const statusTpl = (row) => { + const cfg = STATUS_CFG[row.status] || { severity: 'secondary', label: row.status || '—', icon: 'pi pi-question' }; + return ; + }; + + const expiryTpl = (row) => formatDate(row.expirationDate); + + const chosenDoc = useMemo( + () => docs.find(d => d.id === selectedId) || null, + [docs, selectedId] + ); + + const footer = ( +
+
+ {chosenDoc && ( + + {__('Selezionato', 'gepafin')}: {chosenDoc.fileName || chosenDoc.name} + {' '} + + )} +
+
+
+
+ ); + + return ( + + + {/* FILTRI */} +
+ + + setSearch(e.target.value)} + style={{ width: '100%' }} /> + + setTypeFilter(e.value)} + options={typeOptions} placeholder={__('Tipo', 'gepafin')} + showClear style={{ minWidth: '160px' }} /> + setStatusFilter(e.value)} + options={statusOptions} placeholder={__('Stato', 'gepafin')} + showClear style={{ minWidth: '160px' }} /> +
+ + {/* TABELLA */} + {loading ? ( +
+ ) : error ? ( +
+ {error} +
+ ) : ( + e.value && setSelectedId(e.value.id)}> + + + + + + + )} +
+ ); +}; + +export default CompanyDocumentPicker; diff --git a/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js index 411216d..56f1121 100644 --- a/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js +++ b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js @@ -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 = () => {
{dr.code}
- {existing && existing.id ? ( - openPreview('document', existing.id, dr.label, existing.filename)} - onChange={(meta) => updateDocFile(dr.code, existing.id, meta)} - toastRef={toast} - /> + {existing && existing.id && existing.filename ? ( +
+ openPreview('document', existing.id, dr.label, existing.filename)} + onChange={(meta) => updateDocFile(dr.code, existing.id, meta)} + toastRef={toast} + /> + {existing.source_company_document_id && ( + + )} + {!readOnly && ( +
) : !readOnly ? ( -
) : ( {__('Nessun file', 'gepafin')} )} @@ -908,6 +973,14 @@ const PraticaRendicontazioneEdit = () => { )} + d.doc_code === repoPicker.docCode) || {}).source_company_document_id || null} + onHide={closeRepositoryPicker} + onSelect={handleRepositoryPick} + /> + handleError(e, onError)); }, + /** + * Collega un remission_document a un company_document del repository Gepafin. + * Alternativa all upload dal PC: copia filename/expires_at/storage_path dal sorgente + * e traccia source_company_document_id per lookup live dello status (VALID/DUE/EXPIRED). + * Usato solo su entityType='document'. + */ + linkDocumentFromRepository(remissionDocumentId, companyDocumentId, onSuccess, onError) { + fetch(`${BASE_URL}/api/remission-files/document/${remissionDocumentId}/link-from-repository`, { + method: 'POST', mode: 'cors', headers: buildHeaders(), + body: JSON.stringify({ company_document_id: companyDocumentId }) + }).then(r => handleResponse(r, onSuccess, onError)) + .catch(e => handleError(e, onError)); + }, + /** * Elimina file allegato a una entita. */