diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..1b877d8
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,3 @@
+def classFactory(iface):
+ from .main_plugin import AnimationPlugin
+ return AnimationPlugin(iface)
diff --git a/animation_form.py b/animation_form.py
new file mode 100644
index 0000000..e69de29
diff --git a/db_manager.py b/db_manager.py
new file mode 100644
index 0000000..903126f
--- /dev/null
+++ b/db_manager.py
@@ -0,0 +1,173 @@
+import psycopg2
+import json
+
+class DBManager:
+ def __init__(self, host, port, dbname, user, password):
+ self.conn = psycopg2.connect(
+ host=host, port=port, dbname=dbname, user=user, password=password
+ )
+ self.conn.autocommit = False
+
+ def close(self):
+ if self.conn:
+ self.conn.close()
+
+ def fetch_all(self, query, params=None):
+ with self.conn.cursor() as cur:
+ cur.execute(query, params)
+ return cur.fetchall()
+
+ def execute(self, query, params=None):
+ with self.conn.cursor() as cur:
+ cur.execute(query, params)
+ self.conn.commit()
+
+ # --- Reference data loaders ---
+ def get_animateurs(self):
+ return self.fetch_all("SELECT code_animateur, animateur FROM animation.animateurs ORDER BY animateur")
+
+ def get_lieux(self):
+ return self.fetch_all("SELECT code_lieux, lieux FROM animation.lieux_anim ORDER BY lieux")
+
+ def get_themes(self):
+ return self.fetch_all("SELECT code_type_anim, theme_animation FROM animation.theme_animation ORDER BY theme_animation")
+
+ def get_types_groupe(self):
+ return self.fetch_all("SELECT code_groupe, type_groupe FROM animation.type_groupe ORDER BY type_groupe")
+
+ def get_codes_annulation(self):
+ return self.fetch_all("SELECT code_annul, type_annulation FROM animation.code_annulation ORDER BY type_annulation")
+
+ def get_type_dossier_ens(self):
+ return self.fetch_all("SELECT code_dossier_ens, type_dossier_ens FROM animation.type_dossier_ens ORDER BY type_dossier_ens")
+
+ def get_etablissements(self):
+ return self.fetch_all("SELECT code_etablissement, nom_etablissement, commune FROM animation.liste_etablissements ORDER BY nom_etablissement")
+
+ def get_all_animations(self):
+ """Retourne toutes les animations pour la liste déroulante (date + lieu + thème)."""
+ return self.fetch_all("""
+ SELECT da.code_animation,
+ da.date_anim,
+ da.lieux,
+ ta.theme_animation
+ FROM animation.donnees_animation da
+ LEFT JOIN animation.theme_animation ta ON ta.code_type_anim = da.code_type_anim
+ ORDER BY da.date_anim DESC, da.code_animation DESC
+ """)
+
+ def get_animation_by_id(self, code_animation):
+ rows = self.fetch_all(
+ "SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,)
+ )
+ if rows:
+ with self.conn.cursor() as cur:
+ cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,))
+ cols = [d[0] for d in cur.description]
+ cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,))
+ row = cur.fetchone()
+ return dict(zip(cols, row)) if row else None
+ return None
+
+ # --- Insertions ---
+ def add_animateur(self, nom):
+ rows = self.fetch_all("SELECT MAX(code_animateur) FROM animation.animateurs")
+ next_code = (rows[0][0] or 0) + 1
+ self.execute("INSERT INTO animation.animateurs (code_animateur, animateur) VALUES (%s, %s)", (next_code, nom))
+ return next_code, nom
+
+ def add_lieux(self, lieux, type_lieux='', type_lieux_regroupement='', geom_wkt=None, srid=4326):
+ rows = self.fetch_all("SELECT MAX(code_lieux) FROM animation.lieux_anim")
+ next_code = (rows[0][0] or 0) + 1
+ if geom_wkt:
+ self.execute(
+ "INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement, geom) "
+ "VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))",
+ (next_code, lieux, type_lieux, type_lieux_regroupement, geom_wkt, srid)
+ )
+ else:
+ self.execute(
+ "INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement) VALUES (%s,%s,%s,%s)",
+ (next_code, lieux, type_lieux, type_lieux_regroupement)
+ )
+ return next_code, lieux
+
+ def add_theme(self, theme, famille=''):
+ rows = self.fetch_all("SELECT MAX(code_type_anim) FROM animation.theme_animation")
+ next_code = (rows[0][0] or 0) + 1
+ self.execute("INSERT INTO animation.theme_animation (code_type_anim, theme_animation, famille_theme_animation) VALUES (%s,%s,%s)",
+ (next_code, theme, famille))
+ return next_code, theme
+
+ def add_annulation(self, type_annulation):
+ rows = self.fetch_all("SELECT MAX(code_annul) FROM animation.code_annulation")
+ next_code = (rows[0][0] or 0) + 1
+ self.execute("INSERT INTO animation.code_annulation (code_annul, type_annulation) VALUES (%s,%s)",
+ (next_code, type_annulation))
+ return next_code, type_annulation
+
+ def add_type_dossier(self, type_dossier):
+ rows = self.fetch_all("SELECT MAX(code_dossier_ens) FROM animation.type_dossier_ens")
+ next_code = (rows[0][0] or 0) + 1
+ self.execute("INSERT INTO animation.type_dossier_ens (code_dossier_ens, type_dossier_ens) VALUES (%s,%s)",
+ (next_code, type_dossier))
+ return next_code, type_dossier
+
+ def add_etablissement(self, nom, commune, prive=False, geom_wkt=None, srid=4326):
+ rows = self.fetch_all("SELECT MAX(code_etablissement) FROM animation.liste_etablissements")
+ next_code = (rows[0][0] or 0) + 1
+ if geom_wkt:
+ self.execute(
+ "INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune, geom) "
+ "VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))",
+ (next_code, nom, prive, commune, geom_wkt, srid)
+ )
+ else:
+ self.execute(
+ "INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune) VALUES (%s,%s,%s,%s)",
+ (next_code, nom, prive, commune)
+ )
+ return next_code, nom, commune
+
+ def get_next_code_animation(self):
+ rows = self.fetch_all("SELECT MAX(code_animation) FROM animation.donnees_animation")
+ return (rows[0][0] or 0) + 1
+
+ def insert_donnees_animation(self, data: dict):
+ sql = """
+ INSERT INTO animation.donnees_animation (
+ code_animation, date_anim, duree_anim, animateurs, prepa_deplacemt, temps_total,
+ code_groupe, precisions_type_de_groupe, commune_provenance, nom_etablissement,
+ nbre_pers_total, nbre_pers_enfants, nbre_pers_adultes,
+ lieux, precisions_lieux_anim, accueil_pin, theme_detaille,
+ code_type_anim, payant, type_annul, remarques,
+ type_dossier_ens, realise_par_tiers, detail_tiers
+ ) VALUES (
+ %(code_animation)s, %(date_anim)s, %(duree_anim)s, %(animateurs)s, %(prepa_deplacemt)s, %(temps_total)s,
+ %(code_groupe)s, %(precisions_type_de_groupe)s, %(commune_provenance)s, %(nom_etablissement)s,
+ %(nbre_pers_total)s, %(nbre_pers_enfants)s, %(nbre_pers_adultes)s,
+ %(lieux)s, %(precisions_lieux_anim)s, %(accueil_pin)s, %(theme_detaille)s,
+ %(code_type_anim)s, %(payant)s, %(type_annul)s, %(remarques)s,
+ %(type_dossier_ens)s, %(realise_par_tiers)s, %(detail_tiers)s
+ )
+ """
+ self.execute(sql, data)
+
+ def update_donnees_animation(self, code_animation, data: dict):
+ sql = """
+ UPDATE animation.donnees_animation SET
+ date_anim=%(date_anim)s, duree_anim=%(duree_anim)s, animateurs=%(animateurs)s,
+ prepa_deplacemt=%(prepa_deplacemt)s, temps_total=%(temps_total)s,
+ code_groupe=%(code_groupe)s, precisions_type_de_groupe=%(precisions_type_de_groupe)s,
+ commune_provenance=%(commune_provenance)s, nom_etablissement=%(nom_etablissement)s,
+ nbre_pers_total=%(nbre_pers_total)s, nbre_pers_enfants=%(nbre_pers_enfants)s,
+ nbre_pers_adultes=%(nbre_pers_adultes)s,
+ lieux=%(lieux)s, precisions_lieux_anim=%(precisions_lieux_anim)s,
+ accueil_pin=%(accueil_pin)s, theme_detaille=%(theme_detaille)s,
+ code_type_anim=%(code_type_anim)s, payant=%(payant)s, type_annul=%(type_annul)s,
+ remarques=%(remarques)s, type_dossier_ens=%(type_dossier_ens)s,
+ realise_par_tiers=%(realise_par_tiers)s, detail_tiers=%(detail_tiers)s
+ WHERE code_animation=%(code_animation)s
+ """
+ data['code_animation'] = code_animation
+ self.execute(sql, data)
diff --git a/dialog_add.py b/dialog_add.py
new file mode 100644
index 0000000..5bd3a21
--- /dev/null
+++ b/dialog_add.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from qgis.PyQt.QtWidgets import (
+ QDialog, QVBoxLayout, QFormLayout, QHBoxLayout,
+ QLineEdit, QCheckBox, QPushButton
+)
+
+
+class AddEtablissementDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Ajouter un établissement")
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+
+ form = QFormLayout()
+ self.nom_edit = QLineEdit()
+ self.nom_edit.setPlaceholderText("Nom de l'établissement")
+ self.commune_edit = QLineEdit()
+ self.commune_edit.setPlaceholderText("Commune")
+ self.prive_check = QCheckBox("Établissement privé")
+
+ form.addRow("Nom* :", self.nom_edit)
+ form.addRow("Commune :", self.commune_edit)
+ form.addRow("", self.prive_check)
+ layout.addLayout(form)
+
+ btns = QHBoxLayout()
+ ok = QPushButton("Ajouter")
+ cancel = QPushButton("Annuler")
+ btns.addWidget(ok)
+ btns.addWidget(cancel)
+ layout.addLayout(btns)
+
+ ok.clicked.connect(self.accept)
+ cancel.clicked.connect(self.reject)
+
+ def values(self):
+ return (
+ self.nom_edit.text().strip(),
+ self.prive_check.isChecked(),
+ self.commune_edit.text().strip() or None,
+ )
diff --git a/dialog_auth.py b/dialog_auth.py
new file mode 100644
index 0000000..00753fc
--- /dev/null
+++ b/dialog_auth.py
@@ -0,0 +1,257 @@
+from qgis.PyQt.QtWidgets import (
+ QDialog,
+ QVBoxLayout,
+ QHBoxLayout,
+ QFormLayout,
+ QLabel,
+ QLineEdit,
+ QPushButton,
+ QMessageBox,
+ QSpinBox,
+ QComboBox,
+ QWidget,
+)
+from qgis.PyQt.QtCore import Qt
+from qgis.core import QgsApplication, QgsAuthMethodConfig, QgsSettings
+from .db_manager import DBManager
+
+SETTINGS_SELECTED_CONNECTION = "000"
+
+
+def try_connect_from_keychain():
+ """
+ Tente de se connecter via une connexion PostGIS déjà enregistrée dans QGIS
+ (et potentiellement associée à une config d'authentification 'authcfg').
+ Retourne un DBManager connecté ou None.
+ """
+ try:
+ settings = QgsSettings()
+ connection_name = settings.value(SETTINGS_SELECTED_CONNECTION, "", type=str).strip()
+ if not connection_name:
+ return None
+
+ params = read_postgis_connection(connection_name)
+ if not params:
+ return None
+
+ return connect_from_params(params)
+ except Exception as e:
+ print(f"Keychain connection failed: {e}")
+ return None
+
+
+def parse_uri(uri_string):
+ """Parse QGIS URI format: host=value port=value dbname=value..."""
+ params = {}
+ # Remplacer les espaces de séparation par du split plus robuste
+ parts = uri_string.split()
+ for part in parts:
+ if '=' in part:
+ key, value = part.split('=', 1)
+ params[key.strip()] = value.strip("'\"")
+ return params
+
+
+def list_postgis_connections():
+ """Retourne les noms des connexions PostgreSQL enregistrées dans QGIS."""
+ settings = QgsSettings()
+ settings.beginGroup("PostgreSQL/connections")
+ names = settings.childGroups()
+ settings.endGroup()
+ return sorted(names)
+
+
+def read_postgis_connection(name):
+ """
+ Lit une connexion PostGIS depuis les settings QGIS.
+ Retourne un dict avec host/port/dbname/user/password/authcfg (password peut être vide).
+ """
+ settings = QgsSettings()
+ base = f"PostgreSQL/connections/{name}"
+ host = settings.value(f"{base}/host", "", type=str)
+ port = settings.value(f"{base}/port", "5432", type=str)
+ dbname = settings.value(f"{base}/database", "", type=str)
+ user = settings.value(f"{base}/username", "", type=str)
+ password = settings.value(f"{base}/password", "", type=str)
+ authcfg = settings.value(f"{base}/authcfg", "", type=str)
+
+ if not host and not dbname:
+ return None
+
+ return {
+ "name": name,
+ "host": host or "localhost",
+ "port": int(port) if str(port).strip() else 5432,
+ "dbname": dbname,
+ "user": user,
+ "password": password,
+ "authcfg": authcfg,
+ }
+
+
+def load_auth_config(authcfg):
+ """Charge une config d'auth QGIS et retourne un dict utile."""
+ if not authcfg:
+ return None
+ auth_mgr = QgsApplication.authManager()
+ cfg = QgsAuthMethodConfig()
+ auth_mgr.loadAuthenticationConfig(authcfg, cfg, True)
+ config_map = cfg.configMap() or {}
+ uri = cfg.uri() or ""
+ return {
+ "id": cfg.id(),
+ "name": cfg.name(),
+ "method": cfg.method(),
+ "config_map": config_map,
+ "uri": uri,
+ }
+
+
+def connect_from_params(params):
+ """Construit un DBManager à partir des paramètres + authcfg si présent."""
+ authcfg = (params.get("authcfg") or "").strip()
+ user = params.get("user") or ""
+ password = params.get("password") or ""
+
+ if authcfg:
+ auth = load_auth_config(authcfg)
+ if auth:
+ # Pour les configs de type Basic, on trouve souvent username/password dans configMap
+ user = user or auth["config_map"].get("username") or auth["config_map"].get("user") or ""
+ password = password or auth["config_map"].get("password") or ""
+ if not password and auth.get("uri"):
+ uri_params = parse_uri(auth["uri"])
+ user = user or uri_params.get("user", "")
+ password = password or uri_params.get("password", "")
+
+ if not params.get("dbname"):
+ raise ValueError("Base de données manquante dans la connexion PostGIS QGIS.")
+ if not user:
+ raise ValueError("Utilisateur manquant (connexion QGIS + authcfg n'ont pas fourni de user).")
+ if not password:
+ raise ValueError("Mot de passe manquant (connexion QGIS + authcfg n'ont pas fourni de password).")
+
+ return DBManager(
+ host=params.get("host") or "localhost",
+ port=int(params.get("port") or 5432),
+ dbname=params.get("dbname") or "",
+ user=user,
+ password=password,
+ )
+
+class AuthDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Connexion à la base de données – Animation")
+ self.setMinimumWidth(420)
+ self.db = None
+ self._build_ui()
+
+ def _build_ui(self):
+ layout = QVBoxLayout(self)
+
+ title = QLabel("
Animation – Connexion BDD
")
+ title.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title)
+
+ info = QLabel(
+ "Sélectionnez une connexion PostgreSQL déjà enregistrée dans QGIS "
+ "(idéalement avec une configuration d’authentification associée)."
+ )
+ info.setWordWrap(True)
+ layout.addWidget(info)
+
+ form = QFormLayout()
+ self.connection_combo = QComboBox()
+ self.refresh_button = QPushButton("Actualiser")
+ refresh_layout = QHBoxLayout()
+ refresh_layout.addWidget(self.connection_combo, 1)
+ refresh_layout.addWidget(self.refresh_button)
+ refresh_widget = QWidget()
+ refresh_widget.setLayout(refresh_layout)
+ form.addRow("Connexion PostGIS :", refresh_widget)
+
+ self.manual_host_edit = QLineEdit()
+ self.manual_port_spin = QSpinBox()
+ self.manual_port_spin.setRange(1, 65535)
+ self.manual_port_spin.setValue(5432)
+ self.manual_db_edit = QLineEdit()
+ self.manual_user_edit = QLineEdit()
+ self.manual_password_edit = QLineEdit()
+ self.manual_password_edit.setEchoMode(QLineEdit.Password)
+
+ form.addRow(QLabel("Fallback manuel (si la connexion QGIS ne contient pas le secret)"), QLabel(""))
+ form.addRow("Hôte :", self.manual_host_edit)
+ form.addRow("Port :", self.manual_port_spin)
+ form.addRow("Base de données :", self.manual_db_edit)
+ form.addRow("Utilisateur :", self.manual_user_edit)
+ form.addRow("Mot de passe :", self.manual_password_edit)
+
+ layout.addLayout(form)
+
+ btn_layout = QHBoxLayout()
+ self.btn_cancel = QPushButton("Annuler")
+ self.btn_cancel.clicked.connect(self.reject)
+ self.btn_connect = QPushButton("Se connecter")
+ self.btn_connect.setDefault(True)
+ self.btn_connect.clicked.connect(self.connect)
+ btn_layout.addWidget(self.btn_cancel)
+ btn_layout.addWidget(self.btn_connect)
+ layout.addLayout(btn_layout)
+
+ self.refresh_button.clicked.connect(self.refresh_connections)
+ self.connection_combo.currentIndexChanged.connect(self._on_connection_changed)
+ self.refresh_connections()
+
+ def connect(self):
+ try:
+ params = self._params_from_selection()
+ if params:
+ self.db = connect_from_params(params)
+ QgsSettings().setValue(SETTINGS_SELECTED_CONNECTION, params.get("name", ""))
+ self.accept()
+ return
+
+ # fallback manuel
+ self.db = DBManager(
+ host=self.manual_host_edit.text().strip() or "localhost",
+ port=self.manual_port_spin.value(),
+ dbname=self.manual_db_edit.text().strip(),
+ user=self.manual_user_edit.text().strip(),
+ password=self.manual_password_edit.text(),
+ )
+ self.accept()
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur de connexion", str(e))
+
+ def refresh_connections(self):
+ self.connection_combo.clear()
+ self.connection_combo.addItem("", "")
+ for name in list_postgis_connections():
+ self.connection_combo.addItem(name, name)
+
+ # Pré-sélection du dernier choix
+ last = QgsSettings().value(SETTINGS_SELECTED_CONNECTION, "", type=str)
+ if last:
+ idx = self.connection_combo.findData(last)
+ if idx >= 0:
+ self.connection_combo.setCurrentIndex(idx)
+ else:
+ self._on_connection_changed()
+
+ def _on_connection_changed(self):
+ params = self._params_from_selection()
+ if not params:
+ return
+ # Pré-remplir le fallback manuel avec les valeurs non sensibles
+ self.manual_host_edit.setText(params.get("host", "") or "")
+ self.manual_port_spin.setValue(int(params.get("port") or 5432))
+ self.manual_db_edit.setText(params.get("dbname", "") or "")
+ self.manual_user_edit.setText(params.get("user", "") or "")
+ self.manual_password_edit.clear()
+
+ def _params_from_selection(self):
+ name = (self.connection_combo.currentData() or "").strip()
+ if not name:
+ return None
+ return read_postgis_connection(name)
diff --git a/dialog_saisie.py b/dialog_saisie.py
new file mode 100644
index 0000000..c5c2d51
--- /dev/null
+++ b/dialog_saisie.py
@@ -0,0 +1,718 @@
+import json
+from datetime import date
+
+from qgis.PyQt.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
+ QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox,
+ QDateEdit, QSpinBox, QCheckBox, QScrollArea, QWidget,
+ QListWidget, QListWidgetItem, QInputDialog, QSizePolicy,
+ QFrame, QGroupBox
+)
+from qgis.PyQt.QtCore import Qt, QDate
+from qgis.PyQt.QtGui import QFont, QColor
+from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject
+from qgis.utils import iface
+
+from .map_point_tool import PointCaptureTool
+
+
+# ---------------------------------------------------------------------------
+# Widgets utilitaires
+# ---------------------------------------------------------------------------
+
+class ComboWithAdd(QWidget):
+ def __init__(self, add_callback, parent=None):
+ super().__init__(parent)
+ self._add_callback = add_callback
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.combo = QComboBox()
+ self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.btn_add = QPushButton("+")
+ self.btn_add.setFixedWidth(30)
+ self.btn_add.setToolTip("Ajouter un nouvel élément")
+ self.btn_add.clicked.connect(lambda: self._add_callback(self))
+ layout.addWidget(self.combo)
+ layout.addWidget(self.btn_add)
+
+ def current_data(self):
+ return self.combo.currentData()
+
+ def current_text(self):
+ return self.combo.currentText()
+
+ def populate(self, items, placeholder=None):
+ self.combo.clear()
+ if placeholder:
+ self.combo.addItem(placeholder, None)
+ for code, label in items:
+ self.combo.addItem(label, code)
+
+ def add_item(self, code, label):
+ self.combo.addItem(label, code)
+ self.combo.setCurrentIndex(self.combo.count() - 1)
+
+ def set_by_text(self, text):
+ idx = self.combo.findText(text)
+ if idx >= 0:
+ self.combo.setCurrentIndex(idx)
+
+ def set_by_data(self, data):
+ idx = self.combo.findData(data)
+ if idx >= 0:
+ self.combo.setCurrentIndex(idx)
+
+
+class AnimateursWidget(QWidget):
+ def __init__(self, db, parent=None):
+ super().__init__(parent)
+ self.db = db
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ self.list_widget = QListWidget()
+ self.list_widget.setMaximumHeight(90)
+ layout.addWidget(self.list_widget)
+
+ btn_layout = QHBoxLayout()
+ self.combo = QComboBox()
+ self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ btn_add = QPushButton("Ajouter")
+ btn_add.clicked.connect(self._add_existing)
+ btn_new = QPushButton("+ Nouveau")
+ btn_new.clicked.connect(self._add_new)
+ btn_remove = QPushButton("Retirer")
+ btn_remove.clicked.connect(self._remove_selected)
+ btn_layout.addWidget(self.combo)
+ btn_layout.addWidget(btn_add)
+ btn_layout.addWidget(btn_new)
+ btn_layout.addWidget(btn_remove)
+ layout.addLayout(btn_layout)
+
+ self.load_animateurs()
+
+ def load_animateurs(self):
+ self.combo.clear()
+ for code, nom in self.db.get_animateurs():
+ self.combo.addItem(nom, code)
+
+ def _add_existing(self):
+ code = self.combo.currentData()
+ nom = self.combo.currentText()
+ if code is None:
+ return
+ for i in range(self.list_widget.count()):
+ if self.list_widget.item(i).data(Qt.UserRole) == code:
+ return
+ item = QListWidgetItem(nom)
+ item.setData(Qt.UserRole, code)
+ self.list_widget.addItem(item)
+
+ def _add_new(self):
+ nom, ok = QInputDialog.getText(self, "Nouvel animateur", "Nom :")
+ if ok and nom.strip():
+ try:
+ code, label = self.db.add_animateur(nom.strip())
+ self.combo.addItem(label, code)
+ item = QListWidgetItem(label)
+ item.setData(Qt.UserRole, code)
+ self.list_widget.addItem(item)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _remove_selected(self):
+ for item in self.list_widget.selectedItems():
+ self.list_widget.takeItem(self.list_widget.row(item))
+
+ def get_value(self):
+ result = []
+ for i in range(self.list_widget.count()):
+ item = self.list_widget.item(i)
+ result.append({"code": item.data(Qt.UserRole), "nom": item.text()})
+ return json.dumps(result, ensure_ascii=False) if result else None
+
+ def set_value(self, json_str):
+ self.list_widget.clear()
+ if not json_str:
+ return
+ try:
+ data = json.loads(json_str) if isinstance(json_str, str) else json_str
+ for entry in data:
+ item = QListWidgetItem(entry.get("nom", ""))
+ item.setData(Qt.UserRole, entry.get("code"))
+ self.list_widget.addItem(item)
+ except Exception:
+ pass
+
+
+# ---------------------------------------------------------------------------
+# Dialogue principal
+# ---------------------------------------------------------------------------
+
+class SaisieDialog(QDialog):
+ def __init__(self, db, parent=None):
+ super().__init__(parent)
+ self.db = db
+ self._etab_map = {} # code -> (nom, commune)
+ self._edit_mode = False # True = on édite une animation existante
+ self._edit_code = None # code_animation en cours d'édition
+ self._pending_geom = None # (x, y, crs) en attente pour ajout lieu/etab
+ self._pending_geom_type = None # 'lieu' ou 'etab'
+ self._point_tool = None
+
+ self.setWindowTitle("Animation – Saisie / Édition")
+ self.setMinimumWidth(720)
+ self.setMinimumHeight(820)
+ self._build_ui()
+ self._load_reference_data()
+
+ # -----------------------------------------------------------------------
+ # Construction UI
+ # -----------------------------------------------------------------------
+
+ def _build_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setSpacing(6)
+
+ # --- Bandeau titre ---
+ self.title_label = QLabel("Nouvelle animation
")
+ self.title_label.setAlignment(Qt.AlignCenter)
+ main_layout.addWidget(self.title_label)
+
+ # --- Sélecteur d'animation existante ---
+ grp_sel = QGroupBox("Charger / modifier une animation existante")
+ sel_layout = QHBoxLayout(grp_sel)
+ self.anim_combo = QComboBox()
+ self.anim_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.anim_combo.setPlaceholderText("── Nouvelle saisie ──")
+ self.anim_combo.addItem("── Nouvelle saisie ──", None)
+ self.anim_combo.currentIndexChanged.connect(self._on_anim_selected)
+ sel_layout.addWidget(QLabel("Animation :"))
+ sel_layout.addWidget(self.anim_combo)
+ btn_refresh = QPushButton("🔄")
+ btn_refresh.setFixedWidth(32)
+ btn_refresh.setToolTip("Rafraîchir la liste")
+ btn_refresh.clicked.connect(self._reload_animations_list)
+ sel_layout.addWidget(btn_refresh)
+ main_layout.addWidget(grp_sel)
+
+ # --- Zone scrollable du formulaire ---
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ container = QWidget()
+ self.fl = QFormLayout(container)
+ self.fl.setLabelAlignment(Qt.AlignRight)
+ self.fl.setSpacing(8)
+ scroll.setWidget(container)
+ main_layout.addWidget(scroll)
+
+ fl = self.fl
+
+ # ---- Date & Durée ----
+ self._section(fl, "Date et durée")
+
+ self.date_edit = QDateEdit(QDate.currentDate())
+ self.date_edit.setCalendarPopup(True)
+ self.date_edit.setDisplayFormat("dd/MM/yyyy")
+ fl.addRow("Date *:", self.date_edit)
+
+ dur_w = QWidget()
+ dur_l = QHBoxLayout(dur_w)
+ dur_l.setContentsMargins(0, 0, 0, 0)
+ self.duree_h = QSpinBox()
+ self.duree_h.setRange(0, 24)
+ self.duree_h.setSuffix(" h")
+ self.duree_min = QSpinBox()
+ self.duree_min.setRange(0, 59)
+ self.duree_min.setSuffix(" min")
+ dur_l.addWidget(self.duree_h)
+ dur_l.addWidget(self.duree_min)
+ dur_l.addStretch()
+ fl.addRow("Durée :", dur_w)
+
+ self.prepa_spin = QSpinBox()
+ self.prepa_spin.setRange(0, 9999)
+ self.prepa_spin.setSuffix(" min")
+ for w in (self.duree_h, self.duree_min, self.prepa_spin):
+ w.valueChanged.connect(self._update_temps_total)
+ fl.addRow("Prépa / déplacement :", self.prepa_spin)
+
+ self.temps_total_label = QLabel("0 min")
+ fl.addRow("Temps total (calculé) :", self.temps_total_label)
+
+ # ---- Animateurs ----
+ self._sep(fl)
+ self._section(fl, "Animateurs")
+ self.animateurs_widget = AnimateursWidget(self.db)
+ fl.addRow("Animateurs :", self.animateurs_widget)
+
+ # ---- Groupe ----
+ self._sep(fl)
+ self._section(fl, "Groupe")
+ self.type_groupe_combo = ComboWithAdd(self._add_type_groupe)
+ fl.addRow("Type de groupe :", self.type_groupe_combo)
+ self.precisions_groupe_edit = QLineEdit()
+ fl.addRow("Précisions groupe :", self.precisions_groupe_edit)
+
+ # ---- Établissement ----
+ self._sep(fl)
+ self._section(fl, "Établissement / Provenance")
+ self.etab_combo = ComboWithAdd(self._add_etablissement)
+ self.etab_combo.combo.currentIndexChanged.connect(self._on_etab_changed)
+ fl.addRow("Établissement :", self.etab_combo)
+ self.commune_label = QLabel("")
+ self.commune_label.setStyleSheet("color:#555; font-style:italic;")
+ fl.addRow("Commune (auto) :", self.commune_label)
+
+ # ---- Participants ----
+ self._sep(fl)
+ self._section(fl, "Participants")
+ self.enfants_spin = QSpinBox()
+ self.enfants_spin.setRange(0, 9999)
+ self.enfants_spin.valueChanged.connect(self._update_total_pers)
+ fl.addRow("Enfants :", self.enfants_spin)
+ self.adultes_spin = QSpinBox()
+ self.adultes_spin.setRange(0, 9999)
+ self.adultes_spin.valueChanged.connect(self._update_total_pers)
+ fl.addRow("Adultes :", self.adultes_spin)
+ self.total_pers_label = QLabel("0")
+ fl.addRow("Total (calculé) :", self.total_pers_label)
+
+ # ---- Lieu ----
+ self._sep(fl)
+ self._section(fl, "Lieu")
+ self.lieux_combo = ComboWithAdd(self._add_lieu)
+ fl.addRow("Lieu :", self.lieux_combo)
+ self.precisions_lieux_edit = QLineEdit()
+ fl.addRow("Précisions lieu :", self.precisions_lieux_edit)
+ self.accueil_pin_check = QCheckBox("Oui")
+ fl.addRow("Accueil PIN :", self.accueil_pin_check)
+
+ # ---- Thème ----
+ self._sep(fl)
+ self._section(fl, "Thème")
+ self.theme_combo = ComboWithAdd(self._add_theme)
+ fl.addRow("Thème :", self.theme_combo)
+ self.theme_detaille_edit = QLineEdit()
+ fl.addRow("Thème détaillé :", self.theme_detaille_edit)
+
+ # ---- Statuts ----
+ self._sep(fl)
+ self._section(fl, "Statuts")
+ self.payant_check = QCheckBox("Oui")
+ fl.addRow("Payant :", self.payant_check)
+ self.annul_combo = ComboWithAdd(self._add_annulation)
+ fl.addRow("Annulation :", self.annul_combo)
+ self.dossier_combo = ComboWithAdd(self._add_dossier)
+ fl.addRow("Dossier enseignant :", self.dossier_combo)
+ self.tiers_check = QCheckBox("Oui")
+ self.tiers_check.stateChanged.connect(self._on_tiers_changed)
+ fl.addRow("Réalisé par tiers :", self.tiers_check)
+ self.detail_tiers_edit = QLineEdit()
+ self.detail_tiers_edit.setEnabled(False)
+ fl.addRow("Détail tiers :", self.detail_tiers_edit)
+
+ # ---- Remarques ----
+ self._sep(fl)
+ self.remarques_edit = QLineEdit()
+ fl.addRow("Remarques :", self.remarques_edit)
+
+ # --- Boutons d'action ---
+ btn_layout = QHBoxLayout()
+ btn_cancel = QPushButton("Fermer")
+ btn_cancel.clicked.connect(self.reject)
+
+ self.btn_new_form = QPushButton("✚ Nouveau")
+ self.btn_new_form.setToolTip("Réinitialiser pour une nouvelle saisie")
+ self.btn_new_form.clicked.connect(self._new_form)
+
+ self.btn_save_new = QPushButton("Enregistrer et nouveau")
+ self.btn_save_new.clicked.connect(self._save_and_new)
+
+ self.btn_save = QPushButton("Enregistrer")
+ self.btn_save.setDefault(True)
+ self.btn_save.clicked.connect(self._save)
+
+ btn_layout.addWidget(btn_cancel)
+ btn_layout.addWidget(self.btn_new_form)
+ btn_layout.addStretch()
+ btn_layout.addWidget(self.btn_save_new)
+ btn_layout.addWidget(self.btn_save)
+ main_layout.addLayout(btn_layout)
+
+ def _section(self, fl, title):
+ lbl = QLabel(f"{title}")
+ fl.addRow(lbl)
+
+ def _sep(self, fl):
+ line = QFrame()
+ line.setFrameShape(QFrame.HLine)
+ line.setStyleSheet("color:#ccc;")
+ fl.addRow(line)
+
+ # -----------------------------------------------------------------------
+ # Chargement données de référence
+ # -----------------------------------------------------------------------
+
+ def _load_reference_data(self):
+ try:
+ self.type_groupe_combo.populate(self.db.get_types_groupe(), "-- Sélectionner --")
+ etabs = self.db.get_etablissements()
+ self._etab_map = {c: (n, com) for c, n, com in etabs}
+ self.etab_combo.populate(
+ [(c, f"{n} ({com})" if com else n) for c, n, com in etabs],
+ "-- Sélectionner --"
+ )
+ self.lieux_combo.populate(self.db.get_lieux(), "-- Sélectionner --")
+ self.theme_combo.populate(self.db.get_themes(), "-- Sélectionner --")
+ self.annul_combo.populate(self.db.get_codes_annulation(), "Non")
+ self._set_default(self.annul_combo, "Non")
+ self.dossier_combo.populate(self.db.get_type_dossier_ens(), "Non")
+ self._set_default(self.dossier_combo, "Non")
+ self._reload_animations_list()
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur chargement", str(e))
+
+ def _set_default(self, combo_widget, text):
+ idx = combo_widget.combo.findText(text)
+ if idx >= 0:
+ combo_widget.combo.setCurrentIndex(idx)
+
+ def _reload_animations_list(self):
+ self.anim_combo.blockSignals(True)
+ self.anim_combo.clear()
+ self.anim_combo.addItem("── Nouvelle saisie ──", None)
+ try:
+ rows = self.db.get_all_animations()
+ for code, date_anim, lieux, theme in rows:
+ date_str = date_anim.strftime("%d/%m/%Y") if date_anim else "?"
+ lieux_str = lieux or "Lieu ?"
+ theme_str = theme or "Thème ?"
+ label = f"{date_str} | {lieux_str} | {theme_str} [#{code}]"
+ self.anim_combo.addItem(label, code)
+ except Exception as e:
+ QMessageBox.warning(self, "Erreur", f"Impossible de charger les animations : {e}")
+ self.anim_combo.blockSignals(False)
+
+ # -----------------------------------------------------------------------
+ # Événements
+ # -----------------------------------------------------------------------
+
+ def _on_anim_selected(self, idx):
+ code = self.anim_combo.itemData(idx)
+ if code is None:
+ self._new_form()
+ return
+ try:
+ data = self.db.get_animation_by_id(code)
+ if data:
+ self._edit_mode = True
+ self._edit_code = code
+ self.title_label.setText(f"Édition animation #{code}
")
+ self.btn_save.setText("Mettre à jour")
+ self.btn_save_new.setVisible(False)
+ self._populate_form(data)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _populate_form(self, data):
+ """Remplit le formulaire avec les données d'une animation existante."""
+ if data.get('date_anim'):
+ d = data['date_anim']
+ self.date_edit.setDate(QDate(d.year, d.month, d.day))
+
+ duree = data.get('duree_anim', '0h00') or '0h00'
+ try:
+ if 'h' in duree:
+ h, m = duree.split('h')
+ self.duree_h.setValue(int(h))
+ self.duree_min.setValue(int(m) if m else 0)
+ except Exception:
+ pass
+
+ self.prepa_spin.setValue(data.get('prepa_deplacemt') or 0)
+ self.animateurs_widget.set_value(data.get('animateurs'))
+ self.type_groupe_combo.set_by_data(data.get('code_groupe'))
+ self.precisions_groupe_edit.setText(data.get('precisions_type_de_groupe') or '')
+
+ # Établissement : chercher par nom
+ nom_etab = data.get('nom_etablissement') or ''
+ for code, (nom, com) in self._etab_map.items():
+ if nom == nom_etab:
+ self.etab_combo.set_by_data(code)
+ break
+
+ self.enfants_spin.setValue(data.get('nbre_pers_enfants') or 0)
+ self.adultes_spin.setValue(data.get('nbre_pers_adultes') or 0)
+ self.lieux_combo.set_by_text(data.get('lieux') or '')
+ self.precisions_lieux_edit.setText(data.get('precisions_lieux_anim') or '')
+ self.accueil_pin_check.setChecked(bool(data.get('accueil_pin')))
+ self.theme_detaille_edit.setText(data.get('theme_detaille') or '')
+ self.theme_combo.set_by_data(data.get('code_type_anim'))
+ self.payant_check.setChecked(bool(data.get('payant')))
+ self.annul_combo.set_by_text(data.get('type_annul') or 'Non')
+ self.remarques_edit.setText(data.get('remarques') or '')
+ self.dossier_combo.set_by_text(data.get('type_dossier_ens') or 'Non')
+ realise = bool(data.get('realise_par_tiers'))
+ self.tiers_check.setChecked(realise)
+ self.detail_tiers_edit.setText(data.get('detail_tiers') or '')
+ self.detail_tiers_edit.setEnabled(realise)
+
+ def _new_form(self):
+ self._edit_mode = False
+ self._edit_code = None
+ self.title_label.setText("Nouvelle animation
")
+ self.btn_save.setText("Enregistrer")
+ self.btn_save_new.setVisible(True)
+ self.anim_combo.blockSignals(True)
+ self.anim_combo.setCurrentIndex(0)
+ self.anim_combo.blockSignals(False)
+ self._reset_form()
+
+ def _on_etab_changed(self, idx):
+ code = self.etab_combo.combo.itemData(idx)
+ if code and code in self._etab_map:
+ self.commune_label.setText(self._etab_map[code][1] or "")
+ else:
+ self.commune_label.setText("")
+
+ def _on_tiers_changed(self, state):
+ self.detail_tiers_edit.setEnabled(state == Qt.Checked)
+
+ def _update_temps_total(self):
+ total = self.duree_h.value() * 60 + self.duree_min.value() + self.prepa_spin.value()
+ self.temps_total_label.setText(f"{total} min")
+
+ def _update_total_pers(self):
+ self.total_pers_label.setText(str(self.enfants_spin.value() + self.adultes_spin.value()))
+
+ # -----------------------------------------------------------------------
+ # Capture géométrie sur carte
+ # -----------------------------------------------------------------------
+
+ def _start_point_capture(self, geom_type, on_captured_callback):
+ """
+ Lance l'outil de clic sur la carte. geom_type = 'lieu' ou 'etab'.
+ Après le clic, appelle on_captured_callback(wkt, srid).
+ """
+ canvas = iface.mapCanvas()
+ if not canvas:
+ QMessageBox.warning(self, "Erreur", "Impossible d'accéder à la carte QGIS.")
+ return
+
+ prev_tool = canvas.mapTool()
+ tool = PointCaptureTool(canvas, prev_tool)
+
+ def on_point(x, y, crs):
+ # Convertir en WGS84 (4326) si nécessaire
+ wgs84 = QgsCoordinateReferenceSystem("EPSG:4326")
+ if crs != wgs84:
+ transform = QgsCoordinateTransform(crs, wgs84, QgsProject.instance())
+ from qgis.core import QgsPointXY
+ pt = transform.transform(x, y)
+ x, y = pt.x(), pt.y()
+ wkt = f"POINT({x} {y})"
+ on_captured_callback(wkt, 4326)
+ self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
+ self.activateWindow()
+
+ tool.pointCaptured.connect(on_point)
+ self._point_tool = tool
+
+ # Minimiser le dialogue et activer l'outil
+ self.showMinimized()
+ canvas.setMapTool(tool)
+
+ # -----------------------------------------------------------------------
+ # Callbacks ajout nouveaux éléments
+ # -----------------------------------------------------------------------
+
+ def _add_type_groupe(self, widget):
+ QMessageBox.information(self, "Info",
+ "L'ajout d'un type de groupe nécessite un code entier.\n"
+ "Veuillez contacter l'administrateur de la base.")
+
+ def _add_lieu(self, widget):
+ val, ok = QInputDialog.getText(self, "Nouveau lieu", "Nom du lieu :")
+ if not ok or not val.strip():
+ return
+ nom = val.strip()
+
+ reply = QMessageBox.question(self, "Géométrie",
+ "Souhaitez-vous localiser ce lieu sur la carte ?",
+ QMessageBox.Yes | QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ def on_geom(wkt, srid):
+ try:
+ code, label = self.db.add_lieux(nom, geom_wkt=wkt, srid=srid)
+ widget.add_item(code, label)
+ QMessageBox.information(self, "Succès", f"Lieu « {label} » ajouté avec géométrie.")
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+ self._start_point_capture('lieu', on_geom)
+ else:
+ try:
+ code, label = self.db.add_lieux(nom)
+ widget.add_item(code, label)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _add_theme(self, widget):
+ val, ok = QInputDialog.getText(self, "Nouveau thème", "Libellé :")
+ if ok and val.strip():
+ try:
+ code, label = self.db.add_theme(val.strip())
+ widget.add_item(code, label)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _add_annulation(self, widget):
+ val, ok = QInputDialog.getText(self, "Nouveau type d'annulation", "Libellé :")
+ if ok and val.strip():
+ try:
+ code, label = self.db.add_annulation(val.strip())
+ widget.add_item(code, label)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _add_dossier(self, widget):
+ val, ok = QInputDialog.getText(self, "Nouveau type de dossier", "Libellé :")
+ if ok and val.strip():
+ try:
+ code, label = self.db.add_type_dossier(val.strip())
+ widget.add_item(code, label)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _add_etablissement(self, widget):
+ nom, ok = QInputDialog.getText(self, "Nouvel établissement", "Nom :")
+ if not ok or not nom.strip():
+ return
+ commune, ok2 = QInputDialog.getText(self, "Nouvel établissement", "Commune :")
+ if not ok2:
+ return
+ nom = nom.strip()
+ commune = commune.strip()
+
+ reply = QMessageBox.question(self, "Géométrie",
+ "Souhaitez-vous localiser cet établissement sur la carte ?",
+ QMessageBox.Yes | QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ def on_geom(wkt, srid):
+ try:
+ code, label, com = self.db.add_etablissement(nom, commune, geom_wkt=wkt, srid=srid)
+ self._etab_map[code] = (label, com)
+ display = f"{label} ({com})" if com else label
+ widget.add_item(code, display)
+ QMessageBox.information(self, "Succès", f"Établissement « {label} » ajouté avec géométrie.")
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+ self._start_point_capture('etab', on_geom)
+ else:
+ try:
+ code, label, com = self.db.add_etablissement(nom, commune)
+ self._etab_map[code] = (label, com)
+ display = f"{label} ({com})" if com else label
+ widget.add_item(code, display)
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ # -----------------------------------------------------------------------
+ # Collecte, validation, enregistrement
+ # -----------------------------------------------------------------------
+
+ def _collect_data(self):
+ duree_str = f"{self.duree_h.value()}h{self.duree_min.value():02d}"
+ duree_min_total = self.duree_h.value() * 60 + self.duree_min.value()
+ temps_total = duree_min_total + self.prepa_spin.value() or None
+
+ etab_code = self.etab_combo.current_data()
+ nom_etab, commune_prov = (None, None)
+ if etab_code and etab_code in self._etab_map:
+ nom_etab, commune_prov = self._etab_map[etab_code]
+
+ lieux_txt = self.lieux_combo.current_text() if self.lieux_combo.current_data() else None
+ type_annul = self.annul_combo.current_text() if self.annul_combo.current_data() else None
+ type_dossier = self.dossier_combo.current_text() if self.dossier_combo.current_data() else None
+ realise_tiers = self.tiers_check.isChecked()
+
+ return {
+ "date_anim": self.date_edit.date().toPyDate(),
+ "duree_anim": duree_str,
+ "animateurs": self.animateurs_widget.get_value(),
+ "prepa_deplacemt": self.prepa_spin.value() or None,
+ "temps_total": temps_total,
+ "code_groupe": self.type_groupe_combo.current_data(),
+ "precisions_type_de_groupe": self.precisions_groupe_edit.text().strip() or None,
+ "commune_provenance": commune_prov,
+ "nom_etablissement": nom_etab,
+ "nbre_pers_enfants": self.enfants_spin.value() or None,
+ "nbre_pers_adultes": self.adultes_spin.value() or None,
+ "nbre_pers_total": (self.enfants_spin.value() + self.adultes_spin.value()) or None,
+ "lieux": lieux_txt,
+ "precisions_lieux_anim": self.precisions_lieux_edit.text().strip() or None,
+ "accueil_pin": self.accueil_pin_check.isChecked(),
+ "theme_detaille": self.theme_detaille_edit.text().strip() or None,
+ "code_type_anim": self.theme_combo.current_data(),
+ "payant": self.payant_check.isChecked(),
+ "type_annul": type_annul,
+ "remarques": self.remarques_edit.text().strip() or None,
+ "type_dossier_ens": type_dossier,
+ "realise_par_tiers": realise_tiers,
+ "detail_tiers": self.detail_tiers_edit.text().strip() if realise_tiers else None,
+ }
+
+ def _reset_form(self):
+ self.date_edit.setDate(QDate.currentDate())
+ self.duree_h.setValue(0)
+ self.duree_min.setValue(0)
+ self.prepa_spin.setValue(0)
+ self.animateurs_widget.list_widget.clear()
+ self.type_groupe_combo.combo.setCurrentIndex(0)
+ self.precisions_groupe_edit.clear()
+ self.etab_combo.combo.setCurrentIndex(0)
+ self.commune_label.setText("")
+ self.enfants_spin.setValue(0)
+ self.adultes_spin.setValue(0)
+ self.lieux_combo.combo.setCurrentIndex(0)
+ self.precisions_lieux_edit.clear()
+ self.accueil_pin_check.setChecked(False)
+ self.theme_combo.combo.setCurrentIndex(0)
+ self.theme_detaille_edit.clear()
+ self.payant_check.setChecked(False)
+ self.annul_combo.combo.setCurrentIndex(0)
+ self.remarques_edit.clear()
+ self.dossier_combo.combo.setCurrentIndex(0)
+ self.tiers_check.setChecked(False)
+ self.detail_tiers_edit.clear()
+
+ def _save(self):
+ try:
+ data = self._collect_data()
+ if self._edit_mode:
+ self.db.update_donnees_animation(self._edit_code, data)
+ QMessageBox.information(self, "Succès", f"Animation #{self._edit_code} mise à jour.")
+ self._reload_animations_list()
+ else:
+ code = self.db.get_next_code_animation()
+ data['code_animation'] = code
+ self.db.insert_donnees_animation(data)
+ QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.")
+ self._reload_animations_list()
+ self.accept()
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
+
+ def _save_and_new(self):
+ try:
+ data = self._collect_data()
+ code = self.db.get_next_code_animation()
+ data['code_animation'] = code
+ self.db.insert_donnees_animation(data)
+ QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.")
+ self._reload_animations_list()
+ self._new_form()
+ except Exception as e:
+ QMessageBox.critical(self, "Erreur", str(e))
diff --git a/main_plugin.py b/main_plugin.py
new file mode 100644
index 0000000..082680d
--- /dev/null
+++ b/main_plugin.py
@@ -0,0 +1,32 @@
+from qgis.PyQt.QtWidgets import QAction, QMessageBox, QDialog
+from .dialog_auth import AuthDialog, try_connect_from_keychain
+from .dialog_saisie import SaisieDialog
+
+
+class AnimationPlugin:
+ def __init__(self, iface):
+ self.iface = iface
+ self.action = None
+
+ def initGui(self):
+ self.action = QAction("Saisie Animation", self.iface.mainWindow())
+ self.action.triggered.connect(self.run)
+ self.iface.addToolBarIcon(self.action)
+ self.iface.addPluginToMenu("Animation", self.action)
+
+ def unload(self):
+ self.iface.removePluginMenu("Animation", self.action)
+ self.iface.removeToolBarIcon(self.action)
+
+ def run(self):
+ # Tenter la connexion automatique via le coffre-fort QGIS
+ db = try_connect_from_keychain()
+ if db:
+ dlg = SaisieDialog(db, self.iface.mainWindow())
+ dlg.exec_()
+ else:
+ # Fallback : sélection d'une connexion QGIS (ou saisie manuelle)
+ dlg = AuthDialog(self.iface.mainWindow())
+ if dlg.exec_() == QDialog.Accepted and dlg.db:
+ saisie = SaisieDialog(dlg.db, self.iface.mainWindow())
+ saisie.exec_()
diff --git a/map_point_tool.py b/map_point_tool.py
new file mode 100644
index 0000000..ca0efb0
--- /dev/null
+++ b/map_point_tool.py
@@ -0,0 +1,37 @@
+from qgis.gui import QgsMapTool
+from qgis.PyQt.QtCore import pyqtSignal
+from qgis.PyQt.QtGui import QCursor
+from qgis.PyQt.QtCore import Qt
+
+
+class PointCaptureTool(QgsMapTool):
+ """
+ Outil de capture d'un point sur la carte QGIS.
+ Émet pointCaptured(x, y, crs) au clic gauche.
+ Se désactive automatiquement après un clic.
+ """
+ pointCaptured = pyqtSignal(float, float, object)
+
+ def __init__(self, canvas, previous_tool=None):
+ super().__init__(canvas)
+ self.canvas = canvas
+ self.previous_tool = previous_tool
+ self.setCursor(QCursor(Qt.CrossCursor))
+
+ def canvasReleaseEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ point = self.toMapCoordinates(event.pos())
+ crs = self.canvas.mapSettings().destinationCrs()
+ self.pointCaptured.emit(point.x(), point.y(), crs)
+ # Restaurer l'outil précédent
+ if self.previous_tool:
+ self.canvas.setMapTool(self.previous_tool)
+ else:
+ self.canvas.unsetMapTool(self)
+
+ def keyPressEvent(self, event):
+ if event.key() == Qt.Key_Escape:
+ if self.previous_tool:
+ self.canvas.setMapTool(self.previous_tool)
+ else:
+ self.canvas.unsetMapTool(self)
diff --git a/metadata.txt b/metadata.txt
new file mode 100644
index 0000000..1c6b7ac
--- /dev/null
+++ b/metadata.txt
@@ -0,0 +1,8 @@
+[general]
+name=Animation Saisie
+qgisMinimumVersion=3.0
+description=Plugin de saisie des données d'animation
+version=1.0
+author=Animation
+email=contact@animation.fr
+about=Plugin permettant la saisie des données dans la table animation.donnees_animation