DATABASE IN LOCALE

ChatGPT 5 9-8-25

CIAO, HO BISOGNO DI UNA APPLICAZIONE CHE GIRI IN LOCALE SOTTO WINDOWS O SOTTO LINUX CHE ABBIA I SEGUENTI REQUISITI: -- ARCHIVIAZIONE DI TESTI STRUTTURATI RECORD E CAMPI -- GESTIONE DI ALLEGATI -- RICERCA PER TERMINI SU CAMPI SELEZIONATI -- RICERCA PER TERMINE SU TUTTI I RECORD -- SORT DEI RECORD ED ESPORTAZIONE IN FORMATO TESTUALE COME POSSO PROCEDERE ?

Ciao!

Quello che chiedi è abbastanza specifico, ma rientra in un ambito ben definito: ti serve una piccola applicazione database documentale o gestionale leggero, installabile e utilizzabile in locale (Windows o Linux), senza dipendere da servizi cloud.

Ecco come puoi procedere, con tre possibili approcci:


1️⃣ Usare un software già pronto (senza programmare)

Se vuoi partire subito e non scrivere codice, puoi usare applicazioni “database desktop” già pronte che soddisfano i requisiti:

Software OS Funzioni principali Pro
LibreOffice Base Win/Linux Database relazionale, form personalizzabili, filtri e ordinamenti, allegati BLOB Gratuito, open-source, multipiattaforma
Kexi Win/Linux Simile a Access, gestione record e campi, allegati, query personalizzate Interfaccia semplice, open-source
SQLite + DB Browser Win/Linux Database SQLite, allegati come BLOB, ricerca SQL, esportazione testo/CSV Leggero, portabile
KeePassXC (con campi personalizzati) Win/Linux Archiviazione sicura di dati strutturati + allegati Sicurezza elevata, ricerca veloce

🔹 Vantaggi: pronti all’uso, nessuna o poca programmazione.
🔹 Svantaggi: interfacce più generiche, meno “cucite su misura”.


2️⃣ Creare un’app leggera in Python o Node.js

Se vuoi qualcosa su misura e controllare completamente funzioni e interfaccia, puoi sviluppare un’app usando:

  • Python + SQLite per i dati strutturati

  • Tkinter o PyQt per l’interfaccia grafica

  • Allegati salvati come file su disco e riferimenti nel database

  • Funzioni di ricerca con query SQL

  • Esportazione CSV o TXT integrata

📌 Architettura di base in Python:

  • SQLite → tabella con campi testo, data, numerici, ecc.

  • Colonna per path degli allegati (o BLOB)

  • Funzione di ricerca per campo specifico o globale (SELECT ... WHERE campo LIKE ...)

  • Funzione di ordinamento (ORDER BY)

  • Pulsante “Esporta” per salvare CSV/TXT

Questo approccio richiede un po’ di programmazione ma è portabile e leggerissimo.


3️⃣ Usare un framework no-code/low-code locale

Ci sono strumenti che ti permettono di costruire un’app anche senza saper programmare troppo, ma che girano in locale:

  • Appsmith (può girare in locale con Docker)

  • Budibase (può essere eseguito offline)

  • Toolr Database

  • PortaBase (molto leggero, per Linux e Android)

Puoi così:

  • Creare il database con i campi che vuoi

  • Aggiungere form di inserimento e ricerca

  • Fare sort ed esportazione

  • Gestire allegati


💡 Consiglio pratico per partire
Se non vuoi programmare: Kexi è la via più semplice e potente in locale.
Se vuoi imparare e avere controllo: Python + SQLite + PyQt è la soluzione più flessibile.

Se vuoi, posso prepararti uno schema già funzionante in Python con interfaccia grafica che fa esattamente quello che hai elencato.
Così potresti avviarlo su Windows o Linux senza configurazioni complesse.

===============

Perfetto, allora ti preparo uno script Python multipiattaforma che:

  • Archivia testi strutturati (record con campi personalizzati)

  • Gestisce allegati (file salvati in una cartella, con riferimento nel database)

  • Permette ricerca su campi specifici o su tutto il database

  • Ordina (sort) i record per campo scelto

  • Esporta in formato CSV o TXT


📦 Struttura del progetto

gestionale/
│-- main.py              # programma principale
│-- db.sqlite            # database SQLite
│-- allegati/            # cartella per allegati

💻 Codice di esempio (main.py)

import os
import sqlite3
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import shutil
import csv

DB_FILE = "db.sqlite"
ALLEGATI_DIR = "allegati"

# Inizializza cartella allegati
os.makedirs(ALLEGATI_DIR, exist_ok=True)

# Connessione database
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()

# Creazione tabella (se non esiste)
c.execute("""
CREATE TABLE IF NOT EXISTS records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titolo TEXT,
    descrizione TEXT,
    categoria TEXT,
    allegato TEXT
)
""")
conn.commit()

# Funzioni di gestione
def aggiungi_record():
    titolo = entry_titolo.get()
    descrizione = entry_descrizione.get()
    categoria = entry_categoria.get()

    if not titolo:
        messagebox.showerror("Errore", "Il titolo è obbligatorio.")
        return

    allegato_path = allegato_var.get()
    allegato_nome = ""
    if allegato_path:
        allegato_nome = os.path.basename(allegato_path)
        shutil.copy(allegato_path, os.path.join(ALLEGATI_DIR, allegato_nome))

    c.execute("INSERT INTO records (titolo, descrizione, categoria, allegato) VALUES (?, ?, ?, ?)",
              (titolo, descrizione, categoria, allegato_nome))
    conn.commit()
    aggiorna_lista()
    pulisci_campi()

