Compare commits
17 Commits
d529e34249
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aec9d3a50 | |||
| 65b1777a60 | |||
| a49d338741 | |||
| b0bcea2f10 | |||
| 57f8b230b2 | |||
| fea0699ff1 | |||
| 82becb7569 | |||
| 5e5dadba2c | |||
| 6438d1a51c | |||
| 3fa7758fbe | |||
| 22801f6b75 | |||
| 105a23853a | |||
| 1c758d68fc | |||
| fc1c7990be | |||
| 647bd37fad | |||
| e2f9b209d6 | |||
| a58a3c438c |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Davide Grilli
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# CadRoute
|
||||
|
||||
Smistatore automatico di file CAD (Creo) per Windows e Linux.
|
||||
|
||||
Analizza una cartella o un archivio `.zip`, riconosce i file CAD (`.prt`, `.asm`, `.drw`) e li copia automaticamente nella sottocartella di destinazione corretta, basandosi sulla struttura numerica del nome file.
|
||||
|
||||
## Funzionalità
|
||||
|
||||
- Smistamento da cartella o archivio ZIP (con drag & drop)
|
||||
- Riscansione completa della destinazione a ogni processo (senza cache)
|
||||
- Routing automatico basato sul gruppo di cifre nel nome file
|
||||
- Gestione versioni: mantiene solo la versione più alta
|
||||
- Cartelle speciali: `__NON_SMISTATI`, `__DUPLICATI`, `__SALTATI`
|
||||
- Anteprima e pulizia delle cartelle speciali
|
||||
- Guida utente integrata (Help → Documentazione)
|
||||
- Destinazione persistente tra le sessioni
|
||||
|
||||
## Avvio in sviluppo
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
### Linux (Docker)
|
||||
|
||||
```bash
|
||||
cd contrib/linux
|
||||
./build.sh
|
||||
```
|
||||
|
||||
### Windows (Docker + Wine)
|
||||
|
||||
```bash
|
||||
cd contrib/windows
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Gli artefatti vengono generati in `dist/`.
|
||||
|
||||
## Struttura cartelle speciali
|
||||
|
||||
| Cartella | Contenuto |
|
||||
|----------|-----------|
|
||||
| `__NON_SMISTATI` | File CAD senza destinazione valida |
|
||||
| `__DUPLICATI` | File CAD già presenti in destinazione |
|
||||
| `__SALTATI` | File non CAD (PDF, immagini, ecc.) |
|
||||
|
||||
- **Windows:** `C:\cadroute\<cartella>\`
|
||||
- **Linux:** `~/.cadroute/<cartella>/`
|
||||
|
||||
## Autore
|
||||
|
||||
Davide Grilli — Cevolani Italia s.r.l.
|
||||
|
||||
## Licenza
|
||||
|
||||
MIT
|
||||
0
build/.gitkeep
Normal file
0
build/.gitkeep
Normal file
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
12
contrib/linux/Dockerfile
Normal file
12
contrib/linux/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-bookworm-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libudev-dev \
|
||||
libusb-1.0-0-dev \
|
||||
fuse \
|
||||
rpm \
|
||||
dpkg-dev \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /project
|
||||
25
contrib/linux/build.sh
Executable file
25
contrib/linux/build.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
IMAGE_NAME="cadroute-builder-linux"
|
||||
|
||||
echo "[cadroute] Building Docker image..."
|
||||
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
|
||||
|
||||
echo "[cadroute] Running Linux build..."
|
||||
docker run --rm \
|
||||
--device /dev/fuse \
|
||||
--cap-add SYS_ADMIN \
|
||||
-v "$PROJECT_ROOT":/project \
|
||||
-w /project \
|
||||
-v cadroute-electron-cache:/root/.cache/electron \
|
||||
-v cadroute-electronbuilder-cache:/root/.cache/electron-builder \
|
||||
-v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro \
|
||||
-e SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
|
||||
-e NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt \
|
||||
"$IMAGE_NAME" \
|
||||
bash -c "npm ci && npm run build -- --linux --x64"
|
||||
|
||||
echo "[cadroute] Artefatti in: $PROJECT_ROOT/dist"
|
||||
2
contrib/windows/Dockerfile
Normal file
2
contrib/windows/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM electronuserland/builder:wine-mono
|
||||
WORKDIR /project
|
||||
20
contrib/windows/build.sh
Executable file
20
contrib/windows/build.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
IMAGE_NAME="cadroute-builder-windows"
|
||||
|
||||
echo "[cadroute] Building Docker image..."
|
||||
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
|
||||
|
||||
echo "[cadroute] Running Windows build..."
|
||||
docker run --rm \
|
||||
-v "$PROJECT_ROOT":/project \
|
||||
-w /project \
|
||||
-v cadroute-electron-cache:/root/.cache/electron \
|
||||
-v cadroute-electronbuilder-cache:/root/.cache/electron-builder \
|
||||
"$IMAGE_NAME" \
|
||||
bash -c "npm ci && npm run build -- --win --x64"
|
||||
|
||||
echo "[cadroute] Artefatti in: $PROJECT_ROOT/dist"
|
||||
78
main.js
78
main.js
@@ -1,4 +1,4 @@
|
||||
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron');
|
||||
const { app, BrowserWindow, dialog, ipcMain, shell, Menu } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -8,12 +8,14 @@ const {
|
||||
resolveUnroutedDir,
|
||||
listUnroutedFiles,
|
||||
listDuplicateFiles,
|
||||
listSkippedFiles,
|
||||
clearUnroutedFiles,
|
||||
clearDuplicateFiles,
|
||||
clearSkippedFiles,
|
||||
} = require('./services/unrouted');
|
||||
|
||||
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
|
||||
const DEFAULT_DESTINATION = './output/cad';
|
||||
const CAD_EXTENSIONS = ['prt', 'asm', 'drw'];
|
||||
const DEFAULT_DESTINATION = 'X:\\';
|
||||
const SETTINGS_FILENAME = 'cad-router-settings.json';
|
||||
const LINUX_RUNTIME_DIR = '.cadroute';
|
||||
|
||||
@@ -66,8 +68,9 @@ let config = buildConfig(DEFAULT_DESTINATION);
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 640,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
icon: path.join(__dirname, 'build', 'icon.png'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
@@ -78,6 +81,50 @@ function createWindow() {
|
||||
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||
}
|
||||
|
||||
function createDocsWindow() {
|
||||
const docs = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 700,
|
||||
title: 'CadRoute — Documentazione',
|
||||
icon: path.join(__dirname, 'build', 'icon.png'),
|
||||
webPreferences: { contextIsolation: true, nodeIntegration: false },
|
||||
});
|
||||
docs.loadFile(path.join(__dirname, 'renderer', 'docs', 'index.html'));
|
||||
docs.setMenuBarVisibility(false);
|
||||
}
|
||||
|
||||
function showAboutDialog() {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Informazioni su CadRoute',
|
||||
message: 'CadRoute',
|
||||
detail: [
|
||||
`Versione: ${app.getVersion()}`,
|
||||
`Autore: Davide Grilli`,
|
||||
`Azienda: Cevolani Italia s.r.l.`,
|
||||
`Licenza: MIT`,
|
||||
``,
|
||||
`Smistatore automatico di file CAD (Creo).`,
|
||||
``,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Node: ${process.versions.node}`,
|
||||
].join('\n'),
|
||||
buttons: ['OK'],
|
||||
noLink: true,
|
||||
});
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Informazioni',
|
||||
submenu: [{ label: 'Informazioni su CadRoute...', click: () => showAboutDialog() }],
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
submenu: [{ label: 'Documentazione', click: () => createDocsWindow() }],
|
||||
},
|
||||
]));
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await ensureRuntimeDirectory();
|
||||
const persistedDestination = await loadPersistedDestination();
|
||||
@@ -95,7 +142,7 @@ app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
ipcMain.handle('select-folder', async () => {
|
||||
ipcMain.handle('select-folder', async (event) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
@@ -104,11 +151,12 @@ ipcMain.handle('select-folder', async () => {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const routingResult = await processFolder(result.filePaths[0], config);
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
const routingResult = await processFolder(result.filePaths[0], config, onProgress);
|
||||
return { canceled: false, ...routingResult };
|
||||
});
|
||||
|
||||
ipcMain.handle('select-zip', async () => {
|
||||
ipcMain.handle('select-zip', async (event) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Zip', extensions: ['zip'] }],
|
||||
@@ -118,11 +166,12 @@ ipcMain.handle('select-zip', async () => {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const routingResult = await processZip(result.filePaths[0], config);
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
const routingResult = await processZip(result.filePaths[0], config, onProgress);
|
||||
return { canceled: false, ...routingResult };
|
||||
});
|
||||
|
||||
ipcMain.handle('process-dropped-path', async (_event, payload) => {
|
||||
ipcMain.handle('process-dropped-path', async (event, payload) => {
|
||||
const droppedPath = String(payload?.path || '').trim();
|
||||
if (!droppedPath) {
|
||||
throw new Error('Percorso non valido');
|
||||
@@ -133,13 +182,15 @@ ipcMain.handle('process-dropped-path', async (_event, payload) => {
|
||||
throw new Error('Percorso non trovato');
|
||||
}
|
||||
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const routingResult = await processFolder(droppedPath, config);
|
||||
const routingResult = await processFolder(droppedPath, config, onProgress);
|
||||
return { canceled: false, sourceType: 'folder', ...routingResult };
|
||||
}
|
||||
|
||||
if (stats.isFile() && path.extname(droppedPath).toLowerCase() === '.zip') {
|
||||
const routingResult = await processZip(droppedPath, config);
|
||||
const routingResult = await processZip(droppedPath, config, onProgress);
|
||||
return { canceled: false, sourceType: 'zip', ...routingResult };
|
||||
}
|
||||
|
||||
@@ -176,6 +227,7 @@ ipcMain.handle('update-destination', async (_event, payload) => {
|
||||
return { destination: config.destination };
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('open-unrouted-folder', async () => {
|
||||
const unroutedDir = await resolveUnroutedDir();
|
||||
const openResult = await shell.openPath(unroutedDir);
|
||||
@@ -189,5 +241,7 @@ ipcMain.handle('open-unrouted-folder', async () => {
|
||||
|
||||
ipcMain.handle('list-unrouted-files', async () => listUnroutedFiles());
|
||||
ipcMain.handle('list-duplicates-files', async () => listDuplicateFiles());
|
||||
ipcMain.handle('list-skipped-files', async () => listSkippedFiles());
|
||||
ipcMain.handle('clear-unrouted-files', async () => clearUnroutedFiles());
|
||||
ipcMain.handle('clear-duplicates-files', async () => clearDuplicateFiles());
|
||||
ipcMain.handle('clear-skipped-files', async () => clearSkippedFiles());
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "progetto",
|
||||
"name": "cadroute",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "progetto",
|
||||
"name": "cadroute",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
25
package.json
25
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "progetto",
|
||||
"name": "cadroute",
|
||||
"version": "1.0.0",
|
||||
"description": "CAD File Router MVP",
|
||||
"description": "CadRoute MVP",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "env -u ELECTRON_RUN_AS_NODE electron .",
|
||||
@@ -12,17 +12,32 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"build": {
|
||||
"appId": "com.cad.router",
|
||||
"appId": "com.cadroute",
|
||||
"productName": "CadRoute",
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"renderer/**/*",
|
||||
"services/**/*",
|
||||
"build/icon.png",
|
||||
"renderer/docs/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
"target": "nsis",
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"linux": {
|
||||
"target": ["AppImage", "deb"],
|
||||
"icon": "build/icon.png",
|
||||
"category": "Utility"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"electron": "^40.7.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"unzipper": "^0.12.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^40.7.0",
|
||||
"electron-builder": "^26.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ contextBridge.exposeInMainWorld('api', {
|
||||
openUnroutedFolder: () => ipcRenderer.invoke('open-unrouted-folder'),
|
||||
listUnroutedFiles: () => ipcRenderer.invoke('list-unrouted-files'),
|
||||
listDuplicatesFiles: () => ipcRenderer.invoke('list-duplicates-files'),
|
||||
listSkippedFiles: () => ipcRenderer.invoke('list-skipped-files'),
|
||||
clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'),
|
||||
clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-files'),
|
||||
clearSkippedFiles: () => ipcRenderer.invoke('clear-skipped-files'),
|
||||
onProgress: (cb) => ipcRenderer.on('progress', (_e, data) => cb(data)),
|
||||
offProgress: () => ipcRenderer.removeAllListeners('progress'),
|
||||
});
|
||||
|
||||
303
renderer/docs/index.html
Normal file
303
renderer/docs/index.html
Normal file
@@ -0,0 +1,303 @@
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CadRoute — Documentazione</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-brand">
|
||||
<div class="logo">CadRoute</div>
|
||||
<div class="version">Guida utente</div>
|
||||
</div>
|
||||
|
||||
<span class="nav-section-label">Introduzione</span>
|
||||
<a href="#intro">Cos'è CadRoute</a>
|
||||
<a href="#requisiti">Requisiti</a>
|
||||
|
||||
<span class="nav-section-label">Utilizzo</span>
|
||||
<a href="#destinazione">Configurare la destinazione</a>
|
||||
<a href="#cartella">Smistare i file</a>
|
||||
<a href="#risultati">Leggere i risultati</a>
|
||||
|
||||
<span class="nav-section-label">Approfondimenti</span>
|
||||
<a href="#nomi">Convenzione nomi file</a>
|
||||
<a href="#nonsmistati">File non smistati</a>
|
||||
<a href="#duplicati">File duplicati</a>
|
||||
<a href="#saltati">File saltati</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ── INTRODUZIONE ──────────────────────── -->
|
||||
<section id="intro">
|
||||
<h1>CadRoute — Guida utente</h1>
|
||||
<div class="intro-card">
|
||||
<p><strong>CadRoute</strong> automatizza lo smistamento di file CAD provenienti da Creo. Analizza una cartella o un archivio ZIP, riconosce i file CAD e li copia automaticamente nella sottocartella di destinazione corretta, basandosi sulla struttura numerica del nome file.</p>
|
||||
</div>
|
||||
|
||||
<h3>Formati supportati</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Estensione</th><th>Tipo</th><th>Descrizione</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="badge prt">.prt</span></td>
|
||||
<td>Part</td>
|
||||
<td>File di componente singolo Creo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge asm">.asm</span></td>
|
||||
<td>Assembly</td>
|
||||
<td>File di assemblaggio Creo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge dwr">.drw</span></td>
|
||||
<td>Drawing</td>
|
||||
<td>File di disegno Creo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge skip">altri</span></td>
|
||||
<td>—</td>
|
||||
<td>Copiati in <code>__SALTATI</code> (PDF, immagini, documenti, ecc.)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ── REQUISITI ────────────────────────── -->
|
||||
<section id="requisiti">
|
||||
<h2>Requisiti</h2>
|
||||
<p>Prima di iniziare, assicurati che la <strong>cartella di destinazione</strong> sia quella corretta.</p>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- ── CONFIGURARE DESTINAZIONE ─────────── -->
|
||||
<section id="destinazione">
|
||||
<h2>Configurare la destinazione</h2>
|
||||
<p>La <strong>destinazione</strong> è la cartella radice dove CadRoute copierà i file CAD smistati. Va impostata una volta sola e viene ricordata tra le sessioni.</p>
|
||||
|
||||
<ol class="steps">
|
||||
<li><span>Individua la sezione <strong>Destinazione file CAD</strong> nella finestra principale.</span></li>
|
||||
<li><span>Clicca <strong>Sfoglia</strong> per aprire il selettore cartelle, oppure digita il percorso direttamente nel campo di testo.</span></li>
|
||||
<li><span>Clicca <strong>Salva</strong>. La destinazione viene memorizzata e sarà attiva anche alla prossima apertura dell'app.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout tip">
|
||||
<strong>Tip:</strong> il percorso di default è <code>X:\</code>. Assicurati che sia quello corretto prima di avviare lo smistamento.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── SMISTARE FILE ──────────────────────── -->
|
||||
<section id="cartella">
|
||||
<h2>Smistare i file</h2>
|
||||
<p>È possibile smistare una <strong>cartella</strong> (anche con sottocartelle annidate) o un archivio <strong>.zip</strong>, tramite i pulsanti o trascinando direttamente nell'area apposita.</p>
|
||||
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Process Folder</strong> per selezionare una cartella, oppure <strong>Process ZIP</strong> per selezionare un archivio. In alternativa, trascina la cartella o il file <code>.zip</code> nell'area tratteggiata.</span></li>
|
||||
<li><span>L'app scansiona tutti i file e identifica automaticamente quelli CAD.</span></li>
|
||||
<li><span>I file CAD vengono copiati nella sottocartella di destinazione corrispondente. I file non CAD vengono copiati in <code>__SALTATI</code>.</span></li>
|
||||
<li><span>Al termine vengono mostrate le statistiche nell'area di output.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout info">
|
||||
<strong>Nota:</strong> i file originali non vengono mai modificati né cancellati.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- ── RISULTATI ─────────────────────────── -->
|
||||
<section id="risultati">
|
||||
<h2>Leggere i risultati</h2>
|
||||
<p>Al termine di ogni operazione l'area di output mostra un riepilogo. Ecco il significato di ogni voce.</p>
|
||||
|
||||
<div class="stat-grid">
|
||||
<div class="stat-item info">
|
||||
<div class="stat-label">Scansionati</div>
|
||||
<div class="stat-desc">Totale file analizzati (CAD e non).</div>
|
||||
</div>
|
||||
<div class="stat-item ok">
|
||||
<div class="stat-label">Copiati</div>
|
||||
<div class="stat-desc">File effettivamente scritti su disco (destinazione, non smistati o duplicati).</div>
|
||||
</div>
|
||||
<div class="stat-item muted">
|
||||
<div class="stat-label">Saltati</div>
|
||||
<div class="stat-desc">File non CAD copiati in <code>__SALTATI</code> (PDF, immagini, ecc.).</div>
|
||||
</div>
|
||||
<div class="stat-item warn">
|
||||
<div class="stat-label">Non smistati</div>
|
||||
<div class="stat-desc">File CAD per cui non è stata trovata una destinazione valida.</div>
|
||||
</div>
|
||||
<div class="stat-item warn">
|
||||
<div class="stat-label">Duplicati</div>
|
||||
<div class="stat-desc">File CAD già presenti nella destinazione al momento dello smistamento.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Sotto le statistiche sono elencati i dettagli di ogni file (massimo 20 voci) con il percorso di destinazione e la motivazione, nel caso di file non smistati o duplicati.</p>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- ── NOMI FILE ─────────────────────────── -->
|
||||
<section id="nomi">
|
||||
<h2>Convenzione nomi file CAD</h2>
|
||||
<p>CadRoute determina automaticamente la sottocartella di destinazione leggendo il nome del file. È fondamentale che i file seguano la convenzione corretta.</p>
|
||||
|
||||
<h3>Struttura del nome</h3>
|
||||
<pre class="code-block"><code>CODICE.tipo[.versione]
|
||||
|
||||
Esempi:
|
||||
ABC12345.prt → Part, nessuna versione
|
||||
ABC12345.prt.6 → Part, versione 6
|
||||
XYZ67890.asm.10 → Assembly, versione 10
|
||||
DEF11111.drw.15 → Drawing, versione 15</code></pre>
|
||||
|
||||
<h3>Gestione versioni</h3>
|
||||
<p>Se sono presenti più versioni dello stesso file nella sorgente (es. <code>ABC12345.prt.8</code> e <code>ABC12345.prt.9</code>), CadRoute mantiene <strong>solo la versione più alta</strong> e scarta le inferiori.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Scenario</th><th>Comportamento</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>File non presente in destinazione</td>
|
||||
<td>Copia diretta nella sottocartella corretta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File già presente in destinazione</td>
|
||||
<td>Copiato in <code>__DUPLICATI</code> per revisione</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Versione inferiore presente nella sorgente</td>
|
||||
<td>Saltata — la versione più alta prende precedenza</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ── NON SMISTATI ──────────────────────── -->
|
||||
<section id="nonsmistati">
|
||||
<h2>File non smistati</h2>
|
||||
<p>Un file CAD finisce in <strong>non smistati</strong> quando CadRoute non riesce a determinare una destinazione valida. Le cause più comuni sono:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Causa</th><th>Messaggio</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Codice con meno di 5 cifre</td>
|
||||
<td><em>"Nome file non conforme: servono almeno 5 cifre…"</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sottocartella non trovata in destinazione</td>
|
||||
<td><em>"Sottocartella XXX non trovata nella destinazione"</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sottocartella trovata in più percorsi (ambiguo)</td>
|
||||
<td><em>"Sottocartella XXX trovata in N percorsi diversi"</em></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Dove si trovano</h3>
|
||||
<p>I file non smistati vengono copiati nella cartella <code>__NON_SMISTATI</code>:</p>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__NON_SMISTATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__NON_SMISTATI/</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Anteprima non smistati</strong> per aprire la lista dei file presenti.</span></li>
|
||||
<li><span>Verifica i file, controlla il motivo dell'esclusione nell'area di output.</span></li>
|
||||
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma. L'operazione è irreversibile.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout tip">
|
||||
<strong>Suggerimento:</strong> prima di pulire, sposta manualmente i file che vuoi conservare. La pulizia elimina tutto il contenuto della cartella.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── DUPLICATI ─────────────────────────── -->
|
||||
<section id="duplicati">
|
||||
<h2>File duplicati</h2>
|
||||
<p>Un file CAD viene classificato come <strong>duplicato</strong> quando la sua chiave (<code>CODICE.tipo</code>, senza versione) è già presente nella cartella di destinazione al momento dello smistamento.</p>
|
||||
|
||||
<p>I duplicati non sovrascrivono i file già presenti in destinazione: vengono messi da parte nella cartella <code>__DUPLICATI</code> per permettere una revisione manuale.</p>
|
||||
|
||||
<h3>Dove si trovano</h3>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__DUPLICATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__DUPLICATI/</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Gestione versioni nei duplicati</h3>
|
||||
<p>Anche all'interno della cartella <code>__DUPLICATI</code> viene applicata la logica delle versioni: se arriva una versione più alta di un file già presente nei duplicati, la versione vecchia viene eliminata e sostituita con quella nuova.</p>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Anteprima duplicati</strong> per aprire la lista dei file presenti.</span></li>
|
||||
<li><span>Verifica se i duplicati sono effettivamente superati o se richiedono un intervento manuale.</span></li>
|
||||
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout warning">
|
||||
<strong>Attenzione:</strong> la pulizia è definitiva. Assicurati di non aver bisogno dei file prima di procedere.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── SALTATI ────────────────────────────── -->
|
||||
<section id="saltati">
|
||||
<h2>File saltati</h2>
|
||||
<p>Un file viene classificato come <strong>saltato</strong> quando non è riconosciuto come file CAD, cioè non ha estensione <code>.prt</code>, <code>.asm</code> o <code>.drw</code>. Rientrano in questa categoria PDF, immagini, documenti Word e qualsiasi altro formato non CAD.</p>
|
||||
|
||||
<p>I file saltati non vengono ignorati completamente: vengono copiati nella cartella <code>__SALTATI</code> per permettere una verifica manuale.</p>
|
||||
|
||||
<h3>Dove si trovano</h3>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__SALTATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__SALTATI/</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Anteprima saltati</strong> per aprire la lista dei file presenti.</span></li>
|
||||
<li><span>Verifica che nessun file CAD sia finito qui per errore di denominazione.</span></li>
|
||||
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout info">
|
||||
<strong>Nota:</strong> la cartella <code>__SALTATI</code> è utile per individuare file CAD rinominati erroneamente o con estensione mancante.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Evidenzia il link attivo nella sidebar durante lo scroll
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const links = document.querySelectorAll('nav a[href^="#"]');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
links.forEach(l => l.classList.remove('active'));
|
||||
const active = document.querySelector(`nav a[href="#${entry.target.id}"]`);
|
||||
if (active) active.classList.add('active');
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-20% 0px -70% 0px' });
|
||||
|
||||
sections.forEach(s => observer.observe(s));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
335
renderer/docs/style.css
Normal file
335
renderer/docs/style.css
Normal file
@@ -0,0 +1,335 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f6f8fb;
|
||||
--card: #ffffff;
|
||||
--text: #0f172a;
|
||||
--muted: #475569;
|
||||
--accent: #0b5fff;
|
||||
--accent-light: #eef3ff;
|
||||
--border: #dbe2ea;
|
||||
--sidebar-w: 220px;
|
||||
--code-bg: #f1f5f9;
|
||||
--success: #15803d;
|
||||
--warning: #b45309;
|
||||
--danger: #b91c1c;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── SIDEBAR ─────────────────────────────────── */
|
||||
nav {
|
||||
width: var(--sidebar-w);
|
||||
min-width: var(--sidebar-w);
|
||||
background: var(--card);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 24px 0 24px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
padding: 0 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nav-brand .logo {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.nav-brand .version {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: block;
|
||||
padding: 7px 18px;
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
border-left: 3px solid transparent;
|
||||
transition: color 100ms, border-color 100ms, background 100ms;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--text);
|
||||
background: var(--accent-light);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
color: var(--accent);
|
||||
border-left-color: var(--accent);
|
||||
background: var(--accent-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-section-label {
|
||||
padding: 14px 18px 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ── MAIN CONTENT ────────────────────────────── */
|
||||
main {
|
||||
margin-left: var(--sidebar-w);
|
||||
flex: 1;
|
||||
padding: 40px 48px 80px;
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
/* ── TYPOGRAPHY ──────────────────────────────── */
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
margin: 48px 0 14px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
scroll-margin-top: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 22px 0 8px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* ── INTRO CARD ──────────────────────────────── */
|
||||
.intro-card {
|
||||
background: var(--accent-light);
|
||||
border: 1px solid #c7d9ff;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.intro-card p {
|
||||
color: #1e3a8a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── STEP LIST ───────────────────────────────── */
|
||||
.steps {
|
||||
list-style: none;
|
||||
counter-reset: step;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.steps li::before {
|
||||
content: counter(step);
|
||||
min-width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.steps li span {
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── CALLOUT ─────────────────────────────────── */
|
||||
.callout {
|
||||
border-radius: 10px;
|
||||
padding: 13px 16px;
|
||||
margin: 16px 0;
|
||||
font-size: 13px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.callout.info {
|
||||
background: #f0f9ff;
|
||||
border-color: #38bdf8;
|
||||
color: #075985;
|
||||
}
|
||||
|
||||
.callout.warning {
|
||||
background: #fffbeb;
|
||||
border-color: #f59e0b;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
.callout.tip {
|
||||
background: #f0fdf4;
|
||||
border-color: #22c55e;
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
.callout strong { font-weight: 700; }
|
||||
|
||||
/* ── CODE & INLINE CODE ──────────────────────── */
|
||||
code {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
font-family: 'Cascadia Code', 'Consolas', monospace;
|
||||
font-size: 12.5px;
|
||||
color: #be185d;
|
||||
}
|
||||
|
||||
pre.code-block {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'Cascadia Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
pre.code-block code {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* ── BADGE ───────────────────────────────────── */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
vertical-align: middle;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.badge.prt { background: #dbeafe; color: #1e40af; }
|
||||
.badge.asm { background: #dcfce7; color: #166534; }
|
||||
.badge.dwr { background: #fef3c7; color: #92400e; }
|
||||
.badge.skip { background: #f1f5f9; color: #475569; }
|
||||
|
||||
/* ── TABLE ───────────────────────────────────── */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 14px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 9px 12px;
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td { background: #fafbfc; }
|
||||
|
||||
/* ── STAT GRID ───────────────────────────────── */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.stat-item .stat-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-item .stat-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stat-item.ok { border-top: 3px solid #22c55e; }
|
||||
.stat-item.info { border-top: 3px solid var(--accent); }
|
||||
.stat-item.warn { border-top: 3px solid #f59e0b; }
|
||||
.stat-item.muted { border-top: 3px solid #94a3b8; }
|
||||
|
||||
/* ── SEPARATOR ───────────────────────────────── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
/* ── SECTION ─────────────────────────────────── */
|
||||
section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CAD File Router</title>
|
||||
<title>CadRoute</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
@@ -24,32 +24,35 @@
|
||||
background: linear-gradient(130deg, #eaf0ff 0%, var(--bg) 45%, #f9fbff 100%);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 820px;
|
||||
max-width: 980px;
|
||||
margin: 40px auto;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 26px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
gap: 14px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -57,8 +60,8 @@
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 11px 16px;
|
||||
font-size: 15px;
|
||||
padding: 13px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -116,43 +119,43 @@
|
||||
}
|
||||
|
||||
.rules h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 17px;
|
||||
margin: 0 0 12px;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.rule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr auto auto;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rule-row.single-row {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.rule-row.preview-actions {
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-columns: auto auto auto;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.rule-label {
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.rule-row input {
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rule-row button {
|
||||
padding: 9px 12px;
|
||||
padding: 11px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -162,7 +165,7 @@
|
||||
|
||||
.rule-status {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -251,11 +254,58 @@
|
||||
overflow: auto;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 18px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
progress {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--accent);
|
||||
border-radius: 4px;
|
||||
transition: width 80ms ease;
|
||||
}
|
||||
|
||||
progress:indeterminate {
|
||||
background: linear-gradient(90deg, var(--border) 25%, var(--accent) 50%, var(--border) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: progress-indeterminate 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-indeterminate {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<h1>Smistatore automatico</h1>
|
||||
<h1>CadRoute - Smistatore automatico</h1>
|
||||
<p>Seleziona una cartella o uno ZIP con i disegni da smistare</p>
|
||||
|
||||
<div class="actions">
|
||||
@@ -267,6 +317,11 @@
|
||||
Trascina qui una cartella o un file .zip
|
||||
</section>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-label" id="progressLabel">In corso...</div>
|
||||
<progress id="progressBar"></progress>
|
||||
</div>
|
||||
|
||||
<section class="rules">
|
||||
<h2>Destinazione file CAD</h2>
|
||||
<div class="rule-row single-row">
|
||||
@@ -277,6 +332,7 @@
|
||||
<div class="rule-row single-row preview-actions">
|
||||
<button id="openUnroutedBtn" class="browse">Anteprima non smistati</button>
|
||||
<button id="openDuplicatesBtn" class="browse">Anteprima duplicati</button>
|
||||
<button id="openSkippedBtn" class="browse">Anteprima saltati</button>
|
||||
</div>
|
||||
<div class="rule-status" id="destinationStatus"></div>
|
||||
</section>
|
||||
|
||||
@@ -7,6 +7,7 @@ const browseDestinationBtn = document.getElementById('browseDestinationBtn');
|
||||
const saveDestinationBtn = document.getElementById('saveDestinationBtn');
|
||||
const openUnroutedBtn = document.getElementById('openUnroutedBtn');
|
||||
const openDuplicatesBtn = document.getElementById('openDuplicatesBtn');
|
||||
const openSkippedBtn = document.getElementById('openSkippedBtn');
|
||||
const destinationStatus = document.getElementById('destinationStatus');
|
||||
const previewOverlay = document.getElementById('previewOverlay');
|
||||
const closePreviewBtn = document.getElementById('closePreviewBtn');
|
||||
@@ -14,10 +15,52 @@ const clearPreviewBtn = document.getElementById('clearPreviewBtn');
|
||||
const previewTitle = document.getElementById('previewTitle');
|
||||
const previewMeta = document.getElementById('previewMeta');
|
||||
const previewList = document.getElementById('previewList');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressLabel = document.getElementById('progressLabel');
|
||||
const defaultDropZoneText = 'Trascina qui una cartella o un file .zip';
|
||||
let isProcessing = false;
|
||||
let currentPreviewKind = null;
|
||||
|
||||
function showProgress() {
|
||||
progressContainer.style.display = 'block';
|
||||
progressBar.removeAttribute('value');
|
||||
progressLabel.textContent = 'In corso...';
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
let _indexFilesFound = 0;
|
||||
|
||||
function updateProgress({ phase, current, total, file, scanned, folder }) {
|
||||
if (phase === 'scan') {
|
||||
_indexFilesFound = 0;
|
||||
progressLabel.textContent = 'Scansione file sorgente...';
|
||||
progressBar.removeAttribute('value');
|
||||
} else if (phase === 'index-dest') {
|
||||
progressBar.max = total;
|
||||
progressBar.value = current;
|
||||
const filesInfo = _indexFilesFound > 0 ? ` — ${_indexFilesFound} file CAD trovati` : '';
|
||||
progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? ` — ${folder}` : ''}${filesInfo}`;
|
||||
} else if (phase === 'index-dup') {
|
||||
if (current !== undefined && total !== undefined) {
|
||||
progressBar.max = total;
|
||||
progressBar.value = current;
|
||||
progressLabel.textContent = `Analisi destinazione: ${current} / ${total} cartelle`;
|
||||
} else {
|
||||
_indexFilesFound = scanned;
|
||||
const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max} — ` : '';
|
||||
progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`;
|
||||
}
|
||||
} else if (phase === 'copy') {
|
||||
progressBar.max = total;
|
||||
progressBar.value = current;
|
||||
progressLabel.textContent = `Smistamento: ${current} / ${total}${file ? ` — ${file}` : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
folderBtn.disabled = isLoading;
|
||||
zipBtn.disabled = isLoading;
|
||||
@@ -29,6 +72,7 @@ function setDestinationLoading(isLoading) {
|
||||
saveDestinationBtn.disabled = isLoading;
|
||||
openUnroutedBtn.disabled = isLoading;
|
||||
openDuplicatesBtn.disabled = isLoading;
|
||||
openSkippedBtn.disabled = isLoading;
|
||||
clearPreviewBtn.disabled = isLoading;
|
||||
}
|
||||
|
||||
@@ -60,18 +104,14 @@ function hidePreview() {
|
||||
}
|
||||
|
||||
async function loadPreviewData(kind) {
|
||||
if (kind === 'duplicates') {
|
||||
return window.api.listDuplicatesFiles();
|
||||
}
|
||||
|
||||
if (kind === 'duplicates') return window.api.listDuplicatesFiles();
|
||||
if (kind === 'skipped') return window.api.listSkippedFiles();
|
||||
return window.api.listUnroutedFiles();
|
||||
}
|
||||
|
||||
async function clearPreviewData(kind) {
|
||||
if (kind === 'duplicates') {
|
||||
return window.api.clearDuplicatesFiles();
|
||||
}
|
||||
|
||||
if (kind === 'duplicates') return window.api.clearDuplicatesFiles();
|
||||
if (kind === 'skipped') return window.api.clearSkippedFiles();
|
||||
return window.api.clearUnroutedFiles();
|
||||
}
|
||||
|
||||
@@ -103,6 +143,8 @@ async function handleAction(actionName, actionFn) {
|
||||
|
||||
isProcessing = true;
|
||||
setLoading(true);
|
||||
showProgress();
|
||||
window.api.onProgress(updateProgress);
|
||||
output.textContent = `${actionName} in corso...`;
|
||||
|
||||
try {
|
||||
@@ -117,6 +159,8 @@ async function handleAction(actionName, actionFn) {
|
||||
} catch (error) {
|
||||
output.textContent = `${actionName} fallito:\n${error.message}`;
|
||||
} finally {
|
||||
window.api.offProgress();
|
||||
hideProgress();
|
||||
setLoading(false);
|
||||
isProcessing = false;
|
||||
}
|
||||
@@ -248,7 +292,22 @@ openDuplicatesBtn.addEventListener('click', async () => {
|
||||
destinationStatus.textContent = 'Caricamento anteprima duplicati...';
|
||||
|
||||
const result = await window.api.listDuplicatesFiles();
|
||||
showPreview('duplicates', 'Anteprima duplicati', result, 'Nessun file presente in duplicati.');
|
||||
showPreview('duplicates', 'Anteprima __DUPLICATI', result, 'Nessun file presente in __DUPLICATI.');
|
||||
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
|
||||
} catch (error) {
|
||||
destinationStatus.textContent = `Errore: ${error.message}`;
|
||||
} finally {
|
||||
setDestinationLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
openSkippedBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
setDestinationLoading(true);
|
||||
destinationStatus.textContent = 'Caricamento anteprima saltati...';
|
||||
|
||||
const result = await window.api.listSkippedFiles();
|
||||
showPreview('skipped', 'Anteprima __SALTATI', result, 'Nessun file presente in __SALTATI.');
|
||||
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
|
||||
} catch (error) {
|
||||
destinationStatus.textContent = `Errore: ${error.message}`;
|
||||
@@ -263,9 +322,12 @@ clearPreviewBtn.addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
const isDuplicates = currentPreviewKind === 'duplicates';
|
||||
const isSkipped = currentPreviewKind === 'skipped';
|
||||
const confirmMessage = isDuplicates
|
||||
? 'Confermi la pulizia della cartella duplicati?'
|
||||
: 'Confermi la pulizia della cartella non smistati?';
|
||||
? 'Confermi la pulizia della cartella __DUPLICATI?'
|
||||
: isSkipped
|
||||
? 'Confermi la pulizia della cartella saltati?'
|
||||
: 'Confermi la pulizia della cartella non smistati?';
|
||||
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
@@ -277,10 +339,12 @@ clearPreviewBtn.addEventListener('click', async () => {
|
||||
|
||||
const clearResult = await clearPreviewData(currentPreviewKind);
|
||||
const refreshed = await loadPreviewData(currentPreviewKind);
|
||||
const title = isDuplicates ? 'Anteprima duplicati' : 'Anteprima __NON_SMISTATI';
|
||||
const title = isDuplicates ? 'Anteprima __DUPLICATI' : isSkipped ? 'Anteprima __SALTATI' : 'Anteprima __NON_SMISTATI';
|
||||
const emptyMessage = isDuplicates
|
||||
? 'Nessun file presente in duplicati.'
|
||||
: 'Nessun file presente in __NON_SMISTATI.';
|
||||
? 'Nessun file presente in __DUPLICATI.'
|
||||
: isSkipped
|
||||
? 'Nessun file presente in __SALTATI.'
|
||||
: 'Nessun file presente in __NON_SMISTATI.';
|
||||
|
||||
showPreview(currentPreviewKind, title, refreshed, emptyMessage);
|
||||
destinationStatus.textContent = `Cartella pulita: ${clearResult.directory}`;
|
||||
@@ -291,6 +355,7 @@ clearPreviewBtn.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
closePreviewBtn.addEventListener('click', hidePreview);
|
||||
previewOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === previewOverlay) {
|
||||
|
||||
@@ -4,13 +4,15 @@ const path = require('path');
|
||||
const CODE_FOLDER_REGEX = /^\d{3}$/;
|
||||
const MAX_SCAN_DEPTH = 6;
|
||||
|
||||
async function buildDestinationIndex(destinationRoot) {
|
||||
async function buildDestinationIndex(destinationRoot, onProgress) {
|
||||
const index = new Map();
|
||||
|
||||
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||
return index;
|
||||
}
|
||||
|
||||
let scanned = 0;
|
||||
|
||||
async function walk(currentDir, depth) {
|
||||
if (depth > MAX_SCAN_DEPTH) {
|
||||
return;
|
||||
@@ -29,6 +31,9 @@ async function buildDestinationIndex(destinationRoot) {
|
||||
}
|
||||
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
scanned += 1;
|
||||
onProgress?.({ phase: 'index-dest', scanned, folder: entry.name });
|
||||
|
||||
if (CODE_FOLDER_REGEX.test(entry.name)) {
|
||||
const rows = index.get(entry.name) || [];
|
||||
rows.push(fullPath);
|
||||
|
||||
99
services/destinationScanner.js
Normal file
99
services/destinationScanner.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { getCadInfo } = require('./router');
|
||||
|
||||
const CODE_FOLDER_REGEX = /^\d{3}$/;
|
||||
const MAX_SCAN_DEPTH = 6;
|
||||
|
||||
function toCadKey(cadInfo) {
|
||||
return String(cadInfo?.key || '').toLowerCase();
|
||||
}
|
||||
|
||||
async function buildDestinationIndexes(destinationRoot, onProgress) {
|
||||
const folderIndex = new Map();
|
||||
const cadKeyIndex = new Set();
|
||||
|
||||
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||
return { folderIndex, cadKeyIndex };
|
||||
}
|
||||
|
||||
let topEntries;
|
||||
try {
|
||||
topEntries = await fs.readdir(destinationRoot, { withFileTypes: true });
|
||||
} catch {
|
||||
return { folderIndex, cadKeyIndex };
|
||||
}
|
||||
|
||||
const topDirs = topEntries
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => ({ name: e.name, fullPath: path.join(destinationRoot, e.name) }));
|
||||
|
||||
const total = topDirs.length;
|
||||
let completed = 0;
|
||||
let scannedFiles = 0;
|
||||
|
||||
for (const entry of topEntries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const cadInfo = getCadInfo(entry.name);
|
||||
if (cadInfo) {
|
||||
scannedFiles += 1;
|
||||
cadKeyIndex.add(toCadKey(cadInfo));
|
||||
}
|
||||
}
|
||||
|
||||
async function walkSubtree(dirPath, depth) {
|
||||
if (depth > MAX_SCAN_DEPTH) return;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const subdirPromises = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (CODE_FOLDER_REGEX.test(entry.name)) {
|
||||
const rows = folderIndex.get(entry.name) || [];
|
||||
rows.push(fullPath);
|
||||
folderIndex.set(entry.name, rows);
|
||||
}
|
||||
subdirPromises.push(walkSubtree(fullPath, depth + 1));
|
||||
} else if (entry.isFile()) {
|
||||
const cadInfo = getCadInfo(entry.name);
|
||||
if (cadInfo) {
|
||||
scannedFiles += 1;
|
||||
onProgress?.({ phase: 'index-dup', scanned: scannedFiles, file: entry.name });
|
||||
cadKeyIndex.add(toCadKey(cadInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(subdirPromises);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
topDirs.map(async ({ name, fullPath }) => {
|
||||
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
|
||||
|
||||
if (CODE_FOLDER_REGEX.test(name)) {
|
||||
const rows = folderIndex.get(name) || [];
|
||||
rows.push(fullPath);
|
||||
folderIndex.set(name, rows);
|
||||
}
|
||||
|
||||
await walkSubtree(fullPath, 1);
|
||||
|
||||
completed += 1;
|
||||
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
|
||||
})
|
||||
);
|
||||
|
||||
return { folderIndex, cadKeyIndex };
|
||||
}
|
||||
|
||||
module.exports = { buildDestinationIndexes, toCadKey };
|
||||
@@ -8,13 +8,15 @@ function toCadKey(cadInfo) {
|
||||
return String(cadInfo?.key || '').toLowerCase();
|
||||
}
|
||||
|
||||
async function buildExistingCadKeyIndex(destinationRoot) {
|
||||
async function buildExistingCadKeyIndex(destinationRoot, onProgress) {
|
||||
const keys = new Set();
|
||||
|
||||
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
let scanned = 0;
|
||||
|
||||
async function walk(currentDir, depth) {
|
||||
if (depth > MAX_SCAN_DEPTH) {
|
||||
return;
|
||||
@@ -44,6 +46,8 @@ async function buildExistingCadKeyIndex(destinationRoot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
scanned += 1;
|
||||
onProgress?.({ phase: 'index-dup', scanned, file: entry.name });
|
||||
keys.add(toCadKey(cadInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
const { buildDestinationIndex } = require('./destinationIndex');
|
||||
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
|
||||
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
|
||||
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
|
||||
|
||||
function parseNumericVersion(version) {
|
||||
const rawVersion = String(version || '').trim();
|
||||
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return BigInt(rawVersion);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildHighestNumericVersionByCadKey(files) {
|
||||
const index = new Map();
|
||||
|
||||
for (const row of files) {
|
||||
const cadInfo = getCadInfo(row.fileName);
|
||||
if (!cadInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const version = parseNumericVersion(cadInfo.version);
|
||||
if (version === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = toCadKey(cadInfo);
|
||||
const current = index.get(key);
|
||||
if (current === undefined || version > current) {
|
||||
index.set(key, version);
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
async function collectFilesRecursively(rootDir) {
|
||||
const files = [];
|
||||
@@ -29,10 +65,12 @@ async function collectFilesRecursively(rootDir) {
|
||||
return files;
|
||||
}
|
||||
|
||||
async function processFolder(folder, config) {
|
||||
async function processFolder(folder, config, onProgress) {
|
||||
onProgress?.({ phase: 'scan' });
|
||||
const files = await collectFilesRecursively(folder);
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
|
||||
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
|
||||
const sourceMaxVersions = buildHighestNumericVersionByCadKey(files);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
copied: 0,
|
||||
@@ -42,18 +80,44 @@ async function processFolder(folder, config) {
|
||||
details: [],
|
||||
};
|
||||
|
||||
for (const { fullPath, fileName } of files) {
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { fullPath, fileName } = files[i];
|
||||
const file = fileName;
|
||||
result.scanned += 1;
|
||||
onProgress?.({ phase: 'copy', current: i + 1, total, file });
|
||||
|
||||
const cadInfo = getCadInfo(file);
|
||||
if (!cadInfo) {
|
||||
const skippedTarget = await getSkippedTarget(file);
|
||||
await fs.copy(fullPath, skippedTarget.destinationPath);
|
||||
result.skipped += 1;
|
||||
result.copied += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingCadKeys.has(toCadKey(cadInfo))) {
|
||||
const duplicateTarget = await getDuplicateTarget(file);
|
||||
const incomingVersion = parseNumericVersion(cadInfo.version);
|
||||
const highestInSource = sourceMaxVersions.get(toCadKey(cadInfo));
|
||||
if (incomingVersion !== null && highestInSource !== undefined && incomingVersion < highestInSource) {
|
||||
result.details.push({
|
||||
file,
|
||||
reason: 'Versione piu alta presente nei file da smistare',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const duplicateTarget = await prepareDuplicateTarget(file);
|
||||
if (!duplicateTarget.shouldCopy) {
|
||||
result.details.push({
|
||||
file,
|
||||
destination: duplicateTarget.destinationDir,
|
||||
reason: duplicateTarget.reason || 'Versione piu alta gia presente',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await fs.copy(fullPath, duplicateTarget.destinationPath, { overwrite: false });
|
||||
|
||||
result.copied += 1;
|
||||
@@ -70,7 +134,16 @@ async function processFolder(folder, config) {
|
||||
const destDir = decision.destination;
|
||||
|
||||
if (!destDir) {
|
||||
const unroutedTarget = await getUnroutedTarget(file);
|
||||
const unroutedTarget = await prepareUnroutedTarget(file);
|
||||
if (!unroutedTarget.shouldCopy) {
|
||||
result.details.push({
|
||||
file,
|
||||
destination: unroutedTarget.destinationDir,
|
||||
reason: unroutedTarget.reason || 'Versione piu alta gia presente',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await fs.copy(fullPath, unroutedTarget.destinationPath, { overwrite: false });
|
||||
|
||||
result.copied += 1;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
const path = require('path');
|
||||
|
||||
function normalizeCadType(ext) {
|
||||
const lowerExt = String(ext || '').toLowerCase();
|
||||
return lowerExt === 'drw' ? 'dwr' : lowerExt;
|
||||
return String(ext || '').toLowerCase();
|
||||
}
|
||||
|
||||
function getCadInfo(filename) {
|
||||
const baseName = path.basename(filename);
|
||||
const match = baseName.match(/^(.*)\.(prt|asm|drw|dwr)(?:\.([^.]+))?$/i);
|
||||
const match = baseName.match(/^(.*)\.(prt|asm|drw)(?:\.([^.]+))?$/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
@@ -17,6 +16,9 @@ function getCadInfo(filename) {
|
||||
const version = match[3] || '';
|
||||
const digits = rawCode.replace(/\D/g, '');
|
||||
const routingGroup = digits.length >= 5 ? digits.slice(2, 5) : null;
|
||||
const subfolder = digits.length >= 2
|
||||
? (digits[1] === '1' ? 'A' : digits[1] === '2' ? 'M_B' : null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
code: rawCode,
|
||||
@@ -25,6 +27,7 @@ function getCadInfo(filename) {
|
||||
version,
|
||||
key: `${rawCode}.${type}`,
|
||||
routingGroup,
|
||||
subfolder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,7 +61,14 @@ function getDestinationDecision(filename, config, destinationIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
return { destination: candidates[0], reason: null };
|
||||
if (!cadInfo.subfolder) {
|
||||
return {
|
||||
destination: null,
|
||||
reason: `Prefisso non riconosciuto (${cadInfo.digits[1]}): atteso 91 (→ A) o 92 (→ M_B)`,
|
||||
};
|
||||
}
|
||||
|
||||
return { destination: path.join(candidates[0], cadInfo.subfolder), reason: null };
|
||||
}
|
||||
|
||||
function findDestination(filename, config, destinationIndex) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { getCadInfo } = require('./router');
|
||||
|
||||
const PRIMARY_UNROUTED_DIR = '/cadroute/__NON_SMISTATI';
|
||||
const HOME_UNROUTED_DIR = path.join(os.homedir(), '.cadroute', '__NON_SMISTATI');
|
||||
const PRIMARY_DUPLICATES_DIR = '/cadroute/duplicati';
|
||||
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', 'duplicati');
|
||||
const PRIMARY_DUPLICATES_DIR = '/cadroute/__DUPLICATI';
|
||||
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', '__DUPLICATI');
|
||||
const PRIMARY_SKIPPED_DIR = '/cadroute/__SALTATI';
|
||||
const HOME_SKIPPED_DIR = path.join(os.homedir(), '.cadroute', '__SALTATI');
|
||||
|
||||
const SPECIAL_TARGETS = {
|
||||
unrouted: {
|
||||
@@ -16,6 +19,10 @@ const SPECIAL_TARGETS = {
|
||||
primary: PRIMARY_DUPLICATES_DIR,
|
||||
fallback: HOME_DUPLICATES_DIR,
|
||||
},
|
||||
skipped: {
|
||||
primary: PRIMARY_SKIPPED_DIR,
|
||||
fallback: HOME_SKIPPED_DIR,
|
||||
},
|
||||
};
|
||||
|
||||
const resolvedDirs = new Map();
|
||||
@@ -83,6 +90,116 @@ async function getDuplicateTarget(fileName) {
|
||||
return getTarget('duplicates', fileName);
|
||||
}
|
||||
|
||||
function parseNumericVersion(version) {
|
||||
const rawVersion = String(version || '').trim();
|
||||
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return BigInt(rawVersion);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCadKey(cadInfo) {
|
||||
return String(cadInfo?.key || '').toLowerCase();
|
||||
}
|
||||
|
||||
async function findComparableVersionFiles(destinationDir, incomingCadInfo) {
|
||||
const incomingKey = normalizeCadKey(incomingCadInfo);
|
||||
const entries = await fs.readdir(destinationDir, { withFileTypes: true }).catch(() => []);
|
||||
const comparable = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cadInfo = getCadInfo(entry.name);
|
||||
if (!cadInfo || normalizeCadKey(cadInfo) !== incomingKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const numericVersion = parseNumericVersion(cadInfo.version);
|
||||
if (numericVersion === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
comparable.push({
|
||||
path: path.join(destinationDir, entry.name),
|
||||
version: numericVersion,
|
||||
name: entry.name,
|
||||
});
|
||||
}
|
||||
|
||||
return comparable;
|
||||
}
|
||||
|
||||
async function prepareSpecialTarget(kind, fileName) {
|
||||
const destinationDir = await resolveTargetDir(kind);
|
||||
const incomingCadInfo = getCadInfo(fileName);
|
||||
if (!incomingCadInfo) {
|
||||
const destinationPath = await getUniquePath(destinationDir, fileName);
|
||||
return { shouldCopy: true, destinationDir, destinationPath };
|
||||
}
|
||||
|
||||
const incomingVersion = parseNumericVersion(incomingCadInfo.version);
|
||||
if (incomingVersion === null) {
|
||||
const destinationPath = await getUniquePath(destinationDir, fileName);
|
||||
return { shouldCopy: true, destinationDir, destinationPath };
|
||||
}
|
||||
|
||||
const comparable = await findComparableVersionFiles(destinationDir, incomingCadInfo);
|
||||
if (!comparable.length) {
|
||||
const destinationPath = await getUniquePath(destinationDir, fileName);
|
||||
return { shouldCopy: true, destinationDir, destinationPath };
|
||||
}
|
||||
|
||||
const highest = comparable.reduce((max, row) => (row.version > max.version ? row : max));
|
||||
if (incomingVersion <= highest.version) {
|
||||
return {
|
||||
shouldCopy: false,
|
||||
destinationDir,
|
||||
reason: `Versione piu alta gia presente (${highest.name})`,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(comparable.map((row) => fs.remove(row.path)));
|
||||
const destinationPath = await getUniquePath(destinationDir, fileName);
|
||||
return {
|
||||
shouldCopy: true,
|
||||
destinationDir,
|
||||
destinationPath,
|
||||
cleaned: comparable.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareUnroutedTarget(fileName) {
|
||||
return prepareSpecialTarget('unrouted', fileName);
|
||||
}
|
||||
|
||||
async function prepareDuplicateTarget(fileName) {
|
||||
return prepareSpecialTarget('duplicates', fileName);
|
||||
}
|
||||
|
||||
async function resolveSkippedDir() {
|
||||
return resolveTargetDir('skipped');
|
||||
}
|
||||
|
||||
async function getSkippedTarget(fileName) {
|
||||
return getTarget('skipped', fileName);
|
||||
}
|
||||
|
||||
async function listSkippedFiles() {
|
||||
return listTargetFiles('skipped');
|
||||
}
|
||||
|
||||
async function clearSkippedFiles() {
|
||||
return clearTargetFiles('skipped');
|
||||
}
|
||||
|
||||
async function listFilesRecursively(rootDir) {
|
||||
const files = [];
|
||||
|
||||
@@ -156,14 +273,22 @@ async function clearDuplicateFiles() {
|
||||
module.exports = {
|
||||
getUnroutedTarget,
|
||||
getDuplicateTarget,
|
||||
getSkippedTarget,
|
||||
prepareUnroutedTarget,
|
||||
prepareDuplicateTarget,
|
||||
resolveUnroutedDir,
|
||||
resolveDuplicatesDir,
|
||||
resolveSkippedDir,
|
||||
listUnroutedFiles,
|
||||
listDuplicateFiles,
|
||||
listSkippedFiles,
|
||||
clearUnroutedFiles,
|
||||
clearDuplicateFiles,
|
||||
clearSkippedFiles,
|
||||
PRIMARY_UNROUTED_DIR,
|
||||
HOME_UNROUTED_DIR,
|
||||
PRIMARY_DUPLICATES_DIR,
|
||||
HOME_DUPLICATES_DIR,
|
||||
PRIMARY_SKIPPED_DIR,
|
||||
HOME_SKIPPED_DIR,
|
||||
};
|
||||
|
||||
@@ -3,14 +3,65 @@ const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
const { buildDestinationIndex } = require('./destinationIndex');
|
||||
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
|
||||
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
|
||||
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
|
||||
|
||||
function parseNumericVersion(version) {
|
||||
const rawVersion = String(version || '').trim();
|
||||
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return BigInt(rawVersion);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildZipHighestNumericVersionByCadKey(zipPath) {
|
||||
const index = new Map();
|
||||
let directory;
|
||||
|
||||
try {
|
||||
directory = await unzipper.Open.file(zipPath);
|
||||
} catch {
|
||||
return { index, total: 0 };
|
||||
}
|
||||
|
||||
for (const row of directory.files || []) {
|
||||
if (row.type !== 'File') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseName = path.basename(row.path || '');
|
||||
const cadInfo = getCadInfo(baseName);
|
||||
if (!cadInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const version = parseNumericVersion(cadInfo.version);
|
||||
if (version === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = toCadKey(cadInfo);
|
||||
const current = index.get(key);
|
||||
if (current === undefined || version > current) {
|
||||
index.set(key, version);
|
||||
}
|
||||
}
|
||||
|
||||
return { index, total: (directory.files || []).filter((f) => f.type === 'File').length };
|
||||
}
|
||||
|
||||
async function processZip(zipPath, config, onProgress) {
|
||||
onProgress?.({ phase: 'scan' });
|
||||
const { index: sourceMaxVersions, total } = await buildZipHighestNumericVersionByCadKey(zipPath);
|
||||
|
||||
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
|
||||
|
||||
async function processZip(zipPath, config) {
|
||||
const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true }));
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
copied: 0,
|
||||
@@ -20,6 +71,8 @@ async function processZip(zipPath, config) {
|
||||
details: [],
|
||||
};
|
||||
|
||||
let current = 0;
|
||||
|
||||
for await (const entry of stream) {
|
||||
if (entry.type !== 'File') {
|
||||
entry.autodrain();
|
||||
@@ -28,17 +81,42 @@ async function processZip(zipPath, config) {
|
||||
|
||||
const file = entry.path;
|
||||
const baseName = path.basename(file);
|
||||
current += 1;
|
||||
result.scanned += 1;
|
||||
onProgress?.({ phase: 'copy', current, total, file: baseName });
|
||||
|
||||
const cadInfo = getCadInfo(baseName);
|
||||
if (!cadInfo) {
|
||||
const skippedTarget = await getSkippedTarget(baseName);
|
||||
await pipeline(entry, fs.createWriteStream(skippedTarget.destinationPath));
|
||||
result.skipped += 1;
|
||||
entry.autodrain();
|
||||
result.copied += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingCadKeys.has(toCadKey(cadInfo))) {
|
||||
const duplicateTarget = await getDuplicateTarget(baseName);
|
||||
const incomingVersion = parseNumericVersion(cadInfo.version);
|
||||
const highestInSource = sourceMaxVersions.get(toCadKey(cadInfo));
|
||||
if (incomingVersion !== null && highestInSource !== undefined && incomingVersion < highestInSource) {
|
||||
result.details.push({
|
||||
file: baseName,
|
||||
reason: 'Versione piu alta presente nei file da smistare',
|
||||
});
|
||||
entry.autodrain();
|
||||
continue;
|
||||
}
|
||||
|
||||
const duplicateTarget = await prepareDuplicateTarget(baseName);
|
||||
if (!duplicateTarget.shouldCopy) {
|
||||
result.details.push({
|
||||
file: baseName,
|
||||
destination: duplicateTarget.destinationDir,
|
||||
reason: duplicateTarget.reason || 'Versione piu alta gia presente',
|
||||
});
|
||||
entry.autodrain();
|
||||
continue;
|
||||
}
|
||||
|
||||
await pipeline(entry, fs.createWriteStream(duplicateTarget.destinationPath));
|
||||
|
||||
result.copied += 1;
|
||||
@@ -55,7 +133,17 @@ async function processZip(zipPath, config) {
|
||||
const destDir = decision.destination;
|
||||
|
||||
if (!destDir) {
|
||||
const unroutedTarget = await getUnroutedTarget(baseName);
|
||||
const unroutedTarget = await prepareUnroutedTarget(baseName);
|
||||
if (!unroutedTarget.shouldCopy) {
|
||||
result.details.push({
|
||||
file: baseName,
|
||||
destination: unroutedTarget.destinationDir,
|
||||
reason: unroutedTarget.reason || 'Versione piu alta gia presente',
|
||||
});
|
||||
entry.autodrain();
|
||||
continue;
|
||||
}
|
||||
|
||||
await pipeline(entry, fs.createWriteStream(unroutedTarget.destinationPath));
|
||||
|
||||
result.copied += 1;
|
||||
|
||||
14
sop.md
14
sop.md
@@ -1,4 +1,4 @@
|
||||
# SOP – CAD File Router MVP
|
||||
# SOP – CadRoute MVP
|
||||
|
||||
## Obiettivo
|
||||
Realizzare un'app desktop (.exe) che:
|
||||
@@ -35,8 +35,8 @@ npm -v
|
||||
## 2. Creazione progetto
|
||||
|
||||
```
|
||||
mkdir cad-file-router
|
||||
cd cad-file-router
|
||||
mkdir cadroute
|
||||
cd cadroute
|
||||
npm init -y
|
||||
```
|
||||
|
||||
@@ -52,7 +52,7 @@ npm install electron-builder --save-dev
|
||||
## 3. Struttura progetto
|
||||
|
||||
```
|
||||
cad-file-router
|
||||
cadroute
|
||||
│
|
||||
├── package.json
|
||||
├── main.js
|
||||
@@ -261,7 +261,7 @@ contextBridge.exposeInMainWorld("api", {
|
||||
`renderer/index.html`
|
||||
|
||||
```
|
||||
<h2>CAD File Router</h2>
|
||||
<h2>CadRoute</h2>
|
||||
|
||||
<button onclick="window.api.selectFolder()">
|
||||
Process Folder
|
||||
@@ -298,7 +298,7 @@ Aggiungere nel package.json:
|
||||
|
||||
```
|
||||
"build": {
|
||||
"appId": "com.cad.router",
|
||||
"appId": "com.cadroute",
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
}
|
||||
@@ -315,7 +315,7 @@ Output:
|
||||
|
||||
```
|
||||
dist/
|
||||
cad-file-router Setup.exe
|
||||
CadRoute Setup.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user