405 lines
11 KiB
Python
405 lines
11 KiB
Python
|
|
"""
|
||
|
|
Gestion des tâches - Module modulaire pour Kuby
|
||
|
|
Utilise disnake et un stockage JSON par serveur.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from zoneinfo import ZoneInfo
|
||
|
|
from typing import Optional, Dict, List, Tuple, Any
|
||
|
|
|
||
|
|
# Chemin du fichier de stockage
|
||
|
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
||
|
|
TACHES_FILE = os.path.join(DATA_DIR, "taches.json")
|
||
|
|
|
||
|
|
# Priorités disponibles
|
||
|
|
PRIORITES = ["Haute", "Moyenne", "Basse", "Critique", "Faible"]
|
||
|
|
STATUTS = ["À faire", "En cours", "Terminée", "Annulée"]
|
||
|
|
|
||
|
|
|
||
|
|
def _assurer_dossier():
|
||
|
|
"""Crée le dossier data/ si inexistant."""
|
||
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||
|
|
|
||
|
|
|
||
|
|
def _charger_fichier() -> Dict[str, Any]:
|
||
|
|
"""Charge le fichier taches.json ou retourne une structure vide."""
|
||
|
|
_assurer_dossier()
|
||
|
|
if not os.path.exists(TACHES_FILE):
|
||
|
|
return {"taches": {}, "archive": {}, "settings": {}}
|
||
|
|
try:
|
||
|
|
with open(TACHES_FILE, "r", encoding="utf-8") as f:
|
||
|
|
return json.load(f)
|
||
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
||
|
|
return {"taches": {}, "archive": {}, "settings": {}}
|
||
|
|
|
||
|
|
|
||
|
|
def _sauvegarder_fichier(data: Dict[str, Any]) -> None:
|
||
|
|
"""Sauvegarde les données dans taches.json."""
|
||
|
|
_assurer_dossier()
|
||
|
|
with open(TACHES_FILE, "w", encoding="utf-8") as f:
|
||
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# FONCTIONS DE BASE
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
async def charger_taches() -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Charge toutes les tâches depuis le fichier.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict avec clés: taches, archive, settings
|
||
|
|
"""
|
||
|
|
return _charger_fichier()
|
||
|
|
|
||
|
|
|
||
|
|
async def sauvegarder_taches(data: Optional[Dict[str, Any]] = None) -> None:
|
||
|
|
"""
|
||
|
|
Sauvegarde les tâches dans le fichier.
|
||
|
|
Si data=None, recharge depuis le fichier et sauvegarde (pour rafraîchir).
|
||
|
|
"""
|
||
|
|
if data is None:
|
||
|
|
data = _charger_fichier()
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# GESTION DES TÂCHES
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
async def ajouter_tache(
|
||
|
|
titre: str,
|
||
|
|
description: str,
|
||
|
|
assigne_id: int,
|
||
|
|
priorite: str,
|
||
|
|
echeance: str, # Format: YYYY-MM-DD
|
||
|
|
cree_par_id: int,
|
||
|
|
serveur_id: int
|
||
|
|
) -> str:
|
||
|
|
"""
|
||
|
|
Ajoute une nouvelle tâche.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
titre: Titre de la tâche
|
||
|
|
description: Description détaillée
|
||
|
|
assigne_id: ID Discord de l'utilisateur assigné
|
||
|
|
priorite: Priorité (doit être dans PRIORITES)
|
||
|
|
echeance: Date au format YYYY-MM-DD
|
||
|
|
cree_par_id: ID Discord du créateur
|
||
|
|
serveur_id: ID du serveur (guild)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: L'ID unique de la tâche créée
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
|
||
|
|
# Valider la priorité
|
||
|
|
if priorite not in PRIORITES:
|
||
|
|
priorite = "Moyenne"
|
||
|
|
|
||
|
|
# Générer un ID unique
|
||
|
|
tache_id = str(len(data["taches"]) + 1)
|
||
|
|
|
||
|
|
# Date actuelle
|
||
|
|
now = datetime.now(ZoneInfo("Europe/Paris"))
|
||
|
|
|
||
|
|
data["taches"][tache_id] = {
|
||
|
|
"titre": titre,
|
||
|
|
"description": description,
|
||
|
|
"assigne": assigne_id,
|
||
|
|
"cree_par": cree_par_id,
|
||
|
|
"priorite": priorite,
|
||
|
|
"echeance": echeance,
|
||
|
|
"statut": "À faire",
|
||
|
|
"date_creation": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||
|
|
"date_fin": None,
|
||
|
|
"serveur_id": serveur_id,
|
||
|
|
"archive": False
|
||
|
|
}
|
||
|
|
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
return tache_id
|
||
|
|
|
||
|
|
|
||
|
|
async def lister_taches_utilisateur(user_id: int, serveur_id: int) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Liste les tâches assignées à un utilisateur sur un serveur.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
user_id: ID Discord de l'utilisateur
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict des tâches (non archivées) de l'utilisateur
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
return {
|
||
|
|
tid: tache for tid, tache in data["taches"].items()
|
||
|
|
if tache.get("assigne") == user_id
|
||
|
|
and tache.get("serveur_id") == serveur_id
|
||
|
|
and not tache.get("archive", False)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def lister_toutes_taches(serveur_id: int) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Liste toutes les tâches (non archivées) d'un serveur.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict de toutes les tâches du serveur
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
return {
|
||
|
|
tid: tache for tid, tache in data["taches"].items()
|
||
|
|
if tache.get("serveur_id") == serveur_id
|
||
|
|
and not tache.get("archive", False)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def get_tache(tache_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Récupère une tâche par son ID.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tache_id: ID de la tâche
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict de la tâche ou None si non trouvée
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
return data["taches"].get(tache_id)
|
||
|
|
|
||
|
|
|
||
|
|
async def update_tache_status(tache_id: str, statut: str) -> bool:
|
||
|
|
"""
|
||
|
|
Met à jour le statut d'une tâche.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tache_id: ID de la tâche
|
||
|
|
statut: Nouveau statut
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True si la mise à jour a réussi
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
|
||
|
|
if tache_id not in data["taches"]:
|
||
|
|
return False
|
||
|
|
|
||
|
|
if statut not in STATUTS:
|
||
|
|
statut = "À faire"
|
||
|
|
|
||
|
|
# Mettre à jour le statut
|
||
|
|
data["taches"][tache_id]["statut"] = statut
|
||
|
|
|
||
|
|
# Si terminée, enregistrer la date de fin
|
||
|
|
if statut == "Terminée":
|
||
|
|
now = datetime.now(ZoneInfo("Europe/Paris"))
|
||
|
|
data["taches"][tache_id]["date_fin"] = now.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
|
else:
|
||
|
|
data["taches"][tache_id]["date_fin"] = None
|
||
|
|
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
async def update_tache_priorite(tache_id: str, priorite: str) -> bool:
|
||
|
|
"""
|
||
|
|
Met à jour la priorité d'une tâche.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tache_id: ID de la tâche
|
||
|
|
priorite: Nouvelle priorité
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True si la mise à jour a réussi
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
|
||
|
|
if tache_id not in data["taches"]:
|
||
|
|
return False
|
||
|
|
|
||
|
|
if priorite not in PRIORITES:
|
||
|
|
return False
|
||
|
|
|
||
|
|
data["taches"][tache_id]["priorite"] = priorite
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
async def supprimer_tache(tache_id: str) -> bool:
|
||
|
|
"""
|
||
|
|
Supprime une tâche (la déplace dans l'archive).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tache_id: ID de la tâche
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True si la suppression a réussi
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
|
||
|
|
if tache_id not in data["taches"]:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Archiver la tâche au lieu de la supprimer
|
||
|
|
tache = data["taches"].pop(tache_id)
|
||
|
|
tache["archive"] = True
|
||
|
|
tache["date_archivage"] = datetime.now(ZoneInfo("Europe/Paris")).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
|
|
||
|
|
data["archive"][tache_id] = tache
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# FONCTIONS DE RAPPEL
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
async def taches_proches(serveur_id: int, heures_avant: int = 1) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Récupère les tâches dont l'échéance est dans X heures.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
heures_avant: Nombre d'heures avant l'échéance (défaut: 1)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict des tâches proches de leur échéance
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
now = datetime.now(ZoneInfo("Europe/Paris"))
|
||
|
|
result = {}
|
||
|
|
|
||
|
|
for tid, tache in data["taches"].items():
|
||
|
|
if tache.get("serveur_id") != serveur_id or tache.get("archive", False):
|
||
|
|
continue
|
||
|
|
if tache["statut"] == "Terminée":
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
echeance_date = datetime.strptime(tache["echeance"], "%Y-%m-%d")
|
||
|
|
echeance_date = echeance_date.replace(
|
||
|
|
hour=9, minute=0, second=0, microsecond=0,
|
||
|
|
tzinfo=ZoneInfo("Europe/Paris")
|
||
|
|
)
|
||
|
|
|
||
|
|
# Calculer la différence
|
||
|
|
delta = echeance_date - now
|
||
|
|
|
||
|
|
# Si l'échéance est dans X heures ou moins, et pas déjà passée
|
||
|
|
if timedelta(0) < delta <= timedelta(hours=heures_avant):
|
||
|
|
result[tid] = tache
|
||
|
|
except (ValueError, TypeError):
|
||
|
|
continue
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
async def taches_aujourdhui(serveur_id: int) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Récupère les tâches dont l'échéance est aujourd'hui.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict des tâches d'aujourd'hui
|
||
|
|
"""
|
||
|
|
return await taches_proches(serveur_id, heures_avant=24)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# ARCHIVAGE AUTOMATIQUE
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
async def archiver_taches_anciennes(serveur_id: int, jours: int = 30) -> int:
|
||
|
|
"""
|
||
|
|
Archive les tâches terminées depuis plus de X jours.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
jours: Nombre de jours après lequel archiver (défaut: 30)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
int: Nombre de tâches archivées
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
now = datetime.now(ZoneInfo("Europe/Paris"))
|
||
|
|
count = 0
|
||
|
|
|
||
|
|
to_archive = []
|
||
|
|
for tid, tache in data["taches"].items():
|
||
|
|
if tache.get("serveur_id") != serveur_id:
|
||
|
|
continue
|
||
|
|
if tache.get("statut") != "Terminée":
|
||
|
|
continue
|
||
|
|
if tache.get("archive", False):
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
fin_date = datetime.strptime(tache["date_fin"], "%Y-%m-%d %H:%M:%S")
|
||
|
|
fin_date = fin_date.replace(tzinfo=ZoneInfo("Europe/Paris"))
|
||
|
|
|
||
|
|
if (now - fin_date) > timedelta(days=jours):
|
||
|
|
to_archive.append(tid)
|
||
|
|
except (ValueError, TypeError):
|
||
|
|
continue
|
||
|
|
|
||
|
|
for tid in to_archive:
|
||
|
|
tache = data["taches"].pop(tid)
|
||
|
|
tache["archive"] = True
|
||
|
|
tache["date_archivage"] = now.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
|
data["archive"][tid] = tache
|
||
|
|
count += 1
|
||
|
|
|
||
|
|
if count > 0:
|
||
|
|
_sauvegarder_fichier(data)
|
||
|
|
|
||
|
|
return count
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# CONFIGURATION PAR SERVEUR
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
async def get_taches_settings(serveur_id: int) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Récupère les paramètres des tâches pour un serveur.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dict des paramètres (taches_enabled, auto_archive_days, etc.)
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
return data["settings"].get(str(serveur_id), {
|
||
|
|
"taches_enabled": True,
|
||
|
|
"auto_archive_days": 30,
|
||
|
|
"notify_channel_id": None
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
async def set_taches_settings(serveur_id: int, settings: Dict[str, Any]) -> None:
|
||
|
|
"""
|
||
|
|
Met à jour les paramètres des tâches pour un serveur.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
serveur_id: ID du serveur
|
||
|
|
settings: Dict des paramètres à mettre à jour
|
||
|
|
"""
|
||
|
|
data = _charger_fichier()
|
||
|
|
|
||
|
|
if str(serveur_id) not in data["settings"]:
|
||
|
|
data["settings"][str(serveur_id)] = {}
|
||
|
|
|
||
|
|
data["settings"][str(serveur_id)].update(settings)
|
||
|
|
_sauvegarder_fichier(data)
|