def aggiorna_lista(filtro=""):
    for row in tree.get_children():
        tree.delete(row)
    if filtro:
        c.execute("SELECT * FROM records WHERE titolo LIKE ? OR descrizione LIKE ? OR categoria LIKE ?",
                  (f"%{filtro}%", f"%{filtro}%", f"%{filtro}%"))
    else:
        c.execute("SELECT * FROM records")
    for r in c.fetchall():
        tree.insert("", "end", values=r)

def pulisci_campi():
    entry_titolo.delete(0, tk.END)
    entry_descrizione.delete(0, tk.END)
    entry_categoria.delete(0, tk.END)
    allegato_var.set("")

def scegli_allegato():
    path = filedialog.askopenfilename()
    if path:
        allegato_var.set(path)

def esporta_csv():
    path = filedialog.asksaveasfilename(defaultextension=".csv")
    if path:
        c.execute("SELECT * FROM records")
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["ID", "Titolo", "Descrizione", "Categoria", "Allegato"])
            writer.writerows(c.fetchall())
        messagebox.showinfo("Esportazione", "Esportazione completata.")

# GUI
root = tk.Tk()
root.title("Gestione Archivi Locali")

frame_form = tk.Frame(root)
frame_form.pack(pady=5)

tk.Label(frame_form, text="Titolo:").grid(row=0, column=0)
entry_titolo = tk.Entry(frame_form, width=40)
entry_titolo.grid(row=0, column=1)

tk.Label(frame_form, text="Descrizione:").grid(row=1, column=0)
entry_descrizione = tk.Entry(frame_form, width=40)
entry_descrizione.grid(row=1, column=1)

tk.Label(frame_form, text="Categoria:").grid(row=2, column=0)
entry_categoria = tk.Entry(frame_form, width=40)
entry_categoria.grid(row=2, column=1)

allegato_var = tk.StringVar()
tk.Button(frame_form, text="Scegli allegato", command=scegli_allegato).grid(row=3, column=0)
tk.Label(frame_form, textvariable=allegato_var).grid(row=3, column=1)

tk.Button(frame_form, text="Aggiungi", command=aggiungi_record).grid(row=4, column=0, pady=5)
tk.Button(frame_form, text="Esporta CSV", command=esporta_csv).grid(row=4, column=1, pady=5)

frame_search = tk.Frame(root)
frame_search.pack(pady=5)
tk.Label(frame_search, text="Cerca:").pack(side=tk.LEFT)
search_var = tk.StringVar()
tk.Entry(frame_search, textvariable=search_var).pack(side=tk.LEFT)
tk.Button(frame_search, text="Vai", command=lambda: aggiorna_lista(search_var.get())).pack(side=tk.LEFT)

cols = ("ID", "Titolo", "Descrizione", "Categoria", "Allegato")
tree = ttk.Treeview(root, columns=cols, show="headings")
for col in cols:
    tree.heading(col, text=col)
tree.pack(fill=tk.BOTH, expand=True)

aggiorna_lista()
root.mainloop()

🚀 Come usarlo

  1. Installa Python (versione 3.8+)

  2. Salva il file come main.py

  3. Crea una cartella allegati nella stessa directory

  4. Avvia:

    python main.py
    
  5. L’interfaccia ti permette di:

    • Inserire record con campi strutturati

    • Allegare file

    • Cercare per parola chiave

    • Ordinare cliccando sulle intestazioni

    • Esportare in CSV


Se vuoi, posso anche aggiungere:

  • Ricerca solo su campi selezionati

  • Esportazione diretta in TXT

  • Funzione di sort personalizzato

  • Apertura allegato con doppio click

Vuoi che ti preparo subito anche queste funzioni extra così il gestionale è già “pronto per l’uso” al 100%?

====================

Perfetto — qui sotto trovi una versione migliorata dello script Python che include ricerca su campi selezionati, ricerca globale, sort personalizzato cliccando sulle intestazioni (toggle asc/desc), esportazione in CSV e TXT, e apertura allegati con doppio-click. Funziona su Windows e Linux (per Linux usa xdg-open per aprire i file).

Salvalo come main.py in una cartella vuota e crea — o lascia che lo script crei — la cartella allegati.


Requisiti

  • Python 3.8+

  • Tkinter (in genere incluso in Python)

  • Nessuna libreria esterna


Codice (main.py)

import os
import sqlite3
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import shutil
import csv
import time
import subprocess
import sys

DB_FILE = "db.sqlite"
ALLEGATI_DIR = "allegati"

os.makedirs(ALLEGATI_DIR, exist_ok=True)

conn = sqlite3.connect(DB_FILE)
c = conn.cursor()

c.execute("""
CREATE TABLE IF NOT EXISTS records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titolo TEXT,
    descrizione TEXT,
    categoria TEXT,
    note TEXT,
    allegato TEXT,
    created_at TEXT
)
""")
conn.commit()

# stato sort: {col_name: "ASC" or "DESC"}
sort_state = {"col": "id", "order": "ASC"}

def safe_filename_conflict(dest_dir, filename):
    base, ext = os.path.splitext(filename)
    candidate = filename
    i = 1
    while os.path.exists(os.path.join(dest_dir, candidate)):
        candidate = f"{base}_{i}{ext}"
        i += 1
    return candidate

