- Import di un file XLS contenente una lista di libri, all'interno di un catalogo.

This commit is contained in:
Surya Paolo
2025-07-11 12:55:24 +02:00
parent 2ce8a72286
commit d37797fdad
20 changed files with 568 additions and 80 deletions

View File

@@ -163,7 +163,7 @@
>
</q-btn>
</div>
<div v-if="!optcatalogo.generazionePDFInCorso">
<div v-if="!optcatalogo.generazionePDFInCorso && tools.isLogged()">
<q-btn
icon-right="fas fa-cart-plus"
color="positive"

View File

@@ -0,0 +1,23 @@
button {
margin-top: 10px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
input[type="file"] {
margin-top: 20px;
}
.error {
color: red;
margin-top: 10px;
}

View File

@@ -0,0 +1,313 @@
import type { PropType } from 'vue';
import {
defineComponent,
ref,
toRef,
computed,
watch,
onMounted,
reactive,
onBeforeUnmount,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { useUserStore } from '@store/UserStore';
import { useGlobalStore } from '@store/globalStore';
import { useQuasar } from 'quasar';
import { tools } from '@tools';
import { useProducts } from '@store/Products';
import { shared_consts } from '@src/common/shared_vuejs';
import { useRouter } from 'vue-router';
import { costanti } from '@costanti';
import * as XLSX from 'xlsx';
import { Api } from 'app/src/store/Api';
export default defineComponent({
name: 'CImportListaTitoli',
emits: ['addArrayTitlesToList'],
props: {},
components: {},
setup(props, { emit }) {
const $q = useQuasar();
const { t } = useI18n();
const userStore = useUserStore();
const globalStore = useGlobalStore();
const Products = useProducts();
// Stati reattivi
const fileData = ref<any[]>([]);
const searchResults = ref<any[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const columns = ref([
{
name: 'title',
label: 'Titolo',
field: 'title',
align: 'left',
sortable: true,
},
{
name: 'isbn',
label: 'ISBN',
field: 'isbn',
align: 'left',
sortable: true,
},
{
name: 'select',
label: 'Seleziona',
field: 'select',
align: 'left',
},
]);
const onRequest = (props: any) => {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
const filterValue = filter.value;
const filteredRows = searchResults.value
.filter((row) => {
const title = row.title?.toLowerCase() || '';
const author = row.author?.toLowerCase() || '';
const isbn = row.isbn?.toLowerCase() || '';
return (
title.includes(filterValue.toLowerCase()) ||
author.includes(filterValue.toLowerCase()) ||
isbn.includes(filterValue.toLowerCase())
);
})
.sort((a, b) => {
const sortA = a[sortBy];
const sortB = b[sortBy];
if (descending) {
return sortA < sortB ? 1 : -1;
} else {
return sortA > sortB ? 1 : -1;
}
});
pagination.rowsNumber = filteredRows.length;
pagination.page = page;
pagination.rowsPerPage = rowsPerPage;
pagination.sortBy = sortBy;
pagination.descending = descending;
return {
pagination,
rows: filteredRows.slice((page - 1) * rowsPerPage, page * rowsPerPage),
};
};
const filter = ref('');
const importSelectedBooks = () => {
if (searchResults.value.length === 0) {
$q.notify({
message: t('Nessun libro selezionato'),
color: 'warning',
});
return;
}
// Aggiungi i libri selezionati alla lista dell'utente
emit(
'addArrayTitlesToList',
searchResults.value.filter((row) => row.select),
);
};
const pagination = reactive({
sortBy: 'title',
descending: false,
page: 1,
rowsPerPage: 50,
rowsNumber: searchResults.value.length,
});
const filteredResults = computed(() => {
const { sortBy, descending, page, rowsPerPage, rowsNumber } = pagination;
const firstIndex = (page - 1) * rowsPerPage;
const lastIndex = firstIndex + rowsPerPage;
return searchResults.value.slice(firstIndex, lastIndex).sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
return descending ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue);
});
});
// Gestore del caricamento del file
const handleFileUpload = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
loading.value = true;
error.value = null;
const reader = new FileReader();
reader.onload = async () => {
try {
// Verifica il tipo di file (CSV o Excel) e parse
if (file.name.endsWith('.csv')) {
fileData.value = await parseCSV(reader.result as string);
} else if (file.name.endsWith('.xls') || file.name.endsWith('.xlsx')) {
fileData.value = await parseExcel(reader.result as ArrayBuffer);
}
} catch (err) {
error.value = 'Errore nel parsing del file: ' + err;
} finally {
loading.value = false;
}
};
// Leggi il file con il metodo appropriato basato sul tipo
if (file.name.endsWith('.csv')) {
reader.readAsText(file);
} else if (file.name.endsWith('.xls') || file.name.endsWith('.xlsx')) {
reader.readAsArrayBuffer(file); // ← Questa è la correzione principale
}
}
};
// Funzione di ricerca dei libri
const searchBooksHandler = async () => {
if (!fileData.value || fileData.value.length === 0) {
return;
}
loading.value = true;
error.value = null;
try {
// Esegui la ricerca con la funzione di backend
searchResults.value = await searchBooks(fileData.value);
if (searchResults.value && searchResults.value.length > 0) {
$q.notify({
message: `Trovati ${searchResults.value.length} libri`,
color: 'positive',
});
}
if (searchResults.value && searchResults.value.length === 0) {
$q.notify({
message: 'Nessun libro importabile',
color: 'negative',
});
}
} catch (err) {
error.value = 'Errore nella ricerca dei libri';
} finally {
loading.value = false;
}
};
async function parseCSV(csvText: string): Promise<any[]> {
// Funzione per analizzare un file CSV
const lines = csvText.split('\n');
const headers = lines[0].split(',');
const data = lines.slice(1).map((line) => {
const values = line.split(',');
return headers.reduce((acc, header, index) => {
acc[header.trim()] = values[index].trim();
return acc;
}, {} as any);
});
return data;
}
async function parseExcel(excelData: ArrayBuffer): Promise<any[]> {
try {
// Debug: verifica il tipo e la dimensione di excelData
console.log('Tipo di excelData:', typeof excelData);
console.log('È ArrayBuffer?', excelData instanceof ArrayBuffer);
console.log('Dimensione excelData:', excelData.byteLength);
// Usa direttamente l'ArrayBuffer con type: 'buffer'
const workbook = XLSX.read(excelData, { type: 'buffer' });
// Controllo se ci sono fogli
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
throw new Error('Nessun foglio trovato nel file Excel');
}
console.log('Fogli disponibili:', workbook.SheetNames);
// Prendi il nome del primo foglio
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
// Controllo se il foglio esiste
if (!sheet) {
throw new Error(`Il foglio "${sheetName}" non è stato trovato`);
}
// Debug: mostra la struttura del foglio
console.log('Foglio:', sheetName);
console.log('Range del foglio:', sheet['!ref']);
console.log(
'Tutte le celle:',
Object.keys(sheet).filter((key) => !key.startsWith('!'))
);
// Usa sheet_to_json per convertire il foglio in array di righe
const rows = XLSX.utils.sheet_to_json(sheet, {
header: 1,
defval: '',
raw: false,
});
console.log('Righe estratte:', rows.length);
console.log('Prime 3 righe:', rows.slice(0, 3));
// Filtra le righe completamente vuote
const filteredRows = rows.filter(
(row: any[]) =>
Array.isArray(row) &&
row.some((cell) => cell !== null && cell !== undefined && cell !== '')
);
return filteredRows;
} catch (error) {
console.error("Errore durante l'analisi del file Excel:", error);
throw new Error(`Errore durante l'analisi del file Excel: ${error.message}`);
}
}
async function searchBooks(books: any[]): Promise<any[]> {
const response = await Api.SendReq('/api/search-books', 'POST', { books });
if (response.status !== 200) {
throw new Error('Errore nella risposta del server');
}
return response.data; // Supponiamo che il backend ritorni un array di oggetti con id e title
}
function deselectAll() {
searchResults.value.forEach((row) => (row.select = false));
}
return {
handleFileUpload,
searchBooks: searchBooksHandler,
searchResults,
loading,
error,
fileData,
pagination,
filteredResults,
importSelectedBooks,
filter,
onRequest,
columns,
deselectAll,
};
},
});

