first commit

This commit is contained in:
mrtoine 2025-09-20 13:18:04 +02:00
commit e6c52820cd
227 changed files with 16156 additions and 0 deletions

View file

@ -0,0 +1,67 @@
import re
import uuid
from datetime import datetime
from typing import Optional, Dict, Any
def _slugify(value: str) -> str:
value = value.strip().lower()
value = re.sub(r'[^a-z0-9\-_\s]+', '', value)
value = re.sub(r'[\s]+', '-', value)
return value
class Project:
def __init__(
self,
client_id: str,
name: str,
status: str = 'Nouveau',
start_date: Optional[str] = None,
end_date: Optional[str] = None,
description: str = '',
budget: Optional[float] = None,
id: Optional[str] = None,
created_at: Optional[str] = None,
updated_at: Optional[str] = None
):
self.client_id = client_id
self.name = name
self.status = status
self.start_date = start_date
self.end_date = end_date
self.description = description
self.budget = budget
self.id = id or f"{_slugify(name)}-{uuid.uuid4().hex[:8]}"
now_iso = datetime.utcnow().isoformat()
self.created_at = created_at or now_iso
self.updated_at = updated_at or now_iso
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"client_id": self.client_id,
"name": self.name,
"status": self.status,
"start_date": self.start_date,
"end_date": self.end_date,
"description": self.description,
"budget": self.budget,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
@staticmethod
def from_dict(data: Dict[str, Any]) -> 'Project':
return Project(
client_id=data.get("client_id"),
name=data.get("name", ""),
status=data.get("status", "Nouveau"),
start_date=data.get("start_date"),
end_date=data.get("end_date"),
description=data.get("description", ""),
budget=data.get("budget"),
id=data.get("id"),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
)

View file

@ -0,0 +1,98 @@
import json
import os
from typing import List, Optional
from modules.projects.project import Project
class ProjectHandler:
def __init__(self, base_dir: str = None):
# Par défaut, stocker sous Data/projects
self.base_dir = base_dir or os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'Data', 'projects')
os.makedirs(self.base_dir, exist_ok=True)
def _client_dir(self, client_id: str) -> str:
path = os.path.join(self.base_dir, client_id)
os.makedirs(path, exist_ok=True)
return path
def _project_path(self, client_id: str, project_id: str) -> str:
return os.path.join(self._client_dir(client_id), f"{project_id}.json")
def list_projects(self, client_id: str) -> List[Project]:
projects: List[Project] = []
directory = self._client_dir(client_id)
if not os.path.isdir(directory):
return projects
for filename in os.listdir(directory):
if filename.endswith('.json'):
try:
with open(os.path.join(directory, filename), 'r', encoding='utf-8') as f:
data = json.load(f)
projects.append(Project.from_dict(data))
except Exception:
# Ignorer fichiers corrompus
continue
# Tri par date de mise à jour décroissante
projects.sort(key=lambda p: p.updated_at or "", reverse=True)
return projects
def get_project(self, client_id: str, project_id: str) -> Optional[Project]:
path = self._project_path(client_id, project_id)
if not os.path.exists(path):
return None
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
return Project.from_dict(data)
def add_project(self, project: Project) -> str:
path = self._project_path(project.client_id, project.id)
with open(path, 'w', encoding='utf-8') as f:
json.dump(project.to_dict(), f, ensure_ascii=False, indent=2)
return project.id
def update_project(self, project: Project) -> bool:
path = self._project_path(project.client_id, project.id)
if not os.path.exists(path):
return False
# mettre à jour updated_at
from datetime import datetime
project.updated_at = datetime.utcnow().isoformat()
with open(path, 'w', encoding='utf-8') as f:
json.dump(project.to_dict(), f, ensure_ascii=False, indent=2)
return True
def delete_project(self, client_id: str, project_id: str) -> bool:
path = self._project_path(client_id, project_id)
if os.path.exists(path):
try:
os.remove(path)
return True
except Exception:
return False
return False
def add_task_for_project(self, project_id: str, title: str, due_date: str, description: str = "", priority: str = "normale", metadata: dict = None):
"""
Crée une tâche liée à un projet.
:param project_id: ID du projet
:param title: Titre de la tâche
:param due_date: Date d'échéance au format ISO (YYYY-MM-DD)
:param description: Description optionnelle
:param priority: 'basse' | 'normale' | 'haute'
:param metadata: métadonnées optionnelles
:return: ID de la tâche créée
"""
from modules.tasks.task import Task
from modules.tasks.task_handler import TaskHandler
task = Task(
title=title,
due_date=due_date,
description=description,
entity_type="project",
entity_id=project_id,
priority=priority,
metadata=metadata or {},
)
return TaskHandler().add_task(task)

111
modules/projects/routes.py Normal file
View file

@ -0,0 +1,111 @@
import os
import json
from typing import Dict, List
from flask import Blueprint, render_template, request
from modules.projects.project_handler import ProjectHandler
projects_bp = Blueprint('projects', __name__)
_handler = ProjectHandler()
def _project_root() -> str:
# 3 niveaux au dessus de modules/projects/ = racine projet
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def _clients_dir() -> str:
return os.path.join(_project_root(), 'Data', 'clients')
def _load_clients_index() -> Dict[str, str]:
"""
Retourne un dict {client_id: client_name lisible}
client_id = nom de fichier sans extension dans Data/clients
"""
idx: Dict[str, str] = {}
cdir = _clients_dir()
if not os.path.isdir(cdir):
return idx
for fname in os.listdir(cdir):
if not fname.endswith('.json'):
continue
client_id = os.path.splitext(fname)[0]
path = os.path.join(cdir, fname)
try:
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
name = data.get('client_name') or client_id.replace('_', ' ').title()
idx[client_id] = name
except Exception:
idx[client_id] = client_id.replace('_', ' ').title()
return idx
def _list_all_projects() -> List:
"""
Parcourt Data/projects/<client_id> et retourne la liste des objets Project.
"""
base = _handler.base_dir
projects = []
if os.path.isdir(base):
for client_id in os.listdir(base):
client_path = os.path.join(base, client_id)
if os.path.isdir(client_path):
projects.extend(_handler.list_projects(client_id))
return projects
@projects_bp.route('/projects', methods=['GET'])
def projects_index():
clients_idx = _load_clients_index()
selected_client = request.args.get('client_id', '').strip()
query = request.args.get('q', '').strip().lower()
# Charger tous les projets
projects = _list_all_projects()
# Filtre par client
if selected_client:
projects = [p for p in projects if p.client_id == selected_client]
# Recherche plein texte basique
if query:
def match(p) -> bool:
hay = ' '.join([
p.name or '',
p.status or '',
p.description or ''
]).lower()
return query in hay
projects = [p for p in projects if match(p)]
# Tri par updated_at décroissante
projects.sort(key=lambda p: p.updated_at or '', reverse=True)
# Transformer pour la vue (nom client)
def client_name_for(pid: str) -> str:
return clients_idx.get(pid, pid.replace('_', ' ').title())
view_projects = []
for p in projects:
view_projects.append({
'id': p.id,
'client_id': p.client_id,
'client_name': client_name_for(p.client_id),
'name': p.name,
'status': p.status,
'start_date': p.start_date,
'end_date': p.end_date,
'budget': p.budget,
'updated_at': p.updated_at
})
# Liste triée des clients pour le sélecteur
clients_for_select = sorted(
[{'id': cid, 'name': cname} for cid, cname in clients_idx.items()],
key=lambda c: c['name'].lower()
)
return render_template(
'projects/all_projects.html',
projects=view_projects,
clients=clients_for_select,
selected_client=selected_client,
q=request.args.get('q', '').strip()
)