def aggiungi_record():
    titolo = entry_titolo.get().strip()
    descrizione = entry_descrizione.get().strip()
    categoria = entry_categoria.get().strip()
    note = entry_note.get().strip()
    if not titolo:
        messagebox.showerror("Errore", "Titolo obbligatorio.")
        return

    allegato_path = allegato_var.get()
    allegato_nome = ""
    if allegato_path:
        allegato_nome = os.path.basename(allegato_path)
        allegato_nome = safe_filename_conflict(ALLEGATI_DIR, allegato_nome)
        try:
            shutil.copy(allegato_path, os.path.join(ALLEGATI_DIR, allegato_nome))
        except Exception as e:
            messagebox.showerror("Errore copia allegato", str(e))
            return

    created_at = time.strftime("%Y-%m-%d %H:%M:%S")
    c.execute("""INSERT INTO records (titolo, descrizione, categoria, note, allegato, created_at)
                 VALUES (?, ?, ?, ?, ?, ?)""",
              (titolo, descrizione, categoria, note, allegato_nome, created_at))
    conn.commit()
    aggiorna_lista()
    pulisci_campi()

def aggiorna_lista(filtro=None, fields=None):
    # Costruisce query con WHERE e ORDER BY basati su selezioni
    base = "SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records"
    params = []
    where_clause = ""
    if filtro:
        like = f"%{filtro}%"
        if fields:
            parts = []
            for f in fields:
                # mapping fields user->db column (safety)
                if f in ("Titolo", "Descrizione", "Categoria", "Note", "Allegato"):
                    dbcol = {
                        "Titolo": "titolo",
                        "Descrizione": "descrizione",
                        "Categoria": "categoria",
                        "Note": "note",
                        "Allegato": "allegato"
                    }[f]
                    parts.append(f"{dbcol} LIKE ?")
                    params.append(like)
            if parts:
                where_clause = " WHERE " + " OR ".join(parts)
        else:
            # ricerca globale su tutti text columns
            where_clause = " WHERE titolo LIKE ? OR descrizione LIKE ? OR categoria LIKE ? OR note LIKE ? OR allegato LIKE ?"
            params.extend([like, like, like, like, like])

    order_clause = f" ORDER BY {sort_state['col']} {sort_state['order']}"
    query = base + where_clause + order_clause
    c.execute(query, params)
    rows = c.fetchall()

    for row in tree.get_children():
        tree.delete(row)
    for r in rows:
        tree.insert("", "end", values=r)

def pulisci_campi():
    entry_titolo.delete(0, tk.END)
    entry_descrizione.delete(0, tk.END)
    entry_categoria.delete(0, tk.END)
    entry_note.delete(0, tk.END)
    allegato_var.set("")

def scegli_allegato():
    path = filedialog.askopenfilename()
    if path:
        allegato_var.set(path)

def esporta_csv():
    path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")])
    if not path:
        return
    c.execute("SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records")
    rows = c.fetchall()
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["ID", "Titolo", "Descrizione", "Categoria", "Note", "Allegato", "Created At"])
        writer.writerows(rows)
    messagebox.showinfo("Esportazione", f"CSV salvato in {path}")

def esporta_txt():
    path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files","*.txt")])
    if not path:
        return
    c.execute("SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records")
    rows = c.fetchall()
    with open(path, "w", encoding="utf-8") as f:
        for r in rows:
            # formato leggibile: campi separati da tab, ogni record su una riga, ma con etichette per chiarezza
            f.write(f"ID: {r[0]}\nTitolo: {r[1]}\nDescrizione: {r[2]}\nCategoria: {r[3]}\nNote: {r[4]}\nAllegato: {r[5]}\nCreato: {r[6]}\n")
            f.write("-" * 60 + "\n")
    messagebox.showinfo("Esportazione", f"TXT salvato in {path}")

def on_search_click():
    filtro = search_var.get().strip()
    # raccogli campi selezionati
    fields = []
    if chk_titolo_var.get(): fields.append("Titolo")
    if chk_descrizione_var.get(): fields.append("Descrizione")
    if chk_categoria_var.get(): fields.append("Categoria")
    if chk_note_var.get(): fields.append("Note")
    if chk_allegato_var.get(): fields.append("Allegato")
    if fields:
        aggiorna_lista(filtro=filtro if filtro else None, fields=fields if filtro else None)
    else:
        # nessun campo selezionato -> ricerca globale
        aggiorna_lista(filtro=filtro if filtro else None, fields=None)

def set_sort(col_idx):
    # mappa indice colonna a nome DB
    mapping = {
        0: "id",
        1: "titolo",
        2: "descrizione",
        3: "categoria",
        4: "note",
        5: "allegato",
        6: "created_at"
    }
    col = mapping.get(col_idx, "id")
    # toggle
    if sort_state["col"] == col:
        sort_state["order"] = "DESC" if sort_state["order"] == "ASC" else "ASC"
    else:
        sort_state["col"] = col
        sort_state["order"] = "ASC"
    aggiorna_lista(filtro=search_var.get().strip() or None,
                   fields=[f for f, v in [("Titolo", chk_titolo_var.get()),
                                          ("Descrizione", chk_descrizione_var.get()),
                                          ("Categoria", chk_categoria_var.get()),
                                          ("Note", chk_note_var.get()),
                                          ("Allegato", chk_allegato_var.get())] if v] or None)

def on_item_double_click(event):
    sel = tree.selection()
    if not sel:
        return
    item = tree.item(sel[0])
    values = item['values']
    allegato = values[5]  # col allegato
    if not allegato:
        messagebox.showinfo("Allegato", "Nessun allegato per questo record.")
        return
    path = os.path.join(ALLEGATI_DIR, allegato)
    if not os.path.exists(path):
        messagebox.showerror("Errore", f"File non trovato: {path}")
        return
    try:
        if sys.platform.startswith("win"):
            os.startfile(path)
        elif sys.platform.startswith("darwin"):
            subprocess.run(["open", path])
        else:
            subprocess.run(["xdg-open", path])
    except Exception as e:
        messagebox.showerror("Errore apertura", str(e))

# GUI
root = tk.Tk()
root.title("Gestionale Locale - Avanzato")
root.geometry("900x600")

frame_form = tk.Frame(root)
frame_form.pack(fill=tk.X, padx=8, pady=6)

tk.Label(frame_form, text="Titolo:").grid(row=0, column=0, sticky="e")
entry_titolo = tk.Entry(frame_form, width=40)
entry_titolo.grid(row=0, column=1, sticky="w")

tk.Label(frame_form, text="Descrizione:").grid(row=1, column=0, sticky="e")
entry_descrizione = tk.Entry(frame_form, width=70)
entry_descrizione.grid(row=1, column=1, columnspan=3, sticky="w")

tk.Label(frame_form, text="Categoria:").grid(row=0, column=2, sticky="e")
entry_categoria = tk.Entry(frame_form, width=30)
entry_categoria.grid(row=0, column=3, sticky="w")

tk.Label(frame_form, text="Note:").grid(row=2, column=0, sticky="e")
entry_note = tk.Entry(frame_form, width=70)
entry_note.grid(row=2, column=1, columnspan=3, sticky="w")

allegato_var = tk.StringVar()
tk.Button(frame_form, text="Scegli allegato...", command=scegli_allegato).grid(row=3, column=0, pady=6)
tk.Label(frame_form, textvariable=allegato_var, anchor="w").grid(row=3, column=1, columnspan=3, sticky="w")

tk.Button(frame_form, text="Aggiungi record", command=aggiungi_record).grid(row=4, column=0, pady=6)
tk.Button(frame_form, text="Esporta CSV", command=esporta_csv).grid(row=4, column=1, pady=6)
tk.Button(frame_form, text="Esporta TXT", command=esporta_txt).grid(row=4, column=2, pady=6)
tk.Button(frame_form, text="Ricarica", command=lambda: aggiorna_lista()).grid(row=4, column=3, pady=6)

# ricerca e selezione campi
frame_search = tk.Frame(root)
frame_search.pack(fill=tk.X, padx=8, pady=4)

tk.Label(frame_search, text="Cerca:").pack(side=tk.LEFT)
search_var = tk.StringVar()
tk.Entry(frame_search, textvariable=search_var, width=30).pack(side=tk.LEFT, padx=4)
tk.Button(frame_search, text="Cerca", command=on_search_click).pack(side=tk.LEFT, padx=4)

tk.Label(frame_search, text="  Cerca su:").pack(side=tk.LEFT, padx=6)
chk_titolo_var = tk.IntVar(value=1)
chk_descrizione_var = tk.IntVar(value=1)
chk_categoria_var = tk.IntVar(value=0)
chk_note_var = tk.IntVar(value=0)
chk_allegato_var = tk.IntVar(value=0)

tk.Checkbutton(frame_search, text="Titolo", variable=chk_titolo_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Descrizione", variable=chk_descrizione_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Categoria", variable=chk_categoria_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Note", variable=chk_note_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Allegato", variable=chk_allegato_var).pack(side=tk.LEFT)

# Treeview
cols = ("ID", "Titolo", "Descrizione", "Categoria", "Note", "Allegato", "Creato")
tree = ttk.Treeview(root, columns=cols, show="headings")
for i, col in enumerate(cols):
    tree.heading(col, text=col, command=lambda c=i: set_sort(c))
    tree.column(col, anchor="w", width=120 if i in (1,2) else 80)

tree.bind("<Double-1>", on_item_double_click)

vsb = ttk.Scrollbar(root, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=vsb.set)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
tree.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)

# inizializza
aggiorna_lista()
root.mainloop()

Note d’uso rapide

  • Ricerca su campi selezionati: seleziona le checkbox (Titolo, Descrizione, ...) e poi premere Cerca. Se nessuna checkbox è selezionata, la ricerca è globale.

  • Sort: clicca l’intestazione della colonna per ordinare; clic successive alternano ASC/DESC.

  • Aprire allegato: doppio-click sulla riga (se esiste un allegato) lo aprirà con l’app predefinita del sistema.

  • Esportazioni: CSV (tabulare) o TXT (leggibile con etichette).

  • Allegati: vengono copiati dentro la cartella allegati/ con nomi evitanti conflitti.


Se vuoi, procedo subito con una delle seguenti opzioni:

  1. Aggiungere modifica/eliminazione dei record dall’interfaccia.

  2. Migliorare la UI con PyQt (più bella ma richiede pip install PyQt5).

  3. Creare un pacchetto eseguibile per Windows (pyinstaller) o un pacchetto per Linux.

Dimmi quale preferisci e te lo preparo.

===========================

Ottimo — procedo subito. Qui sotto trovi:

  1. Il codice aggiornato (main.py) — include modifica e cancellazione dei record + gestione corretta dei percorsi quando l’app viene impacchettata con PyInstaller.

  2. Istruzioni per creare eseguibili (Windows / Linux) usando PyInstaller, con script di build pronti. Spiego anche i limiti (cross-compilazione) e il suggerimento migliore per distribuire l’app con la cartella allegati e il database.


1) main.py aggiornato (Edit / Delete + compatibilità PyInstaller)

Salva questo file come main.py. Gestisce: aggiungi, modifica, elimina, ricerca su campi selezionati o globale, sort, apertura allegati, esportazione CSV/TXT. Inoltre rileva se è eseguito come eseguibile (frozen) e usa la cartella dell’eseguibile come base per DB/allegati — così il pacchetto resterà coerente dopo il packaging.

#!/usr/bin/env python3
import os
import sqlite3
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import shutil
import csv
import time
import subprocess
import sys

# ------------------------------------------------
# Path base (funziona anche quando impacchettato)
# ------------------------------------------------
if getattr(sys, "frozen", False):
    BASE_DIR = os.path.dirname(sys.executable)
else:
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))

DB_FILE = os.path.join(BASE_DIR, "db.sqlite")
ALLEGATI_DIR = os.path.join(BASE_DIR, "allegati")

os.makedirs(ALLEGATI_DIR, exist_ok=True)

# Connessione DB
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()

c.execute("""
CREATE TABLE IF NOT EXISTS records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titolo TEXT,
    descrizione TEXT,
    categoria TEXT,
    note TEXT,
    allegato TEXT,
    created_at TEXT
)
""")
conn.commit()

# stato sort: {col_name: "ASC" or "DESC"}
sort_state = {"col": "id", "order": "ASC"}

# id del record in editing (None = modalità nuovo record)
editing_id = None

# -------------------------
# Funzioni utilitarie
# -------------------------
def safe_filename_conflict(dest_dir, filename):
    base, ext = os.path.splitext(filename)
    candidate = filename
    i = 1
    while os.path.exists(os.path.join(dest_dir, candidate)):
        candidate = f"{base}_{i}{ext}"
        i += 1
    return candidate

# -------------------------
# Funzioni CRUD
# -------------------------
def aggiungi_o_aggiorna_record():
    global editing_id
    titolo = entry_titolo.get().strip()
    descrizione = entry_descrizione.get().strip()
    categoria = entry_categoria.get().strip()
    note = entry_note.get().strip()
    if not titolo:
        messagebox.showerror("Errore", "Titolo obbligatorio.")
        return

    allegato_path = allegato_var.get()
    allegato_nome = ""

    # Se sto aggiornando e non ho scelto nuovo allegato, mantengo l'esistente
    if editing_id:
        c.execute("SELECT allegato FROM records WHERE id = ?", (editing_id,))
        cur = c.fetchone()
        existing_allegato = cur[0] if cur else ""
    else:
        existing_allegato = ""

    if allegato_path:
        allegato_nome = os.path.basename(allegato_path)
        allegato_nome = safe_filename_conflict(ALLEGATI_DIR, allegato_nome)
        try:
            shutil.copy(allegato_path, os.path.join(ALLEGATI_DIR, allegato_nome))
        except Exception as e:
            messagebox.showerror("Errore copia allegato", str(e))
            return
    else:
        allegato_nome = existing_allegato  # mantiene allegato precedente (o "")

    created_at = time.strftime("%Y-%m-%d %H:%M:%S")
    if editing_id:
        c.execute("""UPDATE records
                     SET titolo=?, descrizione=?, categoria=?, note=?, allegato=?
                     WHERE id=?""",
                  (titolo, descrizione, categoria, note, allegato_nome, editing_id))
        conn.commit()
        messagebox.showinfo("Modifica", "Record aggiornato.")
    else:
        c.execute("""INSERT INTO records (titolo, descrizione, categoria, note, allegato, created_at)
                     VALUES (?, ?, ?, ?, ?, ?)""",
                  (titolo, descrizione, categoria, note, allegato_nome, created_at))
        conn.commit()
        messagebox.showinfo("Aggiunta", "Record aggiunto.")

    aggiorna_lista()
    reset_form()

def elimina_record():
    sel = tree.selection()
    if not sel:
        messagebox.showwarning("Elimina", "Seleziona un record da eliminare.")
        return
    item = tree.item(sel[0])
    values = item["values"]
    rid = values[0]
    titul = values[1]
    if not messagebox.askyesno("Conferma eliminazione", f"Eliminare il record ID {rid} — '{titul}' ?"):
        return
    # opzionale: rimuovere file allegato dal filesystem? chiedo conferma
    allegato = values[5]
    if allegato:
        if messagebox.askyesno("Eliminare allegato?", f"Eliminare anche il file allegato '{allegato}' dalla cartella allegati?"):
            try:
                os.remove(os.path.join(ALLEGATI_DIR, allegato))
            except Exception:
                pass
    c.execute("DELETE FROM records WHERE id = ?", (rid,))
    conn.commit()
    aggiorna_lista()
    reset_form()
    messagebox.showinfo("Eliminazione", "Record eliminato.")

def carica_per_modifica():
    global editing_id
    sel = tree.selection()
    if not sel:
        messagebox.showwarning("Modifica", "Seleziona un record da modificare.")
        return
    item = tree.item(sel[0])
    values = item["values"]
    rid = values[0]
    c.execute("SELECT id, titolo, descrizione, categoria, note, allegato FROM records WHERE id = ?", (rid,))
    row = c.fetchone()
    if not row:
        messagebox.showerror("Errore", "Record non trovato.")
        return
    editing_id = row[0]
    entry_titolo.delete(0, tk.END); entry_titolo.insert(0, row[1])
    entry_descrizione.delete(0, tk.END); entry_descrizione.insert(0, row[2])
    entry_categoria.delete(0, tk.END); entry_categoria.insert(0, row[3])
    entry_note.delete(0, tk.END); entry_note.insert(0, row[4] or "")
    allegato_var.set("")  # per scegliere nuovo file; mantenimento automatico se non scelto

    btn_add.config(text="Salva modifica")
    btn_delete.config(state=tk.NORMAL)

