first commit
This commit is contained in:
commit
e6c52820cd
227 changed files with 16156 additions and 0 deletions
67
modules/projects/project.py
Normal file
67
modules/projects/project.py
Normal 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"),
|
||||
)
|
||||
98
modules/projects/project_handler.py
Normal file
98
modules/projects/project_handler.py
Normal 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
111
modules/projects/routes.py
Normal 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()
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue