Files
photostation/src/ui/widgets/config_dialog.py
Jules c2c27b1892 Tarification + écran récap avec prix
Config admin :
- Tarification activable avec paliers (ex: 1-5 = 0.50€, 6-10 = 0.40€)
- Fonction calculate_price() utilitaire

Galerie :
- Affiche "X impressions — Y.YY €" quand tarification activée

Écran impression/récap :
- Galerie des photos sélectionnées avec miniatures + quantité (xN)
- Prix total en haut
- Choix du format d'impression
- Bouton IMPRIMER

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:54:10 +01:00

383 lines
14 KiB
Python

"""Dialogue de configuration admin — accessible par appui long sur le titre."""
import json
import os
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLineEdit, QPushButton, QLabel, QComboBox, QScrollArea, QWidget,
QSpinBox, QCheckBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
CONFIG_PATH = os.path.join(
os.path.dirname(__file__), "..", "..", "..", "config", "settings.json"
)
DEFAULT_CONFIG = {
# WiFi hotspot
"wifi_ap_name": "Photostation",
"wifi_ap_password": "photo1234",
# Serveur upload
"server_ip": "0.0.0.0",
"server_port": 8080,
"upload_url": "https://photo.copydev.fr/upload",
# Email IMAP
"email_address": "photo@photostation.local",
"imap_enabled": False,
"imap_server": "",
"imap_port": 993,
"imap_user": "",
"imap_password": "",
"imap_folder": "INBOX",
"imap_interval": 30,
# Formats d'impression (activable + imprimante par format)
"formats": {
"10x15": {"enabled": True, "printer": "", "ratio": 1.5},
"13x18": {"enabled": False, "printer": "", "ratio": 1.385},
"15x20": {"enabled": False, "printer": "", "ratio": 1.333},
},
# Tarification
"pricing": {
"enabled": True,
"currency": "€",
"tiers": [
{"from": 1, "to": 5, "price": 0.50},
{"from": 6, "to": 10, "price": 0.40},
{"from": 11, "to": 999, "price": 0.30},
],
},
}
def load_config():
try:
with open(CONFIG_PATH, "r") as f:
saved = json.load(f)
config = dict(DEFAULT_CONFIG)
config.update(saved)
return config
except (FileNotFoundError, json.JSONDecodeError):
return dict(DEFAULT_CONFIG)
def calculate_price(quantity, config=None):
"""Calcule le prix total pour une quantité donnée selon les paliers."""
if config is None:
config = load_config()
pricing = config.get("pricing", DEFAULT_CONFIG.get("pricing", {}))
if not pricing.get("enabled", False) or quantity <= 0:
return 0.0
tiers = pricing.get("tiers", [])
# Trouver le palier applicable
unit_price = 0.0
for tier in tiers:
if tier["from"] <= quantity <= tier["to"]:
unit_price = tier["price"]
break
if unit_price == 0.0 and tiers:
unit_price = tiers[-1]["price"]
return quantity * unit_price
def save_config(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
class ConfigDialog(QDialog):
"""Panneau de configuration admin — plein écran, responsive."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Configuration — Admin")
self.config = load_config()
self._setup_ui()
def showEvent(self, event):
super().showEvent(event)
if self.parent():
self.setGeometry(self.parent().rect())
self.setStyleSheet("ConfigDialog { background-color: #f0f0f0; }")
def _section_title(self, text):
lbl = QLabel(text)
lbl.setFont(QFont("", 13, QFont.Weight.Bold))
lbl.setStyleSheet("color: #2196F3; padding-top: 5px;")
return lbl
def _label(self, text):
lbl = QLabel(text)
lbl.setFont(QFont("", 12))
lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
return lbl
def _input(self, value, placeholder=""):
inp = QLineEdit(str(value))
inp.setFont(QFont("", 12))
inp.setMinimumHeight(35)
inp.setStyleSheet("""
QLineEdit {
border: 2px solid #ccc; border-radius: 6px;
padding: 5px 10px; background: white;
}
QLineEdit:focus { border-color: #2196F3; }
""")
if placeholder:
inp.setPlaceholderText(placeholder)
return inp
def _password_input(self, value):
inp = self._input(value)
inp.setEchoMode(QLineEdit.EchoMode.Password)
return inp
def _spinbox(self, value, min_val, max_val):
sb = QSpinBox()
sb.setFont(QFont("", 12))
sb.setMinimumHeight(35)
sb.setRange(min_val, max_val)
sb.setValue(value)
return sb
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 10, 20, 10)
layout.setSpacing(8)
title = QLabel("Configuration Photostation")
title.setFont(QFont("", 18, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll.setStyleSheet("QScrollArea { border: none; }")
content = QWidget()
grid = QGridLayout(content)
grid.setSpacing(8)
grid.setColumnStretch(0, 1)
grid.setColumnStretch(1, 2)
row = 0
# ── WiFi ──
grid.addWidget(self._section_title("WiFi (Hotspot)"), row, 0, 1, 2)
row += 1
grid.addWidget(self._label("Nom du réseau :"), row, 0)
self.wifi_name_input = self._input(self.config["wifi_ap_name"])
grid.addWidget(self.wifi_name_input, row, 1)
row += 1
grid.addWidget(self._label("Mot de passe :"), row, 0)
self.wifi_pass_input = self._input(self.config["wifi_ap_password"])
grid.addWidget(self.wifi_pass_input, row, 1)
row += 1
# ── Serveur upload ──
grid.addWidget(self._section_title("Serveur upload (QR Code / WiFi)"), row, 0, 1, 2)
row += 1
grid.addWidget(self._label("IP d'écoute :"), row, 0)
self.server_ip_input = self._input(self.config["server_ip"], "0.0.0.0 = toutes les interfaces")
grid.addWidget(self.server_ip_input, row, 1)
row += 1
grid.addWidget(self._label("Port :"), row, 0)
self.server_port_input = self._spinbox(self.config["server_port"], 80, 65535)
grid.addWidget(self.server_port_input, row, 1)
row += 1
grid.addWidget(self._label("URL publique (QR) :"), row, 0)
self.upload_url_input = self._input(self.config["upload_url"], "https://photo.exemple.fr/upload")
grid.addWidget(self.upload_url_input, row, 1)
row += 1
# ── Email IMAP ──
grid.addWidget(self._section_title("Email (réception IMAP)"), row, 0, 1, 2)
row += 1
grid.addWidget(self._label("Activer :"), row, 0)
self.imap_enabled_cb = QCheckBox("Consulter la boîte mail")
self.imap_enabled_cb.setFont(QFont("", 12))
self.imap_enabled_cb.setChecked(self.config["imap_enabled"])
grid.addWidget(self.imap_enabled_cb, row, 1)
row += 1
grid.addWidget(self._label("Adresse affichée :"), row, 0)
self.email_input = self._input(self.config["email_address"])
grid.addWidget(self.email_input, row, 1)
row += 1
grid.addWidget(self._label("Serveur IMAP :"), row, 0)
self.imap_server_input = self._input(self.config["imap_server"], "imap.gmail.com")
grid.addWidget(self.imap_server_input, row, 1)
row += 1
grid.addWidget(self._label("Port IMAP :"), row, 0)
self.imap_port_input = self._spinbox(self.config["imap_port"], 1, 65535)
grid.addWidget(self.imap_port_input, row, 1)
row += 1
grid.addWidget(self._label("Utilisateur :"), row, 0)
self.imap_user_input = self._input(self.config["imap_user"])
grid.addWidget(self.imap_user_input, row, 1)
row += 1
grid.addWidget(self._label("Mot de passe :"), row, 0)
self.imap_pass_input = self._password_input(self.config["imap_password"])
grid.addWidget(self.imap_pass_input, row, 1)
row += 1
grid.addWidget(self._label("Dossier :"), row, 0)
self.imap_folder_input = self._input(self.config["imap_folder"], "INBOX")
grid.addWidget(self.imap_folder_input, row, 1)
row += 1
grid.addWidget(self._label("Intervalle (sec) :"), row, 0)
self.imap_interval_input = self._spinbox(self.config["imap_interval"], 10, 300)
grid.addWidget(self.imap_interval_input, row, 1)
row += 1
# ── Formats d'impression ──
grid.addWidget(self._section_title("Formats d'impression"), row, 0, 1, 2)
row += 1
formats = self.config.get("formats", DEFAULT_CONFIG["formats"])
self.format_widgets = {}
for fmt_name, fmt_conf in formats.items():
# Checkbox activé
cb = QCheckBox(f"{fmt_name} cm")
cb.setFont(QFont("", 12))
cb.setChecked(fmt_conf.get("enabled", False))
grid.addWidget(cb, row, 0)
# Imprimante associée
printer = self._input(fmt_conf.get("printer", ""), "Imprimante CUPS (vide = défaut)")
grid.addWidget(printer, row, 1)
self.format_widgets[fmt_name] = {"checkbox": cb, "printer": printer}
row += 1
# ── Tarification ──
grid.addWidget(self._section_title("Tarification"), row, 0, 1, 2)
row += 1
pricing = self.config.get("pricing", DEFAULT_CONFIG["pricing"])
grid.addWidget(self._label("Activer :"), row, 0)
self.pricing_enabled_cb = QCheckBox("Afficher les prix")
self.pricing_enabled_cb.setFont(QFont("", 12))
self.pricing_enabled_cb.setChecked(pricing.get("enabled", True))
grid.addWidget(self.pricing_enabled_cb, row, 1)
row += 1
self.tier_widgets = []
tiers = pricing.get("tiers", [])
for i, tier in enumerate(tiers):
lbl = QLabel(f"De {tier['from']} à {tier['to']} :")
lbl.setFont(QFont("", 12))
lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
grid.addWidget(lbl, row, 0)
price_row = QHBoxLayout()
from_sb = self._spinbox(tier["from"], 1, 999)
to_sb = self._spinbox(tier["to"], 1, 999)
price_input = self._input(f"{tier['price']:.2f}", "0.50")
price_input.setMaximumWidth(80)
price_row.addWidget(QLabel("de"))
price_row.addWidget(from_sb)
price_row.addWidget(QLabel("à"))
price_row.addWidget(to_sb)
price_row.addWidget(QLabel("="))
price_row.addWidget(price_input)
price_row.addWidget(QLabel("€/photo"))
tier_container = QWidget()
tier_container.setLayout(price_row)
grid.addWidget(tier_container, row, 1)
self.tier_widgets.append({"from": from_sb, "to": to_sb, "price": price_input})
row += 1
scroll.setWidget(content)
layout.addWidget(scroll, 1)
# Boutons
buttons = QHBoxLayout()
buttons.setSpacing(15)
cancel_btn = QPushButton("Annuler")
cancel_btn.setFont(QFont("", 13))
cancel_btn.setMinimumHeight(45)
cancel_btn.setStyleSheet("""
QPushButton { background-color: #757575; color: white; border-radius: 8px; padding: 8px 25px; }
QPushButton:pressed { background-color: #616161; }
""")
cancel_btn.clicked.connect(self.reject)
buttons.addWidget(cancel_btn)
save_btn = QPushButton("Enregistrer")
save_btn.setFont(QFont("", 13, QFont.Weight.Bold))
save_btn.setMinimumHeight(45)
save_btn.setStyleSheet("""
QPushButton { background-color: #4CAF50; color: white; border-radius: 8px; padding: 8px 25px; }
QPushButton:pressed { background-color: #388E3C; }
""")
save_btn.clicked.connect(self._save)
buttons.addWidget(save_btn)
layout.addLayout(buttons)
def _save(self):
self.config["wifi_ap_name"] = self.wifi_name_input.text()
self.config["wifi_ap_password"] = self.wifi_pass_input.text()
self.config["server_ip"] = self.server_ip_input.text()
self.config["server_port"] = self.server_port_input.value()
self.config["upload_url"] = self.upload_url_input.text()
self.config["email_address"] = self.email_input.text()
self.config["imap_enabled"] = self.imap_enabled_cb.isChecked()
self.config["imap_server"] = self.imap_server_input.text()
self.config["imap_port"] = self.imap_port_input.value()
self.config["imap_user"] = self.imap_user_input.text()
self.config["imap_password"] = self.imap_pass_input.text()
self.config["imap_folder"] = self.imap_folder_input.text()
self.config["imap_interval"] = self.imap_interval_input.value()
# Formats d'impression
formats = self.config.get("formats", dict(DEFAULT_CONFIG["formats"]))
for fmt_name, widgets in self.format_widgets.items():
if fmt_name not in formats:
formats[fmt_name] = {}
formats[fmt_name]["enabled"] = widgets["checkbox"].isChecked()
formats[fmt_name]["printer"] = widgets["printer"].text()
self.config["formats"] = formats
# Tarification
tiers = []
for tw in self.tier_widgets:
try:
price = float(tw["price"].text().replace(",", "."))
except ValueError:
price = 0.0
tiers.append({
"from": tw["from"].value(),
"to": tw["to"].value(),
"price": price,
})
self.config["pricing"] = {
"enabled": self.pricing_enabled_cb.isChecked(),
"currency": "€",
"tiers": tiers,
}
save_config(self.config)
self.accept()
@staticmethod
def open_config(parent=None):
dialog = ConfigDialog(parent)
return dialog.exec() == QDialog.DialogCode.Accepted