def reset_form():
    global editing_id
    editing_id = None
    entry_titolo.delete(0, tk.END)
    entry_descrizione.delete(0, tk.END)
    entry_categoria.delete(0, tk.END)
    entry_note.delete(0, tk.END)
    allegato_var.set("")
    btn_add.config(text="Aggiungi record")
    btn_delete.config(state=tk.DISABLED)

# -------------------------
# Ricerca / Lista / Ordinamento
# -------------------------
def aggiorna_lista(filtro=None, fields=None):
    base = "SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records"
    params = []
    where_clause = ""
    if filtro:
        like = f"%{filtro}%"
        if fields:
            parts = []
            for f in fields:
                if f in ("Titolo", "Descrizione", "Categoria", "Note", "Allegato"):
                    dbcol = {
                        "Titolo": "titolo",
                        "Descrizione": "descrizione",
                        "Categoria": "categoria",
                        "Note": "note",
                        "Allegato": "allegato"
                    }[f]
                    parts.append(f"{dbcol} LIKE ?")
                    params.append(like)
            if parts:
                where_clause = " WHERE " + " OR ".join(parts)
        else:
            where_clause = " WHERE titolo LIKE ? OR descrizione LIKE ? OR categoria LIKE ? OR note LIKE ? OR allegato LIKE ?"
            params.extend([like, like, like, like, like])

    order_clause = f" ORDER BY {sort_state['col']} {sort_state['order']}"
    query = base + where_clause + order_clause
    c.execute(query, params)
    rows = c.fetchall()

    for row in tree.get_children():
        tree.delete(row)
    for r in rows:
        tree.insert("", "end", values=r)

def on_search_click():
    filtro = search_var.get().strip()
    fields = []
    if chk_titolo_var.get(): fields.append("Titolo")
    if chk_descrizione_var.get(): fields.append("Descrizione")
    if chk_categoria_var.get(): fields.append("Categoria")
    if chk_note_var.get(): fields.append("Note")
    if chk_allegato_var.get(): fields.append("Allegato")
    if fields:
        aggiorna_lista(filtro=filtro if filtro else None, fields=fields if filtro else None)
    else:
        aggiorna_lista(filtro=filtro if filtro else None, fields=None)

def set_sort(col_idx):
    mapping = {
        0: "id",
        1: "titolo",
        2: "descrizione",
        3: "categoria",
        4: "note",
        5: "allegato",
        6: "created_at"
    }
    col = mapping.get(col_idx, "id")
    if sort_state["col"] == col:
        sort_state["order"] = "DESC" if sort_state["order"] == "ASC" else "ASC"
    else:
        sort_state["col"] = col
        sort_state["order"] = "ASC"
    on_search_click()

# -------------------------
# Allegati (scegli + apri)
# -------------------------
def scegli_allegato():
    path = filedialog.askopenfilename()
    if path:
        allegato_var.set(path)

def on_item_double_click(event):
    sel = tree.selection()
    if not sel:
        return
    item = tree.item(sel[0])
    values = item['values']
    allegato = values[5]  # col allegato
    if not allegato:
        messagebox.showinfo("Allegato", "Nessun allegato per questo record.")
        return
    path = os.path.join(ALLEGATI_DIR, allegato)
    if not os.path.exists(path):
        messagebox.showerror("Errore", f"File non trovato: {path}")
        return
    try:
        if sys.platform.startswith("win"):
            os.startfile(path)
        elif sys.platform.startswith("darwin"):
            subprocess.run(["open", path])
        else:
            subprocess.run(["xdg-open", path])
    except Exception as e:
        messagebox.showerror("Errore apertura", str(e))

# -------------------------
# Esportazione
# -------------------------
def esporta_csv():
    path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")])
    if not path:
        return
    c.execute("SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records")
    rows = c.fetchall()
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["ID", "Titolo", "Descrizione", "Categoria", "Note", "Allegato", "Created At"])
        writer.writerows(rows)
    messagebox.showinfo("Esportazione", f"CSV salvato in {path}")

def esporta_txt():
    path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files","*.txt")])
    if not path:
        return
    c.execute("SELECT id, titolo, descrizione, categoria, note, allegato, created_at FROM records")
    rows = c.fetchall()
    with open(path, "w", encoding="utf-8") as f:
        for r in rows:
            f.write(f"ID: {r[0]}\nTitolo: {r[1]}\nDescrizione: {r[2]}\nCategoria: {r[3]}\nNote: {r[4]}\nAllegato: {r[5]}\nCreato: {r[6]}\n")
            f.write("-" * 60 + "\n")
    messagebox.showinfo("Esportazione", f"TXT salvato in {path}")

# -------------------------
# GUI
# -------------------------
root = tk.Tk()
root.title("Gestionale Locale - Avanzato (Edit/Delete)")
root.geometry("980x640")

frame_form = tk.Frame(root)
frame_form.pack(fill=tk.X, padx=8, pady=6)

tk.Label(frame_form, text="Titolo:").grid(row=0, column=0, sticky="e")
entry_titolo = tk.Entry(frame_form, width=60)
entry_titolo.grid(row=0, column=1, columnspan=3, sticky="w")

