This commit is contained in:
LORENZO\pacio 2025-05-21 14:51:34 +02:00
parent 83c0f74bf0
commit 2cb448eccf
18 changed files with 2402 additions and 1 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ docs/_book
# TODO: where does this rule come from?
test/
node_modules/
dist/

13
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": [],
"label": "npm: build",
"detail": "vite build"
}
]
}

View File

@ -1,2 +1,5 @@
# unilab-prints
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1566
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "vue-spa-export-tmp",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"html2pdf.js": "^0.10.3",
"vue": "^3.5.13",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.3.5"
}
}

14
public/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<title>Certificato Prova</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="/vue-report/assets/main.js"></script>
<link rel="stylesheet" href="/vue-report/assets/index.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

BIN
public/report_header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

374
src/App.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<div>
<button class="pdf-button" @click="exportPdf">Esporta PDF</button>
<div class="report" ref="printable" contenteditable="true">
<header>
<img src="/report_header.png" alt="Intestazione Unilab" class="report-header-image" />
</header>
<h3 class="section-title">{{ intestazione.intest1 }}</h3>
<div>{{intestazione.modreport}}</div>
<h3 class="section-title">{{ intestazione.intest2 }}</h3>
<section class="info-block">
<table class="info-table">
<tr>
<td>Direttore dei Lavori:</td>
<td>{{ datiacc_info.directorName }}</td>
</tr>
<tr>
<td>Indirizzo:</td>
<td>{{ datiacc_info.directorAddress }}</td>
</tr>
<tr>
<td>Proprietà:</td>
<td>{{ datiacc_info.propertyName }}</td>
</tr>
<tr>
<td>Indirizzo:</td>
<td>{{ datiacc_info.propertyAddress }}</td>
</tr>
<tr>
<td>Cantiere:</td>
<td>{{ datiacc_info.siteName }}</td>
</tr>
<tr>
<td>Indirizzo:</td>
<td>{{ datiacc_info.siteAddress }}</td>
</tr>
<tr>
<td>Impresa esecutrice:</td>
<td>{{ datiacc_info.contractorName }}</td>
</tr>
<tr>
<td>Natura dei campioni:</td>
<td>{{ datiacc_info.sampleNature }}</td>
</tr>
</table>
</section>
<!-- SEZIONE DINAMICA PER MATERIALE E TIPO CAMPIONE-->
<div>
<component :is="resultComponent" :risult_data="risult_data" :intestazione="intestazione" :datiacc_data="datiacc_data" v-if="resultComponent" />
</div>
<!-- NOTE -->
<section v-if="note && note.length" class="note-section">
<table class="note-table">
<tr v-for="(n, index) in note" :key="index">
<td>{{ n.text }}</td>
</tr>
</table>
</section>
<footer>
<div class="signatures">
<p>Lo sperimentatore<br>Dott. Ing. Giacomo Calussi</p>
<p>Il Direttore del Laboratorio<br>Dott. Ing. Paolo Neri</p>
</div>
</footer>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import html2pdf from 'html2pdf.js'
import A_CUB from './components/risultati/A_CUB.vue'
import A_CIL from './components/risultati/A_CIL.vue'
const componentMap = {
'A_CUB': A_CUB,
'A_CIL': A_CIL,
'B_BAR':B_BAR
}
const printable = ref(null)
const intestazione = ref({})
const datiacc_info = ref({})
const datiacc_data = ref([])
const risult_data = ref([])
const note = ref([])
const resultComponent = ref(null)
watch(
() => [intestazione.value.tipMat, intestazione.value.tipCam],
([tipMat, tipCam]) => {
const key = `${(tipMat || '').toUpperCase()}_${(tipCam || '').toUpperCase()}`
console.log('Updated key:', key)
resultComponent.value = componentMap[key] || null
},
{ immediate: true }
)
onMounted(async () => {
try {
const params = new URLSearchParams(window.location.search)
const pSERCER = params.get('pSERCER') ?? 'DEFAULT'
const token = await getLoginToken('servizio_api', 'p0l01nf.'); // credenziali da gestire lato sicurezza
const data = await fetchReportDataWithToken(token, pSERCER);
intestazione.value = data.intestazione
datiacc_info.value = capitalizeAllFields(data.datiacc_info)
datiacc_data.value = data.datiacc_data
risult_data.value = calcolaMediaRcPerGruppo(data.risult_data)
note.value = data.note ?? []
console.log('intestazione', intestazione.value)
console.log('datiacc_info', datiacc_info.value)
console.log('datiacc_data', datiacc_data.value)
console.log('risult_data', risult_data.value)
} catch (err) {
console.error('Errore caricamento dati:', err)
}
})
async function getLoginToken(username, password) {
const credentials = btoa(`${username}:${password}`); // base64 encoding
const response = await fetch('/unilab/servlet/oauth/token?scope=logintoken', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
credentials: 'omit',
body: JSON.stringify({ "sp_company": "001" })
});
if (!response.ok) throw new Error('Autenticazione fallita');
const json = await response.json();
return json.access_token;
}
async function fetchReportDataWithToken(token, pSERCER) {
const response = await fetch(`/unilab/servlet/api/pi_flabreportapi?pSERCER=${encodeURIComponent(pSERCER)}`, {
headers: {
'Authorization': `Bearer ${token}`
},
credentials: 'omit'
});
if (!response.ok) throw new Error('Errore caricamento dati');
return await response.json();
}
function capitalizeAllFields(obj) {
const result = {}
for (const key in obj) {
const val = obj[key]
result[key] = typeof val === 'string' ? capitalizeEachWord(val) : val
}
return result
}
function capitalizeEachWord(str) {
if (!str) return ''
return str
.toLowerCase()
.split(' ')
.map(word =>
word.length > 0
? word[0].toUpperCase() + word.slice(1)
: ''
)
.join(' ')
}
function exportPdf() {
html2pdf()
.from(printable.value)
.set({
filename: `certificato-prova.pdf`,
html2canvas: { scale: 2 },
jsPDF: {
unit: 'mm',
format: [210, 297], // A4
orientation: 'portrait'
}
})
.save()
}
function formatDate(dateStr) {
const date = new Date(dateStr)
return date.toLocaleDateString('it-IT')
}
function formatNumber(value, decimals = 2) {
return value != null
? Number(value).toLocaleString('it-IT', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
: '—'
}
function calcolaMediaRcPerGruppo(data) {
const gruppi = {}
// Raggruppa per serPre
data.forEach(row => {
if (!gruppi[row.serPre]) gruppi[row.serPre] = []
gruppi[row.serPre].push(row.rc)
})
// Calcola la media per gruppo
const medie = {}
for (const serPre in gruppi) {
const somma = gruppi[serPre].reduce((acc, val) => acc + val, 0)
const media = somma / gruppi[serPre].length
medie[serPre] = media
}
// Assegna la media a rprelievo per tutte le righe
return data.map(row => ({
...row,
rprelievo: formatNumber(medie[row.serPre],2)
}))
}
</script>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 2px;
}
.report-header-image {
width: 100%;
height: auto;
margin-bottom: 10px;
display: block;
}
.report {
width: 100%;
max-width: 210mm;
padding: 4mm;
margin: 0 auto;
background: white;
box-sizing: border-box;
}
header {
margin-bottom: 8px;
}
.center {
text-align: center;
font-weight: bold;
margin: 4px 0;
}
.section-title {
padding: 6px;
font-weight: bold;
margin-top: 10px;
text-align: center;
}
.sub-header {
padding: 6px;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin: 2px 0;
}
th,
td {
border: 1px solid #000;
padding: 2px;
font-size: 0.85em;
text-align: center;
}
.info-table {
width: 80%;
border: 2px solid black;
margin-left: auto;
margin-right: auto;
border-collapse: collapse;
}
.info-table th,
.info-table td {
border: 1px solid black;
padding: 2px;
text-align: left;
}
.info-table th:first-child,
.info-table td:first-child {
font-weight: bold;
width: 30%;
white-space: nowrap;
}
.note-section {
margin-top: 10px;
}
.note-table {
width: 100%;
border-collapse: collapse;
}
.note-table td {
padding: 1px 1px;
font-size: 0.7em;
border: none;
text-align: left;
}
.signatures {
display: flex;
justify-content: space-between;
margin-top: 40px;
font-weight: bold;
}
.pdf-button {
position: fixed;
top: 20px;
right: 20px;
background-color: #007BFF;
color: white;
padding: 10px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
z-index: 999;
}
.pdf-button:hover {
background-color: #0056b3;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="mt-4 space-x-2">
<button @click="downloadPDF" class="bg-blue-500 text-white px-4 py-2 rounded">Export PDF</button>
<button @click="downloadExcel" class="bg-green-500 text-white px-4 py-2 rounded">Export Excel</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js';
import * as XLSX from 'xlsx';
const props = defineProps({
report: Object,
printRef: Object
});
function downloadPDF() {
html2pdf().from(props.printRef.value.$el).save('report.pdf');
}
function downloadExcel() {
const ws = XLSX.utils.json_to_sheet(props.report.samples);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Report");
XLSX.writeFile(wb, "report.xlsx");
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<div ref="printArea" class="border p-4 bg-white">
<div>
<h2 class="font-semibold">Report ID: {{ report.reportId }}</h2>
<p><strong>Date:</strong> {{ report.date }}</p>
<p><strong>Site:</strong> {{ report.site }}</p>
</div>
<table class="table-auto w-full mt-4 border">
<thead>
<tr class="bg-gray-100">
<th class="border px-2 py-1">ID</th>
<th class="border px-2 py-1">Date</th>
<th class="border px-2 py-1">a (mm)</th>
<th class="border px-2 py-1">b (mm)</th>
<th class="border px-2 py-1">h (mm)</th>
<th class="border px-2 py-1">Mass (kg/)</th>
<th class="border px-2 py-1">Rc (N/mm²)</th>
</tr>
</thead>
<tbody>
<tr v-for="sample in report.samples" :key="sample.id">
<td class="border px-2 py-1">{{ sample.id }}</td>
<td class="border px-2 py-1">{{ sample.date }}</td>
<td class="border px-2 py-1">{{ sample.a }}</td>
<td class="border px-2 py-1">{{ sample.b }}</td>
<td class="border px-2 py-1">{{ sample.h || '-' }}</td>
<td class="border px-2 py-1">{{ sample.mass }}</td>
<td class="border px-2 py-1">{{ sample.rc }}</td>
</tr>
</tbody>
</table>
<p class="mt-4 italic text-sm">{{ report.note }}</p>
</div>
</template>
<script setup>
const props = defineProps({
report: Object
});
</script>

View File

@ -0,0 +1,98 @@
<template>
<h3 class="section-title">DATI DICHIARATI ALLACCETTAZIONE</h3>
<table>
<thead>
<tr>
<th>ID<br>Provino</th>
<th>Contrassegno</th>
<th>Verbale<br>di prelievo n.</th>
<th>Data<br>prelievo</th>
<th>R<sub>CK</sub></th>
<th>Area o elemento strutturale</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in datiacc_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ row.contrassegno }}</td>
<td>{{ row.verbale }}</td>
<td>{{ formatDate(row.dataPrelievo) }}</td>
<td>{{ formatNumber(row.rck, 0) }}</td>
<td v-if="row.rowspanArea > 0" :rowspan="row.rowspanArea">{{ row.area }}</td>
</tr>
</tbody>
</table>
<!-- RISULTATI -->
<div class="section-title">{{ intestazione.prova1 }}</div>
<div class="sub-header">{{ intestazione.prova2 }}</div>
<table>
<thead>
<tr>
<th colspan="5">DATI PRELIMINARI ALLA PROVA</th>
<th colspan="5">RESISTENZA ALLA COMPRESSIONE</th>
</tr>
<tr>
<th rowspan="3">ID<br>Provino</th>
<th rowspan="3">Data<br>prova</th>
<th colspan="2">Dimensioni</th>
<th rowspan="2">Massa<br>volumica</th>
<th rowspan="2">F</th>
<th rowspan="2">R<sub>c</sub></th>
<th rowspan="2">R<sub>PRELIEVO</sub></th>
<th rowspan="3">R**</th>
<th rowspan="3">P***</th>
</tr>
<tr>
<th>D</th>
<th>h</th>
</tr>
<tr>
<th>[mm]</th>
<th>[mm]</th>
<th>[kg/]</th>
<th>[kN]</th>
<th>[N/mm²]</th>
<th>[N/mm²]</th>
</tr>
</thead>
<tbody>
<tr v-for="row in risult_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ formatDate(row.dataProva) }}</td>
<td>{{ formatNumber(row.dimA, 2) }}</td>
<td>{{ formatNumber(row.dimH, 2) }}</td>
<td>{{ formatNumber(row.massaVolumica, 0) }}</td>
<td>{{ formatNumber(row.f, 1) }}</td>
<td>{{ formatNumber(row.rc, 2) }}</td>
<td v-if="row.rowspanRprelievo > 0" :rowspan="row.rowspanRprelievo">{{ row.rprelievo }}</td>
<td>{{ row.rType }}</td>
<td>{{ row.pType }}</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
risult_data: Array,
datiacc_data: Array,
intestazione: Object
})
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('it-IT')
}
function formatNumber(val, decimals) {
return val != null
? Number(val).toLocaleString('it-IT', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
: '—'
}
</script>

View File

@ -0,0 +1,101 @@
<template>
<h3 class="section-title">DATI DICHIARATI ALLACCETTAZIONE</h3>
<table>
<thead>
<tr>
<th>ID<br>Provino</th>
<th>Contrassegno</th>
<th>Verbale<br>di prelievo n.</th>
<th>Data<br>prelievo</th>
<th>R<sub>CK</sub></th>
<th>Area o elemento strutturale</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in datiacc_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ row.contrassegno }}</td>
<td>{{ row.verbale }}</td>
<td>{{ formatDate(row.dataPrelievo) }}</td>
<td>{{ formatNumber(row.rck, 0) }}</td>
<td v-if="row.rowspanArea > 0" :rowspan="row.rowspanArea">{{ row.area }}</td>
</tr>
</tbody>
</table>
<!-- RISULTATI -->
<div class="section-title">{{ intestazione.prova1 }}</div>
<div class="sub-header">{{ intestazione.prova2 }}</div>
<table>
<thead>
<tr>
<th colspan="6">DATI PRELIMINARI ALLA PROVA</th>
<th colspan="5">RESISTENZA ALLA COMPRESSIONE</th>
</tr>
<tr>
<th rowspan="3">ID<br>Provino</th>
<th rowspan="3">Data<br>prova</th>
<th colspan="3">Dimensioni</th>
<th rowspan="2">Massa<br>volumica</th>
<th rowspan="2">F</th>
<th rowspan="2">R<sub>c</sub></th>
<th rowspan="2">R<sub>PRELIEVO</sub></th>
<th rowspan="3">R**</th>
<th rowspan="3">P***</th>
</tr>
<tr>
<th>a</th>
<th>b</th>
<th>h</th>
</tr>
<tr>
<th>[mm]</th>
<th>[mm]</th>
<th>[mm]</th>
<th>[kg/]</th>
<th>[kN]</th>
<th>[N/mm²]</th>
<th>[N/mm²]</th>
</tr>
</thead>
<tbody>
<tr v-for="row in risult_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ formatDate(row.dataProva) }}</td>
<td>{{ formatNumber(row.dimA, 2) }}</td>
<td>{{ formatNumber(row.dimB, 2) }}</td>
<td>{{ formatNumber(row.dimH, 2) }}</td>
<td>{{ formatNumber(row.massaVolumica, 0) }}</td>
<td>{{ formatNumber(row.f, 1) }}</td>
<td>{{ formatNumber(row.rc, 2) }}</td>
<td v-if="row.rowspanRprelievo > 0" :rowspan="row.rowspanRprelievo">{{ row.rprelievo }}</td>
<td>{{ row.rType }}</td>
<td>{{ row.pType }}</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
risult_data: Array,
datiacc_data: Array,
intestazione: Object
})
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('it-IT')
}
function formatNumber(val, decimals) {
return val != null
? Number(val).toLocaleString('it-IT', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
: '—'
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<h3 class="section-title">DATI PRELIMINARI DI PROVA</h3>
<table>
<thead>
<tr>
<th>ID<br>Provino</th>
<th>Contrassegno</th>
<th>Verbale<br>di prelievo n.</th>
<th>Data<br>prelievo</th>
<th>R<sub>CK</sub></th>
<th>Area o elemento strutturale</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in datiacc_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ row.contrassegno }}</td>
<td>{{ row.verbale }}</td>
<td>{{ formatDate(row.dataPrelievo) }}</td>
<td>{{ formatNumber(row.rck, 0) }}</td>
<td v-if="row.rowspanArea > 0" :rowspan="row.rowspanArea">{{ row.area }}</td>
</tr>
</tbody>
</table>
<!-- RISULTATI -->
<div class="section-title">{{ intestazione.prova1 }}</div>
<div class="sub-header">{{ intestazione.prova2 }}</div>
<table>
<thead>
<tr>
<th colspan="5">DATI PRELIMINARI ALLA PROVA</th>
<th colspan="5">RESISTENZA ALLA COMPRESSIONE</th>
</tr>
<tr>
<th rowspan="3">ID<br>Provino</th>
<th rowspan="3">Data<br>prova</th>
<th colspan="2">Dimensioni</th>
<th rowspan="2">Massa<br>volumica</th>
<th rowspan="2">F</th>
<th rowspan="2">R<sub>c</sub></th>
<th rowspan="2">R<sub>PRELIEVO</sub></th>
<th rowspan="3">R**</th>
<th rowspan="3">P***</th>
</tr>
<tr>
<th>D</th>
<th>h</th>
</tr>
<tr>
<th>[mm]</th>
<th>[mm]</th>
<th>[kg/]</th>
<th>[kN]</th>
<th>[N/mm²]</th>
<th>[N/mm²]</th>
</tr>
</thead>
<tbody>
<tr v-for="row in risult_data" :key="row.idProvino">
<td>{{ row.idProvino }}</td>
<td>{{ formatDate(row.dataProva) }}</td>
<td>{{ formatNumber(row.dimA, 2) }}</td>
<td>{{ formatNumber(row.dimH, 2) }}</td>
<td>{{ formatNumber(row.massaVolumica, 0) }}</td>
<td>{{ formatNumber(row.f, 1) }}</td>
<td>{{ formatNumber(row.rc, 2) }}</td>
<td v-if="row.rowspanRprelievo > 0" :rowspan="row.rowspanRprelievo">{{ row.rprelievo }}</td>
<td>{{ row.rType }}</td>
<td>{{ row.pType }}</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
risult_data: Array,
datiacc_data: Array,
intestazione: Object
})
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('it-IT')
}
function formatNumber(val, decimals) {
return val != null
? Number(val).toLocaleString('it-IT', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
: '—'
}
</script>

5
src/main.js Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')

8
src/style.css Normal file
View File

@ -0,0 +1,8 @@
body {
font-family: sans-serif;
background: #f3f4f6;
}
h1, h2 {
color: #1f2937;
}

17
vite.config.js Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: '/vue-report/',
build: {
outDir: 'dist',
rollupOptions: {
output: {
entryFileNames: 'assets/main.js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name][extname]'
}
}
}
})