View File

@@ -0,0 +1,87 @@
<template>
<div class="row justify-center">
<h3>Carica un file Excel contenente l'ISBN oppure il nome del titolo del libro</h3>
<!-- Input per il file -->
<input
type="file"
@change="handleFileUpload"
accept=".csv, .xls, .xlsx"
/>
<br />
<q-btn
class="row"
@click="searchBooks"
:disabled="!fileData || loading"
>
Cerca Libri
</q-btn>
<br />
<!-- Risultati della ricerca -->
<div v-if="searchResults.length">
<h3>{{ searchResults.length }} Libri trovati:</h3>
<br />
<q-btn
@click="deselectAll"
:disabled="!searchResults.length"
color="negative"
dense
>
Deseleziona tutti
</q-btn>
<q-table
:columns="columns"
:rows="searchResults"
row-key="_id"
:filter="filter"
:pagination.sync="pagination"
:loading="loading"
:rows-per-page-options="[0]"
@request="onRequest"
binary-state-sort
>
<template v-slot:top-right>
<!--<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Cerca"
>
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>-->
</template>
<template v-slot:body-cell-select="props">
<q-td :props="props">
<q-checkbox v-model="props.row.select" />
</q-td>
</template>
</q-table>
<q-btn
color="primary"
@click="importSelectedBooks"
:disabled="!searchResults.some((r) => r.select)"
label="Importa selezionati"
/>
</div>
<!-- Messaggio di errore -->
<div
v-if="error"
class="error"
>
{{ error }}
</div>
</div>
</template>
<script lang="ts" src="./CImportListaTitoli.ts"></script>
<style lang="scss" scoped>
@import './CImportListaTitoli.scss';
</style>