tk.Label(frame_form, text="Descrizione:").grid(row=1, column=0, sticky="e")
entry_descrizione = tk.Entry(frame_form, width=90)
entry_descrizione.grid(row=1, column=1, columnspan=3, sticky="w")

tk.Label(frame_form, text="Categoria:").grid(row=0, column=4, sticky="e")
entry_categoria = tk.Entry(frame_form, width=30)
entry_categoria.grid(row=0, column=5, sticky="w")

tk.Label(frame_form, text="Note:").grid(row=2, column=0, sticky="e")
entry_note = tk.Entry(frame_form, width=90)
entry_note.grid(row=2, column=1, columnspan=5, sticky="w")

allegato_var = tk.StringVar()
tk.Button(frame_form, text="Scegli allegato...", command=scegli_allegato).grid(row=3, column=0, pady=6)
tk.Label(frame_form, textvariable=allegato_var, anchor="w").grid(row=3, column=1, columnspan=5, sticky="w")

btn_add = tk.Button(frame_form, text="Aggiungi record", command=aggiungi_o_aggiorna_record)
btn_add.grid(row=4, column=1, pady=6)
btn_export_csv = tk.Button(frame_form, text="Esporta CSV", command=esporta_csv)
btn_export_csv.grid(row=4, column=2, pady=6)
btn_export_txt = tk.Button(frame_form, text="Esporta TXT", command=esporta_txt)
btn_export_txt.grid(row=4, column=3, pady=6)
btn_reload = tk.Button(frame_form, text="Ricarica", command=lambda: aggiorna_lista())
btn_reload.grid(row=4, column=4, pady=6)

btn_edit = tk.Button(frame_form, text="Modifica selezionato", command=carica_per_modifica)
btn_edit.grid(row=4, column=0, pady=6)
btn_delete = tk.Button(frame_form, text="Elimina selezionato", command=elimina_record, state=tk.DISABLED)
btn_delete.grid(row=4, column=5, pady=6)

# ricerca
frame_search = tk.Frame(root)
frame_search.pack(fill=tk.X, padx=8, pady=6)

tk.Label(frame_search, text="Cerca:").pack(side=tk.LEFT)
search_var = tk.StringVar()
tk.Entry(frame_search, textvariable=search_var, width=36).pack(side=tk.LEFT, padx=4)
tk.Button(frame_search, text="Cerca", command=on_search_click).pack(side=tk.LEFT, padx=4)

tk.Label(frame_search, text="  Cerca su:").pack(side=tk.LEFT, padx=6)
chk_titolo_var = tk.IntVar(value=1)
chk_descrizione_var = tk.IntVar(value=1)
chk_categoria_var = tk.IntVar(value=0)
chk_note_var = tk.IntVar(value=0)
chk_allegato_var = tk.IntVar(value=0)

tk.Checkbutton(frame_search, text="Titolo", variable=chk_titolo_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Descrizione", variable=chk_descrizione_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Categoria", variable=chk_categoria_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Note", variable=chk_note_var).pack(side=tk.LEFT)
tk.Checkbutton(frame_search, text="Allegato", variable=chk_allegato_var).pack(side=tk.LEFT)

# Treeview
cols = ("ID", "Titolo", "Descrizione", "Categoria", "Note", "Allegato", "Creato")
tree = ttk.Treeview(root, columns=cols, show="headings", selectmode="browse")
for i, col in enumerate(cols):
    tree.heading(col, text=col, command=lambda c=i: set_sort(c))
    tree.column(col, anchor="w", width=160 if i in (1,2) else 100)

tree.bind("<Double-1>", on_item_double_click)

vsb = ttk.Scrollbar(root, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=vsb.set)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
tree.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)

# inizializza
aggiorna_lista()
root.mainloop()

2) Creare l’eseguibile con PyInstaller

Requisiti

  • Python 3.8+ installato sulla macchina di build.

  • pip install pyinstaller

  • Assicurati che lo script funzioni come python main.py prima del packaging.


Nota importante sulla cross-compilation

PyInstaller non effettua cross-compilation completa: normalmente devi costruire il .exe su Windows e il pacchetto Linux su Linux. Esistono workaround (es. Wine) ma sono meno semplici e non sempre affidabili. Quindi: usa la macchina target per costruire o una VM.


Consigli di packaging

  • Consiglio pratico: usa --onedir (cartella) invece di --onefile per semplificare la gestione della cartella allegati e del DB (più trasparente per l’utente).

  • Se usi --onefile, l’eseguibile estrarrà temporaneamente i file in cartelle temporanee all’avvio — questo complica la persistenza di db.sqlite e allegati. Lo script è pensato per usare la cartella dell’eseguibile, quindi onedir è il più semplice.


Script di build per Windows (build_windows.bat)

Crea un file build_windows.bat nella cartella del progetto (dove c'è main.py, allegati/, eventuale db.sqlite) con questo contenuto:

@echo off
REM Assicurati di avere pip install pyinstaller
pyinstaller --noconsole --onedir --add-data "allegati;allegati" --add-data "db.sqlite;." main.py
echo Build completata. Controlla la cartella dist\main
pause
  • --noconsole rimuove la console (utile su Windows per GUI pure). Se vuoi vedere la console per debug, rimuovilo.

  • --add-data "allegati;allegati" copia la cartella allegati nella cartella dist. (Sintassi Windows: ; come separatore)

  • --add-data "db.sqlite;." copia il DB se vuoi pre-popolare. Se non esiste, verrà creato al primo avvio.

Dopo l’esecuzione troverai dist\main\ con un main.exe e la cartella allegati e (se aggiunta) db.sqlite.


Script di build per Linux (build_linux.sh)

Crea build_linux.sh:

#!/bin/bash
# Assicurati di avere pip install pyinstaller
pyinstaller --noconsole --onedir --add-data "allegati:allegati" --add-data "db.sqlite:." main.py
echo "Build completata. Controlla la cartella dist/main"

Rendi eseguibile: chmod +x build_linux.sh e poi ./build_linux.sh.

(Su Linux il separatore --add-data è :).


Cose da controllare dopo il build

  1. Avvia l’eseguibile dentro la cartella dist/main/ (Windows: dist\main\main.exe, Linux: dist/main/main) e verifica che:

    • il DB venga creato o letto nella cartella corretta;

    • la cartella allegati sia presente e i doppio-click aprano i file.

  2. Se preferisci che il DB e gli allegati siano nella cartella User (es. %APPDATA% su Windows o ~/.local/share/myapp su Linux), posso aggiungere al codice una logica per usare una cartella dati utente; fammi sapere se la preferisci.


3) Consegna / Distribuzione consigliata

  • Distribuzione semplice: comprimere la cartella dist/main (ZIP) e condividere. L’utente scompatta e lancia l’eseguibile. Tutto rimane locale.

  • Installer (.msi/.exe): si può creare un installer Windows (Inno Setup, NSIS) se vuoi una UX professionale — te lo posso preparare dopo.

  • Pacchetto Linux: per Linux puoi distribuire la cartella oppure creare .deb/AppImage; anche questo posso aiutarti a crearli su richiesta.


Vuoi che:

  1. Prepari gli script di build già salvati in una cartella e te li fornisca (file .bat e .sh) — così li scarichi e lanci; oppure

  2. Aggiunga la logica per usare una cartella dati utente (es. %APPDATA%/GestionaleLocale o ~/.local/share/gestionale) per DB e allegati (consigliato se più utenti useranno la stessa macchina).

Dimmi quale preferisci e procedo — se vuoi, creo anche il .bat e lo sh già pronti e ti fornisco le istruzioni rapide per eseguirli.

====================

scusa non mi ero spiegato bene : Potresti scrivere l'APP che hai creato con la struttura del record che contenga questi campi? Intestazione Autore Altri contributi Titolo principale Titoli secondari Testo Immagine Link Descrizione Note Categorie Soggetti Parole chiave

-----------------

Perfetto, ora ho capito: vuoi un’applicazione (React, con Tailwind e una struttura di record) che gestisca i dati secondo i campi che hai elencato.
Ti preparo un esempio base di app CRUD (crea, leggi, aggiorna, elimina) in cui ogni elemento è un “record” con questa struttura:

// Record.js — struttura del record
export const createEmptyRecord = () => ({
  intestazione: "",
  autore: "",
  altriContributi: "",
  titoloPrincipale: "",
  titoliSecondari: "",
  testo: "",
  immagine: "",
  link: "",
  descrizione: "",
  note: "",
  categorie: "",
  soggetti: "",
  paroleChiave: ""
});

Ecco un esempio completo di React App per inserire e visualizzare questi record:

export default function App() {
  const [records, setRecords] = React.useState([]);
  const [form, setForm] = React.useState(createEmptyRecord());

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  };

  const handleAdd = () => {
    if (!form.titoloPrincipale.trim()) return;
    setRecords([...records, { ...form }]);
    setForm(createEmptyRecord());
  };

  return (
    <div className="p-6 max-w-5xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">Gestione Record</h1>
      <div className="grid grid-cols-2 gap-4">
        {/* Form di inserimento */}
        <div className="bg-white p-4 rounded-2xl shadow">
          {Object.keys(form).map((field) => (
            <div key={field} className="mb-2">
              <label className="block text-sm font-medium mb-1 capitalize">
                {field}
              </label>
              <input
                type="text"
                name={field}
                value={form[field]}
                onChange={handleChange}
                className="w-full border rounded-lg p-2"
              />
            </div>
          ))}
          <button
            onClick={handleAdd}
            className="mt-4 bg-blue-500 text-white px-4 py-2 rounded-lg"
          >
            Aggiungi Record
          </button>
        </div>

        {/* Lista record */}
        <div className="space-y-4">
          {records.map((rec, i) => (
            <div
              key={i}
              className="bg-gray-100 p-4 rounded-xl shadow space-y-1"
            >
              <h2 className="text-lg font-bold">{rec.titoloPrincipale}</h2>
              {rec.immagine && (
                <img
                  src={rec.immagine}
                  alt=""
                  className="max-h-48 rounded-xl"
                />
              )}
              <p className="text-sm text-gray-700">{rec.descrizione}</p>
              <a
                href={rec.link}
                className="text-blue-600 underline text-sm"
                target="_blank"
                rel="noreferrer"
              >
                Link
              </a>
              <div className="text-xs text-gray-500">
                {rec.categorie} | {rec.soggetti} | {rec.paroleChiave}
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Questo esempio:

  • Usa un oggetto record con i campi che hai chiesto.

  • Ha un form dinamico: i campi vengono generati in base alla struttura createEmptyRecord() così se in futuro aggiungi un campo basta modificarlo lì.

  • Mostra i record inseriti a destra, con titolo, immagine, descrizione, link e metadati.

Se vuoi, posso prepararti la versione completa già pronta da incollare in un progetto React con Tailwind così puoi aprirla in un browser e usarla subito.
Vuoi che la faccia così?

-----------------------------



Commenti

Post popolari in questo blog

ChatGPT (Free e Plus)

tablet E‑Ink -Obsidian