View File

@@ -0,0 +1 @@
export {default as CImportListaTitoli} from './CImportListaTitoli.vue'

View File

@@ -23,6 +23,7 @@ import { CSchedaProdotto } from '@src/components/CSchedaProdotto';
import { CSearchProduct } from '@src/components/CSearchProduct';
import { CMyDialog } from '@src/components/CMyDialog';
import { CModifTrafiletto } from '@src/components/CModifTrafiletto';
import { CImportListaTitoli } from '@src/components/CImportListaTitoli';
import { costanti } from '@costanti';
import { IAuthor, ICatProd } from 'app/src/model';
@@ -47,12 +48,18 @@ export default defineComponent({
CLabel,
CSchedaProdotto,
CModifTrafiletto,
CImportListaTitoli,
},
props: {
lista_prodotti: {
type: Array,
required: true,
},
canadd: {
trype: Boolean,
required: false,
default: false,
},
lista_prod_confronto: {
type: Array,
required: false,
@@ -128,6 +135,7 @@ export default defineComponent({
const addstr = ref('');
const showDialogExport = ref(false);
const showDialogImport = ref(false);
const selectedExportColumns = ref([]);
const optionscatalogo = ref(<any>{ maxlength: 0 });
@@ -138,7 +146,7 @@ export default defineComponent({
}
function riaggiornaListaProdAlGenitore() {
emit('update:lista_prodotti', internalProducts.value);
aggiornaLista();
}
const editOn = computed({
@@ -551,7 +559,8 @@ export default defineComponent({
};
const savedColumns = tools.getCookie(addstr.value + 'selColCat_2');
selectedExportColumns.value = tools.getCookie(addstr.value + 'Exp_Columns');
const col = tools.getCookie(addstr.value + 'Exp_Columns', null);
selectedExportColumns.value = col ? col : [];
if (savedColumns) {
selectedColumns.value = savedColumns;
}
@@ -987,13 +996,27 @@ export default defineComponent({
persistent: false,
})
.onOk(() => {
internalProducts.value = internalProducts.value.filter(
(p: any) => p._id !== product._id
);
emit('update:lista_prodotti', internalProducts.value); // Notifica il parent del cambiamento
aggiornaLista(product);
});
};
function aggiornaLista(deleteelem: any = null) {
const precsearch = searchText.value;
searchText.value = '';
internalProducts.value = [...props.lista_prodotti];
if (deleteelem) {
internalProducts.value = internalProducts.value.filter(
(p: any) => p._id !== deleteelem._id
);
}
emit('update:lista_prodotti', internalProducts.value); // Notifica il parent del cambiamento
searchText.value = precsearch;
}
// 8. Salvataggio delle colonne selezionate in un cookie
const saveSelectedColumns = () => {
tools.setCookie(
@@ -1031,7 +1054,7 @@ export default defineComponent({
// Funzione chiamata alla fine del drag-and-drop
const onDragEnd = () => {
// console.log("Nuovo ordine:", internalProducts.value);
emit('update:lista_prodotti', internalProducts.value); // Notifica il parent del cambiamento
aggiornaLista();
};
function formatAuthors(authors: IAuthor[] | undefined | null): string {
@@ -1096,7 +1119,7 @@ export default defineComponent({
return prod;
});
emit('update:lista_prodotti', internalProducts.value); // Notifica il parent del cambiamento
aggiornaLista();
}
async function updateproductmodif(element: any) {
@@ -1184,7 +1207,7 @@ export default defineComponent({
emit('rigenera');
}
function addtolist(element) {
function addtolist(element: any) {
emit('addtolist', element);
}
@@ -1221,13 +1244,9 @@ export default defineComponent({
) {
saveSelectedColumnsExport(columns);
const csvContent = [
columns
.filter((col) => !columns.find((c) => c.name === col)?.noexp)
.map((col) => getColumnLabelByName(col))
.join(separatore),
columns.map((col) => getColumnLabelByName(col)).join(separatore),
...internalProducts.value.map((product: any) => {
return columns
.filter((col) => !columns.find((c) => c.name === col)?.noexp)
.map((col: string) => {
const field = { field: col };
return field.field === 'pos'
@@ -1257,15 +1276,15 @@ export default defineComponent({
columns: any[],
separatore: string = '\t'
) {
if (!Array.isArray(columns)) {
console.error('Errore: columns non è un array:', columns);
return;
}
saveSelectedColumnsExport(columns);
const csvContent = [
columns
.filter((col) => !columns.find((c) => c.name === col)?.noexp)
.map((col) => getColumnLabelByName(col))
.join(separatore),
columns.map((col) => getColumnLabelByName(col)).join(separatore),
...internalProducts.value.map((product: any) => {
return columns
.filter((col) => !columns.find((c) => c.name === col)?.noexp)
.map((col: string) => {
const field = { field: col };
return field.field === 'pos'
@@ -1276,6 +1295,12 @@ export default defineComponent({
}),
].join('\r\n');
// Verifica che csvContent non sia vuoto e che abbia un formato valido
if (!csvContent || csvContent.trim() === '') {
console.error('Errore: csvContent è vuoto o malformato');
return;
}
// Creazione del file XLS dopo il CSV
exportToXLS(csvContent, title);
}
@@ -1403,6 +1428,16 @@ export default defineComponent({
});
}
function addArrayTitlesToList(myarr: IProduct[]) {
console.log('addArrayTitlesToList');
for (const elem of myarr) {
addtolist(elem);
}
showDialogImport.value = false; // chiudi dialog
}
onMounted(mounted);
return {
@@ -1459,8 +1494,10 @@ export default defineComponent({
isElementVisible,
fabexp,
showDialogExport,
showDialogImport,
selectedExportColumns,
allColumnsToExported,
addArrayTitlesToList,
};
},
});

View File

@@ -44,9 +44,7 @@
<q-icon name="settings" />
</template>
</q-select>
<q-dialog
v-model="showDialogExport"
>
<q-dialog v-model="showDialogExport">
<q-card style="min-width: 400px">
<q-card-section>
<div class="text-h6">Esporta {{ title }}</div>
@@ -112,14 +110,40 @@
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showDialogImport">
<q-card style="min-width: 800px">
<q-card-section>
<div class="text-h6">Importa su {{ title }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-markup-table
separator="cell"
flat
bordered
>
<CImportListaTitoli @addArrayTitlesToList="addArrayTitlesToList"></CImportListaTitoli>
</q-markup-table>
</q-card-section>
</q-card>
</q-dialog>
<q-btn
color="positive"
icon="archive"
label="Esporta"
label="Esporta Lista"
flat
dense
@click="showDialogExport = true"
/>
<q-btn
v-if="tools.isLogged() && canadd && tools.isCollaboratore()"
color="accent"
icon="fas fa-file-import"
label="Importa da XLS"
flat
dense
@click="showDialogImport = true"
/>
</div>
</div>