commit e6c52820cd3cd9a8399f13304c3e8d7489ab2a2f Author: mrtoine Date: Sat Sep 20 13:18:04 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..813a198 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Flask +instance/ +.webassets-cache + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db + +# Local development +*.log +.flask_session/ + +# Specific to the project +output/devis/* +!output/devis/.gitkeep +output/propositions/* +!output/propositions/.gitkeep +output/factures/* +!output/factures/.gitkeep diff --git a/Data/BG.png b/Data/BG.png new file mode 100644 index 0000000..fce9615 Binary files /dev/null and b/Data/BG.png differ diff --git a/Data/email_drafts/ed_b211c2d284.json b/Data/email_drafts/ed_b211c2d284.json new file mode 100644 index 0000000..0fa48b8 --- /dev/null +++ b/Data/email_drafts/ed_b211c2d284.json @@ -0,0 +1,17 @@ +{ + "id": "ed_b211c2d284", + "prospect_id": "pros_532dfd81", + "to_email": "aucun@aucun.com", + "subject": "Proposition de services en développement pour Freelancer", + "content": "

Bonjour Client Freelancer anonyme (côte d'ivoire)

\r\n\r\n

Je m’appelle Anthony et je suis développeur freelance spécialisé dans dans le développement web et logiciel. Je suis actuellement à la recherche de nouvelles collaborations avec des entreprises locales comme la vôtre, afin de les aider à optimiser leurs outils numériques et à développer de nouvelles solutions.

\r\n\r\n

Bien que je débute dans la consultance, j’ai déjà une solide expérience en développement et je suis motivé à apporter des solutions sur mesure à vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d’avance pour votre attention et au plaisir d’échanger avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "status": "sent", + "template_id": "prospect_followup", + "task_id": "tsk_5fa1da17", + "created_at": "2025-08-24T09:26:11.417077", + "sent_at": "2025-08-24T09:26:29.011820", + "error_message": null, + "metadata": { + "generated_for_date": "2025-08-24", + "task_title": "relance client" + } +} \ No newline at end of file diff --git a/Data/email_history/pros_149451b1.json b/Data/email_history/pros_149451b1.json new file mode 100644 index 0000000..72bba41 --- /dev/null +++ b/Data/email_history/pros_149451b1.json @@ -0,0 +1,26 @@ +[ + { + "to": "info@fermederestaumont.be", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme de Restaumont", + "content": "

Bonjour Ferme de Restaumont

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:18.608708" + }, + { + "to": "info@fermederestaumont.be", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme de Restaumont", + "content": "

Bonjour Ferme de Restaumont

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:46.058783" + }, + { + "to": "info@fermederestaumont.be", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme de Restaumont", + "content": "

Bonjour Ferme de Restaumont

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:55.326124" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_2245d2f8.json b/Data/email_history/pros_2245d2f8.json new file mode 100644 index 0000000..061059a --- /dev/null +++ b/Data/email_history/pros_2245d2f8.json @@ -0,0 +1,18 @@ +[ + { + "to": "contact@info-sat.be", + "subject": "Proposition de services en d\u00e9veloppement pour Info Sat", + "content": "

Bonjour Info Sat

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:21.592734" + }, + { + "to": "contact@info-sat.be", + "subject": "Proposition de services en d\u00e9veloppement pour Info Sat", + "content": "

Bonjour Info Sat

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:09.973452" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_245c68bc.json b/Data/email_history/pros_245c68bc.json new file mode 100644 index 0000000..86e7019 --- /dev/null +++ b/Data/email_history/pros_245c68bc.json @@ -0,0 +1,34 @@ +[ + { + "to": "tipidubonheur@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Tipi du bonheur", + "content": "

Bonjour Tipi du bonheur

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Seriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-06-01T16:45:34.553346" + }, + { + "to": "tipidubonheur@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour Tipi du bonheur

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:12.325730" + }, + { + "to": "tipidubonheur@hotmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Tipi du bonheur

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:28.817283" + }, + { + "to": "tipidubonheur@hotmail.com", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Tipi du bonheur

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:19.014299" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_248b289a.json b/Data/email_history/pros_248b289a.json new file mode 100644 index 0000000..da635ad --- /dev/null +++ b/Data/email_history/pros_248b289a.json @@ -0,0 +1,10 @@ +[ + { + "to": "nicole@nisav.be", + "subject": "Proposition de services en d\u00e9veloppement pour NISAV EVENTS", + "content": "

Bonjour Nicole

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:58.631421" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_28680571.json b/Data/email_history/pros_28680571.json new file mode 100644 index 0000000..099978f --- /dev/null +++ b/Data/email_history/pros_28680571.json @@ -0,0 +1,10 @@ +[ + { + "to": "lessaveursdepaca@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Les Saveurs de PACA", + "content": "

Bonjour Saveurs PACA

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:56.380496" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_2968493a.json b/Data/email_history/pros_2968493a.json new file mode 100644 index 0000000..2286354 --- /dev/null +++ b/Data/email_history/pros_2968493a.json @@ -0,0 +1,34 @@ +[ + { + "to": "Beraurelie@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Les cochons d'ici", + "content": "

Bonjour Les cochons d'ici

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T19:31:40.605557" + }, + { + "to": "Beraurelie@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Les cochons d'ici", + "content": "

Bonjour Les cochons d'ici

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:03.520133" + }, + { + "to": "Beraurelie@hotmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Les cochons d'ici

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:21.754284" + }, + { + "to": "Beraurelie@hotmail.com", + "subject": "Relance : Collaboration avec Les cochons d'ici pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Les cochons d'ici

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:11.033721" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_2e687b94.json b/Data/email_history/pros_2e687b94.json new file mode 100644 index 0000000..28c6c9a --- /dev/null +++ b/Data/email_history/pros_2e687b94.json @@ -0,0 +1,26 @@ +[ + { + "to": "oceaneballez13@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Oc\u00e9ane Ballez - Professionnelle de la beaut\u00e9 & bien-\u00eatre ", + "content": "

Bonjour Oc\u00e9ane Ballez

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:12:07.884080" + }, + { + "to": "oceaneballez13@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Oc\u00e9ane Ballez

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:24.826067" + }, + { + "to": "oceaneballez13@gmail.com", + "subject": "Relance : Collaboration avec Oc\u00e9ane Ballez - Professionnelle de la beaut\u00e9 & bien-\u00eatre pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Oc\u00e9ane Ballez

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:14.207804" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_38947d7a.json b/Data/email_history/pros_38947d7a.json new file mode 100644 index 0000000..96337c6 --- /dev/null +++ b/Data/email_history/pros_38947d7a.json @@ -0,0 +1,18 @@ +[ + { + "to": "artechbeton@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Art Tech Beton", + "content": "

Bonjour Art Tech Beton

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:12:06.860917" + }, + { + "to": "artechbeton@gmail.com", + "subject": "Relance : Collaboration avec {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:14:34.979392" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3924c706.json b/Data/email_history/pros_3924c706.json new file mode 100644 index 0000000..d172029 --- /dev/null +++ b/Data/email_history/pros_3924c706.json @@ -0,0 +1,34 @@ +[ + { + "to": "info@fluide.be", + "subject": "Proposition de services en d\u00e9veloppement pour Fluide", + "content": "

Bonjour Fluide

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:23.731388" + }, + { + "to": "info@fluide.be", + "subject": "Proposition de services en d\u00e9veloppement pour Fluide", + "content": "

Bonjour Fluide

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:15.040941" + }, + { + "to": "info@fluide.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Fluide

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:34.551344" + }, + { + "to": "info@fluide.be", + "subject": "Relance : Collaboration avec Fluide pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Fluide

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:23.218066" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_392fbe80.json b/Data/email_history/pros_392fbe80.json new file mode 100644 index 0000000..24045c7 --- /dev/null +++ b/Data/email_history/pros_392fbe80.json @@ -0,0 +1,10 @@ +[ + { + "to": "laboiteajeuxdelolotte@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour La bo\u00eete \u00e0 jeu de lolotte", + "content": "

Bonjour Lolotte

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:54.434790" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3aa69d74.json b/Data/email_history/pros_3aa69d74.json new file mode 100644 index 0000000..7502fae --- /dev/null +++ b/Data/email_history/pros_3aa69d74.json @@ -0,0 +1,34 @@ +[ + { + "to": "Lebleu.elise@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Lebleu Elise", + "content": "

Bonjour Lebleu Elise

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:48.819870" + }, + { + "to": "Lebleu.elise@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme Delporte", + "content": "

Bonjour Lebleu Elise

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:02.495738" + }, + { + "to": "Lebleu.elise@hotmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Lebleu Elise

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:20.722246" + }, + { + "to": "Lebleu.elise@hotmail.com", + "subject": "Relance : Collaboration avec Ferme Delporte pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Lebleu Elise

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:09.598750" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3cebd19d.json b/Data/email_history/pros_3cebd19d.json new file mode 100644 index 0000000..5ad9fe9 --- /dev/null +++ b/Data/email_history/pros_3cebd19d.json @@ -0,0 +1,34 @@ +[ + { + "to": "rcguerrieri@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour TP Studio Plus", + "content": "

Bonjour TP Studio Plus

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Seriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-06-01T16:45:33.333823" + }, + { + "to": "rcguerrieri@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Tempo studio plus", + "content": "

Bonjour TP Studio Plus

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:52.905720" + }, + { + "to": "rcguerrieri@hotmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour TP Studio Plus

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:11.922595" + }, + { + "to": "rcguerrieri@hotmail.com", + "subject": "Relance : Collaboration avec Tempo studio plus pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour TP Studio Plus

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:01.847005" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3dfec9c4.json b/Data/email_history/pros_3dfec9c4.json new file mode 100644 index 0000000..55f264e --- /dev/null +++ b/Data/email_history/pros_3dfec9c4.json @@ -0,0 +1,10 @@ +[ + { + "to": "info@mariage-prelude.com", + "subject": "Proposition de services en d\u00e9veloppement pour Mariage pr\u00e9lude", + "content": "

Bonjour Mariage Pr\u00e9lude

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:53.422049" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3e35a76f.json b/Data/email_history/pros_3e35a76f.json new file mode 100644 index 0000000..4fa8d98 --- /dev/null +++ b/Data/email_history/pros_3e35a76f.json @@ -0,0 +1,34 @@ +[ + { + "to": "brasserieblc@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Brasserie Artisanale BLC", + "content": "

Bonjour Brasserie Artisanale BLC

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:50.868720" + }, + { + "to": "brasserieblc@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour Brasserie Artisanale BLC

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:13.652934" + }, + { + "to": "brasserieblc@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Brasserie Artisanale BLC

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:33.013355" + }, + { + "to": "brasserieblc@gmail.com", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Brasserie Artisanale BLC

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:22.194788" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_3f7db2e1.json b/Data/email_history/pros_3f7db2e1.json new file mode 100644 index 0000000..af9110f --- /dev/null +++ b/Data/email_history/pros_3f7db2e1.json @@ -0,0 +1,34 @@ +[ + { + "to": "scassenes.runners@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour S'Cassennes Runners", + "content": "

Bonjour S'Cassennes Runners

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:16.458539" + }, + { + "to": "scassenes.runners@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour S'Cassennes Runners", + "content": "

Bonjour S'Cassennes Runners

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:48.659723" + }, + { + "to": "scassenes.runners@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour S'Cassennes Runners

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:08.748068" + }, + { + "to": "scassenes.runners@gmail.com", + "subject": "Relance : Collaboration avec S'Cassennes Runners pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour S'Cassennes Runners

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:15:58.744136" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_49288094.json b/Data/email_history/pros_49288094.json new file mode 100644 index 0000000..cc01aae --- /dev/null +++ b/Data/email_history/pros_49288094.json @@ -0,0 +1,34 @@ +[ + { + "to": "info@accessandgo-abp.be", + "subject": "Proposition de services en d\u00e9veloppement pour Access & Go", + "content": "

Bonjour Access & Go

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:49.841892" + }, + { + "to": "info@accessandgo-abp.be", + "subject": "Proposition de services en d\u00e9veloppement pour Access & Go ASBL", + "content": "

Bonjour Access & Go

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:06.561087" + }, + { + "to": "info@accessandgo-abp.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Access & Go

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:23.797500" + }, + { + "to": "info@accessandgo-abp.be", + "subject": "Relance : Collaboration avec Access & Go ASBL pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Access & Go

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:13.180745" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_4ba87e61.json b/Data/email_history/pros_4ba87e61.json new file mode 100644 index 0000000..f9beb35 --- /dev/null +++ b/Data/email_history/pros_4ba87e61.json @@ -0,0 +1,34 @@ +[ + { + "to": "sportinn@skynet.com", + "subject": "Proposition de services en d\u00e9veloppement pour Sport Inn", + "content": "

Bonjour Sport Inn

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:54:26.723241" + }, + { + "to": "sportinn@skynet.com", + "subject": "Proposition de services en d\u00e9veloppement pour Sport Inn SPRL", + "content": "

Bonjour Sport Inn

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:57.475753" + }, + { + "to": "sportinn@skynet.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Sport Inn

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:14.929817" + }, + { + "to": "sportinn@skynet.com", + "subject": "Relance : Collaboration avec Sport Inn SPRL pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Sport Inn

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:05.572170" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_5a76f6ad.json b/Data/email_history/pros_5a76f6ad.json new file mode 100644 index 0000000..e3afddf --- /dev/null +++ b/Data/email_history/pros_5a76f6ad.json @@ -0,0 +1,34 @@ +[ + { + "to": "helene.vanco@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme Clara", + "content": "

Bonjour Ferme Clara

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:54:25.650029" + }, + { + "to": "helene.vanco@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme Clara", + "content": "

Bonjour Ferme Clara

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:50.722752" + }, + { + "to": "helene.vanco@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ferme Clara

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:09.870982" + }, + { + "to": "helene.vanco@gmail.com", + "subject": "Relance : Collaboration avec Ferme Clara pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ferme Clara

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:15:59.871786" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_6408b8f9.json b/Data/email_history/pros_6408b8f9.json new file mode 100644 index 0000000..d39af27 --- /dev/null +++ b/Data/email_history/pros_6408b8f9.json @@ -0,0 +1,10 @@ +[ + { + "to": "rudy.lucas67@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour LR Citerne Service", + "content": "

Bonjour Rudy

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:18:02.511983" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_6862956b.json b/Data/email_history/pros_6862956b.json new file mode 100644 index 0000000..14a9c49 --- /dev/null +++ b/Data/email_history/pros_6862956b.json @@ -0,0 +1,26 @@ +[ + { + "to": "judopassion.7190@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour JudoPassion", + "content": "

Bonjour JudoPassion

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:15.331296" + }, + { + "to": "judopassion.7190@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour JudoPassion", + "content": "

Bonjour JudoPassion

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:43.803766" + }, + { + "to": "judopassion.7190@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour JudoPassion", + "content": "

Bonjour JudoPassion

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:47.649430" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_6c39a6a7.json b/Data/email_history/pros_6c39a6a7.json new file mode 100644 index 0000000..64c76bd --- /dev/null +++ b/Data/email_history/pros_6c39a6a7.json @@ -0,0 +1,34 @@ +[ + { + "to": "lemogroupe@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour EPIK", + "content": "

Bonjour EPIK

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:47.777289" + }, + { + "to": "lemogroupe@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour EPIK

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:58.788141" + }, + { + "to": "lemogroupe@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour EPIK

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:16.015900" + }, + { + "to": "lemogroupe@gmail.com", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour EPIK

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:06.633605" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_70b4ce19.json b/Data/email_history/pros_70b4ce19.json new file mode 100644 index 0000000..3adfec7 --- /dev/null +++ b/Data/email_history/pros_70b4ce19.json @@ -0,0 +1,34 @@ +[ + { + "to": "info@coquins-coquines.net", + "subject": "Proposition de services en d\u00e9veloppement pour Coquins Coquines", + "content": "

Bonjour Coquins Coquines

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:17.481551" + }, + { + "to": "info@coquins-coquines.net", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour Coquins Coquines

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:51.741909" + }, + { + "to": "info@coquins-coquines.net", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Coquins Coquines

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:10.894863" + }, + { + "to": "info@coquins-coquines.net", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Coquins Coquines

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:00.868219" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_81e5eda3.json b/Data/email_history/pros_81e5eda3.json new file mode 100644 index 0000000..0a5f262 --- /dev/null +++ b/Data/email_history/pros_81e5eda3.json @@ -0,0 +1,34 @@ +[ + { + "to": "Benhamouda1900@hotmail.gr", + "subject": "Proposition de services en d\u00e9veloppement pour Bella Ciao", + "content": "

Bonjour Bella Ciao

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-16T08:52:34.261276" + }, + { + "to": "Benhamouda1900@hotmail.gr", + "subject": "Proposition de services en d\u00e9veloppement pour Bella Ciao Pizzeria", + "content": "

Bonjour Bella Ciao

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:56.347271" + }, + { + "to": "Benhamouda1900@hotmail.gr", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Bella Ciao

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:13.966370" + }, + { + "to": "Benhamouda1900@hotmail.gr", + "subject": "Relance : Collaboration avec Bella Ciao Pizzeria pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Bella Ciao

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:04.537335" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_84b2b824.json b/Data/email_history/pros_84b2b824.json new file mode 100644 index 0000000..7e09951 --- /dev/null +++ b/Data/email_history/pros_84b2b824.json @@ -0,0 +1,50 @@ +[ + { + "to": "anthony.violet@outlook.be", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-24T11:57:08.322288" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Anthony

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:19.692890" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:51:33.640091" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Proposition de services en d\u00e9veloppement pour ToineIndustries", + "content": "

Bonjour Anthony

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:55.447457" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:10:15.465661" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Relance : Collaboration avec {{company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:11:21.540828" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_87f0f049.json b/Data/email_history/pros_87f0f049.json new file mode 100644 index 0000000..db45399 --- /dev/null +++ b/Data/email_history/pros_87f0f049.json @@ -0,0 +1,10 @@ +[ + { + "to": "rcguerrieri@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T16:00:40.503146" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_88356f38.json b/Data/email_history/pros_88356f38.json new file mode 100644 index 0000000..95737e5 --- /dev/null +++ b/Data/email_history/pros_88356f38.json @@ -0,0 +1,34 @@ +[ + { + "to": "info@dynarythmique.be", + "subject": "Proposition de services en d\u00e9veloppement pour DynaRythmique", + "content": "

Bonjour DynaRythmique

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:54:24.634381" + }, + { + "to": "info@dynarythmique.be", + "subject": "Proposition de services en d\u00e9veloppement pour DynaRythmique", + "content": "

Bonjour DynaRythmique

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:46.416255" + }, + { + "to": "info@dynarythmique.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour DynaRythmique

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:07.721598" + }, + { + "to": "info@dynarythmique.be", + "subject": "Relance : Collaboration avec DynaRythmique pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour DynaRythmique

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:15:57.717506" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_893aaaaf.json b/Data/email_history/pros_893aaaaf.json new file mode 100644 index 0000000..e8bf204 --- /dev/null +++ b/Data/email_history/pros_893aaaaf.json @@ -0,0 +1,34 @@ +[ + { + "to": "lesapprentis.sages7090@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Les Apprentis Sages", + "content": "

Bonjour Les Apprentis Sages

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:25.675421" + }, + { + "to": "lesapprentis.sages7090@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour Les Apprentis Sages

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:17.240530" + }, + { + "to": "lesapprentis.sages7090@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Les Apprentis Sages

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:35.575745" + }, + { + "to": "lesapprentis.sages7090@gmail.com", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Les Apprentis Sages

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:24.244093" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_8b54671e.json b/Data/email_history/pros_8b54671e.json new file mode 100644 index 0000000..74a67ad --- /dev/null +++ b/Data/email_history/pros_8b54671e.json @@ -0,0 +1,18 @@ +[ + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}},

\r\n\r\n

Je reviens vers vous comme convenu suite \u00e0 notre pr\u00e9c\u00e9dent \u00e9change.

\r\n\r\n

Je serais ravi de vous pr\u00e9senter une proposition adapt\u00e9e aux besoins de {{company}}, afin de voir ensemble comment mes services en d\u00e9veloppement web et logiciel peuvent vous accompagner.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-24T10:43:39.557118" + }, + { + "to": "anthony.violet@outlook.be", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}},

\r\n\r\n

Je reviens vers vous comme convenu suite \u00e0 notre pr\u00e9c\u00e9dent \u00e9change.

\r\n\r\n

Je serais ravi de vous pr\u00e9senter une proposition adapt\u00e9e aux besoins de {{company}}, afin de voir ensemble comment mes services en d\u00e9veloppement web et logiciel peuvent vous accompagner.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-24T10:44:47.408461" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_8d36a719.json b/Data/email_history/pros_8d36a719.json new file mode 100644 index 0000000..5d5bcc0 --- /dev/null +++ b/Data/email_history/pros_8d36a719.json @@ -0,0 +1,26 @@ +[ + { + "to": "siana2812@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:01:33.916537" + }, + { + "to": "siana2812@hotmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ana\u00efs

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:37.725598" + }, + { + "to": "siana2812@hotmail.com", + "subject": "Relance : Collaboration avec Ana\u00efs Hair Artist pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ana\u00efs

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:26.391429" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_8f11aab0.json b/Data/email_history/pros_8f11aab0.json new file mode 100644 index 0000000..64ee580 --- /dev/null +++ b/Data/email_history/pros_8f11aab0.json @@ -0,0 +1,10 @@ +[ + { + "to": "stephanedelaval@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour SRL St\u00e9phane Delaval", + "content": "

Bonjour St\u00e9phane

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:57.508055" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_92e930a9.json b/Data/email_history/pros_92e930a9.json new file mode 100644 index 0000000..d9b222e --- /dev/null +++ b/Data/email_history/pros_92e930a9.json @@ -0,0 +1,26 @@ +[ + { + "to": "Verbiestconstruct@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Verbiest Construct SRL", + "content": "

Bonjour Verbiest Construct

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:12:09.014397" + }, + { + "to": "Verbiestconstruct@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Verbiest Construct

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:30.250258" + }, + { + "to": "Verbiestconstruct@gmail.com", + "subject": "Relance : Collaboration avec Verbiest Construct SRL pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Verbiest Construct

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:20.148924" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_9a4bc976.json b/Data/email_history/pros_9a4bc976.json new file mode 100644 index 0000000..2cf26d2 --- /dev/null +++ b/Data/email_history/pros_9a4bc976.json @@ -0,0 +1,34 @@ +[ + { + "to": "marieetlucdr@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme du vieux canal", + "content": "

Bonjour Ferme du vieux canal

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T19:31:41.806654" + }, + { + "to": "marieetlucdr@skynet.be", + "subject": "Proposition de services en d\u00e9veloppement pour Ferme Du Vieux Canal", + "content": "

Bonjour Ferme du vieux canal

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:07.721339" + }, + { + "to": "marieetlucdr@skynet.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ferme du vieux canal

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:25.846180" + }, + { + "to": "marieetlucdr@skynet.be", + "subject": "Relance : Collaboration avec Ferme Du Vieux Canal pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Ferme du vieux canal

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:15.228158" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_9db969b9.json b/Data/email_history/pros_9db969b9.json new file mode 100644 index 0000000..2c292b7 --- /dev/null +++ b/Data/email_history/pros_9db969b9.json @@ -0,0 +1,10 @@ +[ + { + "to": "geoffroy.fenain@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Geoffrey Fenain", + "content": "

Bonjour Geoffrey

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:18:01.602646" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_a807ad21.json b/Data/email_history/pros_a807ad21.json new file mode 100644 index 0000000..8f9090e --- /dev/null +++ b/Data/email_history/pros_a807ad21.json @@ -0,0 +1,34 @@ +[ + { + "to": "jns.coaching.nutrition@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour JNS Coaching", + "content": "

Bonjour JNS Coaching

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:20.554272" + }, + { + "to": "jns.coaching.nutrition@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour JNS Coaching", + "content": "

Bonjour JNS Coaching

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:08.756911" + }, + { + "to": "jns.coaching.nutrition@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour JNS Coaching

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:26.788025" + }, + { + "to": "jns.coaching.nutrition@gmail.com", + "subject": "Relance : Collaboration avec JNS Coaching pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour JNS Coaching

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:16.971478" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_a8e85854.json b/Data/email_history/pros_a8e85854.json new file mode 100644 index 0000000..eb953b3 --- /dev/null +++ b/Data/email_history/pros_a8e85854.json @@ -0,0 +1,26 @@ +[ + { + "to": "aurelie.verbiest@hotmail.be", + "subject": "Proposition de services en d\u00e9veloppement pour Les Caprices de Juliette ", + "content": "

Bonjour Aur\u00e9lie

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:12:10.240548" + }, + { + "to": "aurelie.verbiest@hotmail.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Aur\u00e9lie

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:31.999161" + }, + { + "to": "aurelie.verbiest@hotmail.be", + "subject": "Relance : Collaboration avec Les Caprices de Juliette pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Aur\u00e9lie

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:21.169384" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_b31e8791.json b/Data/email_history/pros_b31e8791.json new file mode 100644 index 0000000..d0fae87 --- /dev/null +++ b/Data/email_history/pros_b31e8791.json @@ -0,0 +1,10 @@ +[ + { + "to": "opticalementvotre@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Opticalement Votre", + "content": "

Bonjour Opticalement Votre

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:17:52.489201" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_c29ab1cf.json b/Data/email_history/pros_c29ab1cf.json new file mode 100644 index 0000000..a7df6a2 --- /dev/null +++ b/Data/email_history/pros_c29ab1cf.json @@ -0,0 +1,34 @@ +[ + { + "to": "recreabraine@7090.be", + "subject": "Proposition de services en d\u00e9veloppement pour Recrea Braine", + "content": "

Bonjour Recrea Braine

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T15:59:45.615173" + }, + { + "to": "recreabraine@7090.be", + "subject": "Proposition de services en d\u00e9veloppement pour ", + "content": "

Bonjour Recrea Braine

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:01.305979" + }, + { + "to": "recreabraine@7090.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Recrea Braine

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:18.782406" + }, + { + "to": "recreabraine@7090.be", + "subject": "Relance : Collaboration avec pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Recrea Braine

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:08.575058" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_c93b6d00.json b/Data/email_history/pros_c93b6d00.json new file mode 100644 index 0000000..c54791e --- /dev/null +++ b/Data/email_history/pros_c93b6d00.json @@ -0,0 +1,10 @@ +[ + { + "to": "sergio.vasquez-perez@vinitales.be", + "subject": "Proposition de services en d\u00e9veloppement pour Vinicoles", + "content": "

Bonjour Sergio

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T11:18:00.337497" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_ca965f43.json b/Data/email_history/pros_ca965f43.json new file mode 100644 index 0000000..00fba64 --- /dev/null +++ b/Data/email_history/pros_ca965f43.json @@ -0,0 +1,34 @@ +[ + { + "to": "infokidsandcoevents@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Le chat pitre", + "content": "

Bonjour Le chat pitre

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-16T08:52:35.352102" + }, + { + "to": "infokidsandcoevents@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Le chat pitre", + "content": "

Bonjour Le chat pitre

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:11.199985" + }, + { + "to": "infokidsandcoevents@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Le chat pitre

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:27.763081" + }, + { + "to": "infokidsandcoevents@gmail.com", + "subject": "Relance : Collaboration avec Le chat pitre pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Le chat pitre

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:17.994066" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_cfb8950b.json b/Data/email_history/pros_cfb8950b.json new file mode 100644 index 0000000..8ac53e5 --- /dev/null +++ b/Data/email_history/pros_cfb8950b.json @@ -0,0 +1,34 @@ +[ + { + "to": "info@midservices.be", + "subject": "Proposition de services en d\u00e9veloppement pour MIDServices", + "content": "

Bonjour MIDServices

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:54:27.697024" + }, + { + "to": "info@midservices.be", + "subject": "Proposition de services en d\u00e9veloppement pour MiD Service", + "content": "

Bonjour MIDServices

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:00.044540" + }, + { + "to": "info@midservices.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour MIDServices

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:17.655342" + }, + { + "to": "info@midservices.be", + "subject": "Relance : Collaboration avec MiD Service pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour MIDServices

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:07.562527" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_d970305c.json b/Data/email_history/pros_d970305c.json new file mode 100644 index 0000000..5022ff2 --- /dev/null +++ b/Data/email_history/pros_d970305c.json @@ -0,0 +1,34 @@ +[ + { + "to": "unpeudetout.fromagerie@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Un peu de tout ", + "content": "

Bonjour Un peu de tout

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Seriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-06-01T16:45:36.406557" + }, + { + "to": "unpeudetout.fromagerie@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Un peu de tout", + "content": "

Bonjour Un peu de tout

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:18.321454" + }, + { + "to": "unpeudetout.fromagerie@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Un peu de tout

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:36.701160" + }, + { + "to": "unpeudetout.fromagerie@gmail.com", + "subject": "Relance : Collaboration avec Un peu de tout pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Un peu de tout

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:25.368930" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_e1855f79.json b/Data/email_history/pros_e1855f79.json new file mode 100644 index 0000000..aeaea6b --- /dev/null +++ b/Data/email_history/pros_e1855f79.json @@ -0,0 +1,10 @@ +[ + { + "to": "marjorie-malschalck@hotmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Marjorie Malscalk", + "content": "

Bonjour Marjorie Malscalk

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:22.602994" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_e8b7dd8b.json b/Data/email_history/pros_e8b7dd8b.json new file mode 100644 index 0000000..031cb6a --- /dev/null +++ b/Data/email_history/pros_e8b7dd8b.json @@ -0,0 +1,34 @@ +[ + { + "to": "c.vaneeckerode@asbl-belair.be", + "subject": "Proposition de services en d\u00e9veloppement pour Claire Van Eeckenrode", + "content": "

Bonjour Claire Van Eeckenrode

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-14T12:57:19.632457" + }, + { + "to": "c.vaneeckerode@asbl-belair.be", + "subject": "Proposition de services en d\u00e9veloppement pour ASBL Bel'Air", + "content": "

Bonjour Claire Van Eeckenrode

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:05.468784" + }, + { + "to": "c.vaneeckerode@asbl-belair.be", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Claire Van Eeckenrode

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:22.773591" + }, + { + "to": "c.vaneeckerode@asbl-belair.be", + "subject": "Relance : Collaboration avec ASBL Bel'Air pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Claire Van Eeckenrode

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:12.161291" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_f279cb3e.json b/Data/email_history/pros_f279cb3e.json new file mode 100644 index 0000000..33c2010 --- /dev/null +++ b/Data/email_history/pros_f279cb3e.json @@ -0,0 +1,34 @@ +[ + { + "to": "lecape.asbl@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour Le CAP", + "content": "

Bonjour Le CAP

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-15T08:31:44.921330" + }, + { + "to": "lecape.asbl@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour LE CAP", + "content": "

Bonjour Le CAP

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:16:54.202810" + }, + { + "to": "lecape.asbl@gmail.com", + "subject": "Relance : Collaboration avec {{Company}} pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Le CAP

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

nt\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-06T10:49:12.943211" + }, + { + "to": "lecape.asbl@gmail.com", + "subject": "Relance : Collaboration avec LE CAP pour optimiser vos outils num\u00e9riques ?", + "content": "

Bonjour Le CAP

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma pr\u00e9c\u00e9dente proposition pour vous aider \u00e0 optimiser vos outils num\u00e9riques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la v\u00f4tre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adapt\u00e9s \u00e0 votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de r\u00e9alisations : BatiPro - Expert en construction

\r\n

Int\u00e9ress\u00e9 ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-09-20T11:16:02.839847" + } +] \ No newline at end of file diff --git a/Data/email_history/pros_fac952ce.json b/Data/email_history/pros_fac952ce.json new file mode 100644 index 0000000..c6147c0 --- /dev/null +++ b/Data/email_history/pros_fac952ce.json @@ -0,0 +1,98 @@ +[ + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Toine et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": false, + "error": "(534, b'5.7.9 Application-specific password required. For more information, go to\\n5.7.9 https://support.google.com/mail/?p=InvalidSecondFactor ffacd0b85a97d-3a1f5a4cc44sm16815447f8f.85 - gsmtp')", + "timestamp": "2025-05-13T20:37:55.262480" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Toine et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": false, + "error": "(534, b'5.7.9 Application-specific password required. For more information, go to\\n5.7.9 https://support.google.com/mail/?p=InvalidSecondFactor ffacd0b85a97d-3a1f5a2cf38sm17038601f8f.77 - gsmtp')", + "timestamp": "2025-05-13T20:38:12.249481" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Toine et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": false, + "error": "(534, b'5.7.9 Application-specific password required. For more information, go to\\n5.7.9 https://support.google.com/mail/?p=InvalidSecondFactor ffacd0b85a97d-3a1f58f2f65sm17418489f8f.55 - gsmtp')", + "timestamp": "2025-05-13T20:39:31.270747" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Toine et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": false, + "error": "(534, b'5.7.9 Application-specific password required. For more information, go to\\n5.7.9 https://support.google.com/mail/?p=InvalidSecondFactor 5b1f17b1804b1-442d687ae10sm175778715e9.37 - gsmtp')", + "timestamp": "2025-05-13T20:40:48.484102" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Toine et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:43:20.012289" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition commerciale - moi", + "content": "

Bonjour moi,

Suite \u00e0 nos diff\u00e9rents \u00e9changes, j'ai le plaisir de vous faire parvenir notre proposition commerciale pour votre projet.

Vous trouverez en pi\u00e8ce jointe un document d\u00e9taillant :

N'h\u00e9sitez pas \u00e0 me contacter pour toute question ou pr\u00e9cision concernant cette proposition. Je reste \u00e0 votre enti\u00e8re disposition pour en discuter via t\u00e9l\u00e9phone ou lors d'une r\u00e9union.

Dans l'attente de votre retour, je vous souhaite une excellente journ\u00e9e.

Cordialement,

Suite Consultance
Experts en solutions num\u00e9riques

", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:46:28.579685" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "Bonjour {{name}}\r\n\r\nJe m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.\r\n\r\nBien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.\r\n\r\nSeriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.\r\n\r\nMerci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.\r\n\r\nBien cordialement,\r\nVIOLET Anthony", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:47:34.862542" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Seriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:51:46.972105" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Seriez-vous disponible pour un appel ou une rencontre dans les prochains jours ? Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:53:13.468575" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour moi", + "content": "

Bonjour moi

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Je reste \u00e0 votre disposition pour toute question.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-05-13T20:54:28.694200" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Suivi de notre premi\u00e8re conversation - {{name}}", + "content": "

Bonjour {{name}},

Je vous remercie pour notre \u00e9change r\u00e9cent concernant vos besoins en d\u00e9veloppement informatique.

Suite \u00e0 notre discussion, je serais heureux de vous pr\u00e9senter nos services qui pourraient r\u00e9pondre \u00e0 vos attentes pour votre entreprise {{company}}.

Nous proposons des solutions personnalis\u00e9es pour :

Seriez-vous disponible pour un appel plus approfondi cette semaine afin de discuter davantage de votre projet ?

Je reste \u00e0 votre disposition pour toute question.

Cordialement,

Suite Consultance
Experts en solutions num\u00e9riques

", + "success": true, + "error": null, + "timestamp": "2025-05-14T08:40:15.438584" + }, + { + "to": "anthony.flet@gmail.com", + "subject": "Proposition de services en d\u00e9veloppement pour test", + "content": "

Bonjour moi

\r\n\r\n

Je m\u2019appelle Anthony et je suis d\u00e9veloppeur freelance sp\u00e9cialis\u00e9 dans dans le d\u00e9veloppement web et logiciel. Je suis actuellement \u00e0 la recherche de nouvelles collaborations avec des entreprises locales comme la v\u00f4tre, afin de les aider \u00e0 optimiser leurs outils num\u00e9riques et \u00e0 d\u00e9velopper de nouvelles solutions.

\r\n\r\n

Bien que je d\u00e9bute dans la consultance, j\u2019ai d\u00e9j\u00e0 une solide exp\u00e9rience en d\u00e9veloppement et je suis motiv\u00e9 \u00e0 apporter des solutions sur mesure \u00e0 vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d\u2019avance pour votre attention et au plaisir d\u2019\u00e9changer avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin", + "success": true, + "error": null, + "timestamp": "2025-08-23T20:17:16.113232" + } +] \ No newline at end of file diff --git a/Data/email_templates/prospect_followup.json b/Data/email_templates/prospect_followup.json new file mode 100644 index 0000000..51a1b19 --- /dev/null +++ b/Data/email_templates/prospect_followup.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_followup", + "name": "Relance Cron auto", + "description": "cron relance", + "subject": "Proposition de services en développement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m’appelle Anthony et je suis développeur freelance spécialisé dans dans le développement web et logiciel. Je suis actuellement à la recherche de nouvelles collaborations avec des entreprises locales comme la vôtre, afin de les aider à optimiser leurs outils numériques et à développer de nouvelles solutions.

\r\n\r\n

Bien que je débute dans la consultance, j’ai déjà une solide expérience en développement et je suis motivé à apporter des solutions sur mesure à vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d’avance pour votre attention et au plaisir d’échanger avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin" +} \ No newline at end of file diff --git a/Data/email_templates/tpl_2ed2996c.json b/Data/email_templates/tpl_2ed2996c.json new file mode 100644 index 0000000..10c05eb --- /dev/null +++ b/Data/email_templates/tpl_2ed2996c.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_2ed2996c", + "name": "second contact prospect", + "description": "Après un premier contact, si le prospect propose de se recontacter.", + "subject": "Proposition de services en développement pour {{company}}", + "content": "

Bonjour {{name}},

\r\n\r\n

Je reviens vers vous comme convenu suite à notre précédent échange.

\r\n\r\n

Je serais ravi de vous présenter une proposition adaptée aux besoins de {{company}}, afin de voir ensemble comment mes services en développement web et logiciel peuvent vous accompagner.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin" +} \ No newline at end of file diff --git a/Data/email_templates/tpl_3fb244d7.json b/Data/email_templates/tpl_3fb244d7.json new file mode 100644 index 0000000..ff79203 --- /dev/null +++ b/Data/email_templates/tpl_3fb244d7.json @@ -0,0 +1,7 @@ +{ + "id": "prospect_followup", + "name": "Relance Cron auto", + "description": "cron relance", + "subject": "Proposition de services en développement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m’appelle Anthony et je suis développeur freelance spécialisé dans dans le développement web et logiciel. Je suis actuellement à la recherche de nouvelles collaborations avec des entreprises locales comme la vôtre, afin de les aider à optimiser leurs outils numériques et à développer de nouvelles solutions.

\r\n\r\n

Bien que je débute dans la consultance, j’ai déjà une solide expérience en développement et je suis motivé à apporter des solutions sur mesure à vos projets. Je serais ravi de discuter de vos besoins et de voir comment nous pourrions collaborer.

\r\n\r\n

Merci d’avance pour votre attention et au plaisir d’échanger avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin" +} \ No newline at end of file diff --git a/Data/email_templates/tpl_60f1978e.json b/Data/email_templates/tpl_60f1978e.json new file mode 100644 index 0000000..af779c1 --- /dev/null +++ b/Data/email_templates/tpl_60f1978e.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_60f1978e", + "name": "Premier prospect", + "description": "Email pour se présenter", + "subject": "Proposition de services en développement pour {{company}}", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m’appelle Anthony et je suis développeur freelance spécialisé dans dans le développement web et logiciel. Je suis actuellement à la recherche de nouvelles collaborations avec des entreprises locales comme la vôtre, afin de les aider à optimiser leurs outils numériques et à développer de nouvelles solutions.

\r\n\r\n

Je travaille avec des entreprises comme la vôtre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adaptés à votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de réalisations : BatiPro - Expert en construction

\r\n

ntéressé ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d’avance pour votre attention et au plaisir d’échanger avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin" +} \ No newline at end of file diff --git a/Data/email_templates/tpl_93164bd8.json b/Data/email_templates/tpl_93164bd8.json new file mode 100644 index 0000000..fe502aa --- /dev/null +++ b/Data/email_templates/tpl_93164bd8.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_93164bd8", + "name": "relance prospect", + "description": "Permet de relancer poliment le prospect", + "subject": "Relance : Collaboration avec {{company}} pour optimiser vos outils numériques ?", + "content": "

Bonjour {{name}}

\r\n\r\n

Je m’appelle Anthony et je suis développeur freelance spécialisé dans dans le développement web et logiciel. Je suis actuellement à la recherche de nouvelles collaborations avec des entreprises locales comme la vôtre, afin de les aider à optimiser leurs outils numériques et à développer de nouvelles solutions.

\r\n\r\n

Je me permets de vous recontacter concernant ma précédente proposition pour vous aider à optimiser vos outils numériques (site web, automatisation, solutions sur mesure).

\r\n\r\n

Je travaille avec des entreprises comme la vôtre pour leur proposer des solutions simples, efficaces et sans jargon technique. Mon objectif : vous faire gagner du temps et des clients avec des outils adaptés à votre budget.

\r\n\r\n

Pourquoi me contacter ?

\r\n\r\n\r\n

Exemple de réalisations : BatiPro - Expert en construction

\r\n

Intéressé ? Un simple retour par email suffit pour en discuter.

\r\n\r\n

Merci d’avance pour votre attention et au plaisir d’échanger avec vous.

\r\n\r\n

Bien cordialement,
\r\nVIOLET Anthony

\r\n
\r\nMon profil linkedin" +} \ No newline at end of file diff --git a/Data/email_templates/tpl_first_contact.json b/Data/email_templates/tpl_first_contact.json new file mode 100644 index 0000000..d970ab7 --- /dev/null +++ b/Data/email_templates/tpl_first_contact.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_first_contact", + "name": "Premier contact", + "description": "Email de premier contact avec un prospect", + "subject": "Suivi de notre première conversation - {{name}}", + "content": "

Bonjour {{name}},

Je vous remercie pour notre échange récent concernant vos besoins en développement informatique.

Suite à notre discussion, je serais heureux de vous présenter nos services qui pourraient répondre à vos attentes pour votre entreprise {{company}}.

Nous proposons des solutions personnalisées pour :

Seriez-vous disponible pour un appel plus approfondi cette semaine afin de discuter davantage de votre projet ?

Je reste à votre disposition pour toute question.

Cordialement,

Suite Consultance
Experts en solutions numériques

" +} diff --git a/Data/email_templates/tpl_proposition.json b/Data/email_templates/tpl_proposition.json new file mode 100644 index 0000000..98cb53e --- /dev/null +++ b/Data/email_templates/tpl_proposition.json @@ -0,0 +1,7 @@ +{ + "id": "tpl_proposition", + "name": "Envoi de proposition", + "description": "Email pour l'envoi d'une proposition commerciale", + "subject": "Proposition commerciale - {{company}}", + "content": "

Bonjour {{name}},

Suite à nos différents échanges, j'ai le plaisir de vous faire parvenir notre proposition commerciale pour votre projet.

Vous trouverez en pièce jointe un document détaillant :

N'hésitez pas à me contacter pour toute question ou précision concernant cette proposition. Je reste à votre entière disposition pour en discuter via téléphone ou lors d'une réunion.

Dans l'attente de votre retour, je vous souhaite une excellente journée.

Cordialement,

Suite Consultance
Experts en solutions numériques

" +} diff --git a/Data/logo_dark.png b/Data/logo_dark.png new file mode 100644 index 0000000..e5d3572 Binary files /dev/null and b/Data/logo_dark.png differ diff --git a/Data/logo_light.png b/Data/logo_light.png new file mode 100644 index 0000000..d9e00f8 Binary files /dev/null and b/Data/logo_light.png differ diff --git a/Data/prospects/pros_245c68bc.json b/Data/prospects/pros_245c68bc.json new file mode 100644 index 0000000..bc4a334 --- /dev/null +++ b/Data/prospects/pros_245c68bc.json @@ -0,0 +1,19 @@ +{ + "id": "pros_245c68bc", + "name": "Tipi du bonheur", + "company": "", + "email": "tipidubonheur@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "braine le comte", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-06-01", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_248b289a.json b/Data/prospects/pros_248b289a.json new file mode 100644 index 0000000..8fe3275 --- /dev/null +++ b/Data/prospects/pros_248b289a.json @@ -0,0 +1,21 @@ +{ + "id": "pros_248b289a", + "name": "Nicole", + "company": "NISAV EVENTS", + "email": "nicole@nisav.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "evenementiel" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_27cfe29d.json b/Data/prospects/pros_27cfe29d.json new file mode 100644 index 0000000..f2b0cd7 --- /dev/null +++ b/Data/prospects/pros_27cfe29d.json @@ -0,0 +1,22 @@ +{ + "id": "pros_27cfe29d", + "name": "Freelancer Client anonyme", + "company": "Freelancer", + "email": "aucune@aucune.com", + "phone": "-", + "source": "Réseaux sociaux", + "notes": "Lien : https://www.fr.freelancer.com/projects/php/Developpement-site-internet-39716647/details", + "status": "Non intéressé", + "tags": [ + "freelancer", + "site vitrine" + ], + "last_contact": "2025-08-23", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_28680571.json b/Data/prospects/pros_28680571.json new file mode 100644 index 0000000..043d1dc --- /dev/null +++ b/Data/prospects/pros_28680571.json @@ -0,0 +1,23 @@ +{ + "id": "pros_28680571", + "name": "Saveurs PACA", + "company": "Les Saveurs de PACA", + "email": "lessaveursdepaca@gmail.com", + "phone": "", + "source": "Site web", + "notes": "Situé à Écaussinnes", + "status": "Contacté", + "tags": [ + "pme", + "commerce", + "locale" + ], + "last_contact": "2025-09-06", + "next_action": "Recontacter", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_2968493a.json b/Data/prospects/pros_2968493a.json new file mode 100644 index 0000000..635c8cc --- /dev/null +++ b/Data/prospects/pros_2968493a.json @@ -0,0 +1,22 @@ +{ + "id": "pros_2968493a", + "name": "Les cochons d'ici", + "company": "Les cochons d'ici", + "email": "Beraurelie@hotmail.com", + "phone": "", + "source": "", + "notes": "", + "status": "Relancé", + "tags": [ + "ferme", + "agriculture" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_2e687b94.json b/Data/prospects/pros_2e687b94.json new file mode 100644 index 0000000..14ef236 --- /dev/null +++ b/Data/prospects/pros_2e687b94.json @@ -0,0 +1,24 @@ +{ + "id": "pros_2e687b94", + "name": "Océane Ballez", + "company": "Océane Ballez - Professionnelle de la beauté & bien-être ", + "email": "oceaneballez13@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "beauté", + "bien être", + "soin", + "esthétique" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_38947d7a.json b/Data/prospects/pros_38947d7a.json new file mode 100644 index 0000000..88f2450 --- /dev/null +++ b/Data/prospects/pros_38947d7a.json @@ -0,0 +1,20 @@ +{ + "id": "pros_38947d7a", + "name": "Laurent Sartiaux", + "company": "Art Tech Beton", + "email": "artechbeton@gmail.com", + "phone": "0471534789 ", + "source": "Site web", + "notes": "Prise de contact réalisée. Le prospect s'attend a une nouvelle prise de contact pour début ou mi septembre.", + "status": "Qualifié", + "tags": [ + "maçon", + "entreprise du bâtiment" + ], + "last_contact": "2025-08-23", + "next_action": "Reprendre contact en septembre.", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23" +} \ No newline at end of file diff --git a/Data/prospects/pros_3924c706.json b/Data/prospects/pros_3924c706.json new file mode 100644 index 0000000..e6891e2 --- /dev/null +++ b/Data/prospects/pros_3924c706.json @@ -0,0 +1,19 @@ +{ + "id": "pros_3924c706", + "name": "Fluide", + "company": "Fluide", + "email": "info@fluide.be", + "phone": "", + "source": "", + "notes": "", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_392fbe80.json b/Data/prospects/pros_392fbe80.json new file mode 100644 index 0000000..e2160f7 --- /dev/null +++ b/Data/prospects/pros_392fbe80.json @@ -0,0 +1,22 @@ +{ + "id": "pros_392fbe80", + "name": "Lolotte", + "company": "La boîte à jeu de lolotte", + "email": "laboiteajeuxdelolotte@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "Divertissement", + "loisirs" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_3aa69d74.json b/Data/prospects/pros_3aa69d74.json new file mode 100644 index 0000000..a4e789b --- /dev/null +++ b/Data/prospects/pros_3aa69d74.json @@ -0,0 +1,23 @@ +{ + "id": "pros_3aa69d74", + "name": "Lebleu Elise", + "company": "Ferme Delporte", + "email": "Lebleu.elise@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "ferme", + "agriculture", + "marché" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_3cebd19d.json b/Data/prospects/pros_3cebd19d.json new file mode 100644 index 0000000..e53d77e --- /dev/null +++ b/Data/prospects/pros_3cebd19d.json @@ -0,0 +1,23 @@ +{ + "id": "pros_3cebd19d", + "name": "TP Studio Plus", + "company": "Tempo studio plus", + "email": "rcguerrieri@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "braine le comte", + "status": "Relancé", + "tags": [ + "evenementiel", + "DJ", + "musique" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-06-01", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_3dfec9c4.json b/Data/prospects/pros_3dfec9c4.json new file mode 100644 index 0000000..16a0352 --- /dev/null +++ b/Data/prospects/pros_3dfec9c4.json @@ -0,0 +1,21 @@ +{ + "id": "pros_3dfec9c4", + "name": "Mariage Prélude", + "company": "Mariage prélude", + "email": "info@mariage-prelude.com", + "phone": "", + "source": "Site web", + "notes": "email inconnue", + "status": "Non intéressé", + "tags": [ + "commerce" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_3e35a76f.json b/Data/prospects/pros_3e35a76f.json new file mode 100644 index 0000000..590038b --- /dev/null +++ b/Data/prospects/pros_3e35a76f.json @@ -0,0 +1,22 @@ +{ + "id": "pros_3e35a76f", + "name": "Brasserie Artisanale BLC", + "company": "", + "email": "brasserieblc@gmail.com", + "phone": "+32 (0) 484 81 50 88", + "source": "Site web", + "notes": "Vers la résidence Rey\r\n", + "status": "Relancé", + "tags": [ + "brasserie", + "commerce" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_3f7db2e1.json b/Data/prospects/pros_3f7db2e1.json new file mode 100644 index 0000000..780569e --- /dev/null +++ b/Data/prospects/pros_3f7db2e1.json @@ -0,0 +1,21 @@ +{ + "id": "pros_3f7db2e1", + "name": "S'Cassennes Runners", + "company": "S'Cassennes Runners", + "email": "scassenes.runners@gmail.com", + "phone": "", + "source": "Site web", + "notes": "Probablement une association", + "status": "Relancé", + "tags": [ + "pme" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_49288094.json b/Data/prospects/pros_49288094.json new file mode 100644 index 0000000..cce2a8f --- /dev/null +++ b/Data/prospects/pros_49288094.json @@ -0,0 +1,19 @@ +{ + "id": "pros_49288094", + "name": "Access & Go", + "company": "Access & Go ASBL", + "email": "info@accessandgo-abp.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_4ba87e61.json b/Data/prospects/pros_4ba87e61.json new file mode 100644 index 0000000..4a64b2b --- /dev/null +++ b/Data/prospects/pros_4ba87e61.json @@ -0,0 +1,19 @@ +{ + "id": "pros_4ba87e61", + "name": "Sport Inn", + "company": "Sport Inn SPRL", + "email": "sportinn@skynet.com", + "phone": "+32 (0) 67 44 29 79", + "source": "annuaire", + "notes": "magasin de sport", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-11", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_532dfd81.json b/Data/prospects/pros_532dfd81.json new file mode 100644 index 0000000..9a18c30 --- /dev/null +++ b/Data/prospects/pros_532dfd81.json @@ -0,0 +1,22 @@ +{ + "id": "pros_532dfd81", + "name": "Client Freelancer anonyme (côte d'ivoire)", + "company": "Freelancer", + "email": "aucun@aucun.com", + "phone": "", + "source": "Réseaux sociaux", + "notes": "lien : https://www.fr.freelancer.com/projects/website-design/Landing-page-39724630/details", + "status": "Non intéressé", + "tags": [ + "freelancer", + "landing page" + ], + "last_contact": "2025-08-23", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_5a76f6ad.json b/Data/prospects/pros_5a76f6ad.json new file mode 100644 index 0000000..01546b7 --- /dev/null +++ b/Data/prospects/pros_5a76f6ad.json @@ -0,0 +1,19 @@ +{ + "id": "pros_5a76f6ad", + "name": "Ferme Clara", + "company": "Ferme Clara", + "email": "helene.vanco@gmail.com", + "phone": "+32 (0) 67 48 59 60", + "source": "annuaire", + "notes": "Ferme locale", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-11", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_6408b8f9.json b/Data/prospects/pros_6408b8f9.json new file mode 100644 index 0000000..afcc922 --- /dev/null +++ b/Data/prospects/pros_6408b8f9.json @@ -0,0 +1,21 @@ +{ + "id": "pros_6408b8f9", + "name": "Rudy", + "company": "LR Citerne Service", + "email": "rudy.lucas67@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "artisan" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_6c39a6a7.json b/Data/prospects/pros_6c39a6a7.json new file mode 100644 index 0000000..2ba4e05 --- /dev/null +++ b/Data/prospects/pros_6c39a6a7.json @@ -0,0 +1,22 @@ +{ + "id": "pros_6c39a6a7", + "name": "EPIK", + "company": "", + "email": "lemogroupe@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "ressources", + "alimentation" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_70b4ce19.json b/Data/prospects/pros_70b4ce19.json new file mode 100644 index 0000000..dbbb359 --- /dev/null +++ b/Data/prospects/pros_70b4ce19.json @@ -0,0 +1,22 @@ +{ + "id": "pros_70b4ce19", + "name": "Coquins Coquines", + "company": "", + "email": "info@coquins-coquines.net", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "petite enfance", + "creche" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_81e5eda3.json b/Data/prospects/pros_81e5eda3.json new file mode 100644 index 0000000..9690019 --- /dev/null +++ b/Data/prospects/pros_81e5eda3.json @@ -0,0 +1,23 @@ +{ + "id": "pros_81e5eda3", + "name": "Bella Ciao", + "company": "Bella Ciao Pizzeria", + "email": "Benhamouda1900@hotmail.gr", + "phone": "", + "source": "Site web", + "notes": "Soignies", + "status": "Relancé", + "tags": [ + "restauration", + "alimentation", + "pizzeria" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-16", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_84b2b824.json b/Data/prospects/pros_84b2b824.json new file mode 100644 index 0000000..07ed5b9 --- /dev/null +++ b/Data/prospects/pros_84b2b824.json @@ -0,0 +1,19 @@ +{ + "id": "pros_84b2b824", + "name": "Anthony ", + "company": "ToineIndustries", + "email": "anthony.violet@outlook.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [], + "last_contact": "2025-09-20", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-24", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_88356f38.json b/Data/prospects/pros_88356f38.json new file mode 100644 index 0000000..37152db --- /dev/null +++ b/Data/prospects/pros_88356f38.json @@ -0,0 +1,19 @@ +{ + "id": "pros_88356f38", + "name": "DynaRythmique", + "company": "DynaRythmique", + "email": "info@dynarythmique.be", + "phone": "+32 (0) 67 21 70 67", + "source": "annuaire", + "notes": "Cours de danse", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-11", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_893aaaaf.json b/Data/prospects/pros_893aaaaf.json new file mode 100644 index 0000000..ea594a5 --- /dev/null +++ b/Data/prospects/pros_893aaaaf.json @@ -0,0 +1,22 @@ +{ + "id": "pros_893aaaaf", + "name": "Les Apprentis Sages", + "company": "", + "email": "lesapprentis.sages7090@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "petite enfance", + "creche" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_8d36a719.json b/Data/prospects/pros_8d36a719.json new file mode 100644 index 0000000..1f7c1a2 --- /dev/null +++ b/Data/prospects/pros_8d36a719.json @@ -0,0 +1,21 @@ +{ + "id": "pros_8d36a719", + "name": "Anaïs", + "company": "Anaïs Hair Artist", + "email": "siana2812@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "coiffeuse" + ], + "last_contact": "2025-09-06", + "next_action": "relancer le 02/09/2025", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_8f11aab0.json b/Data/prospects/pros_8f11aab0.json new file mode 100644 index 0000000..13884b3 --- /dev/null +++ b/Data/prospects/pros_8f11aab0.json @@ -0,0 +1,24 @@ +{ + "id": "pros_8f11aab0", + "name": "Stéphane", + "company": "SRL Stéphane Delaval", + "email": "stephanedelaval@skynet.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "Artisan", + "locale", + "commerce", + "boutique en ligne" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_92e930a9.json b/Data/prospects/pros_92e930a9.json new file mode 100644 index 0000000..01f18c3 --- /dev/null +++ b/Data/prospects/pros_92e930a9.json @@ -0,0 +1,22 @@ +{ + "id": "pros_92e930a9", + "name": "Verbiest Construct", + "company": "Verbiest Construct SRL", + "email": "Verbiestconstruct@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "toiture", + "entreprise du bâtiment" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_9a4bc976.json b/Data/prospects/pros_9a4bc976.json new file mode 100644 index 0000000..910ccef --- /dev/null +++ b/Data/prospects/pros_9a4bc976.json @@ -0,0 +1,23 @@ +{ + "id": "pros_9a4bc976", + "name": "Ferme du vieux canal", + "company": "Ferme Du Vieux Canal", + "email": "marieetlucdr@skynet.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "ferme", + "agriculture", + "marché" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_9db969b9.json b/Data/prospects/pros_9db969b9.json new file mode 100644 index 0000000..b1102b6 --- /dev/null +++ b/Data/prospects/pros_9db969b9.json @@ -0,0 +1,22 @@ +{ + "id": "pros_9db969b9", + "name": "Geoffrey", + "company": "Geoffrey Fenain", + "email": "geoffroy.fenain@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "medecin", + "psychomotricien" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_a807ad21.json b/Data/prospects/pros_a807ad21.json new file mode 100644 index 0000000..d0815df --- /dev/null +++ b/Data/prospects/pros_a807ad21.json @@ -0,0 +1,19 @@ +{ + "id": "pros_a807ad21", + "name": "JNS Coaching", + "company": "JNS Coaching", + "email": "jns.coaching.nutrition@gmail.com", + "phone": "", + "source": "Site web", + "notes": "probablement freelance ", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_a8e85854.json b/Data/prospects/pros_a8e85854.json new file mode 100644 index 0000000..8c59211 --- /dev/null +++ b/Data/prospects/pros_a8e85854.json @@ -0,0 +1,22 @@ +{ + "id": "pros_a8e85854", + "name": "Aurélie", + "company": "Les Caprices de Juliette ", + "email": "aurelie.verbiest@hotmail.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "ferme", + "agriculture" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-08-23", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_b31e8791.json b/Data/prospects/pros_b31e8791.json new file mode 100644 index 0000000..a1aeae7 --- /dev/null +++ b/Data/prospects/pros_b31e8791.json @@ -0,0 +1,23 @@ +{ + "id": "pros_b31e8791", + "name": "Opticalement Votre", + "company": "Opticalement Votre", + "email": "opticalementvotre@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "Artisan", + "opticien", + "commerce" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_c29ab1cf.json b/Data/prospects/pros_c29ab1cf.json new file mode 100644 index 0000000..aa91380 --- /dev/null +++ b/Data/prospects/pros_c29ab1cf.json @@ -0,0 +1,23 @@ +{ + "id": "pros_c29ab1cf", + "name": "Recrea Braine", + "company": "", + "email": "recreabraine@7090.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "ludique", + "recreatif", + "jeunesse" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_c93b6d00.json b/Data/prospects/pros_c93b6d00.json new file mode 100644 index 0000000..b267550 --- /dev/null +++ b/Data/prospects/pros_c93b6d00.json @@ -0,0 +1,23 @@ +{ + "id": "pros_c93b6d00", + "name": "Sergio", + "company": "Vinicoles", + "email": "sergio.vasquez-perez@vinitales.be", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Contacté", + "tags": [ + "commerce", + "pme", + "locale" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-09-06", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_ca965f43.json b/Data/prospects/pros_ca965f43.json new file mode 100644 index 0000000..ba68b58 --- /dev/null +++ b/Data/prospects/pros_ca965f43.json @@ -0,0 +1,22 @@ +{ + "id": "pros_ca965f43", + "name": "Le chat pitre", + "company": "Le chat pitre", + "email": "infokidsandcoevents@gmail.com", + "phone": "", + "source": "Site web", + "notes": "Soignies", + "status": "Relancé", + "tags": [ + "Librairie", + "commerce" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-16", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_cfb8950b.json b/Data/prospects/pros_cfb8950b.json new file mode 100644 index 0000000..9c96b76 --- /dev/null +++ b/Data/prospects/pros_cfb8950b.json @@ -0,0 +1,19 @@ +{ + "id": "pros_cfb8950b", + "name": "MIDServices", + "company": "MiD Service", + "email": "info@midservices.be", + "phone": "0492/72.16.73", + "source": "annuaire", + "notes": "Titre service ménage", + "status": "Relancé", + "tags": [], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-11", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_d970305c.json b/Data/prospects/pros_d970305c.json new file mode 100644 index 0000000..2b63f12 --- /dev/null +++ b/Data/prospects/pros_d970305c.json @@ -0,0 +1,22 @@ +{ + "id": "pros_d970305c", + "name": "Un peu de tout ", + "company": "Un peu de tout", + "email": "unpeudetout.fromagerie@gmail.com", + "phone": "+32 (0) 67 49 39 60", + "source": "Site web", + "notes": "braine le comte", + "status": "Relancé", + "tags": [ + "alimentation", + "fromagerie" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-06-01", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_e1855f79.json b/Data/prospects/pros_e1855f79.json new file mode 100644 index 0000000..fef0760 --- /dev/null +++ b/Data/prospects/pros_e1855f79.json @@ -0,0 +1,22 @@ +{ + "id": "pros_e1855f79", + "name": "Marjorie Malscalk", + "company": "", + "email": "marjorie-malschalck@hotmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Non intéressé", + "tags": [ + "petite enfance", + "creche", + "independant", + "freelance" + ], + "last_contact": "2025-05-14", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14" +} \ No newline at end of file diff --git a/Data/prospects/pros_e8b7dd8b.json b/Data/prospects/pros_e8b7dd8b.json new file mode 100644 index 0000000..563f537 --- /dev/null +++ b/Data/prospects/pros_e8b7dd8b.json @@ -0,0 +1,21 @@ +{ + "id": "pros_e8b7dd8b", + "name": "Claire Van Eeckenrode", + "company": "ASBL Bel'Air", + "email": "c.vaneeckerode@asbl-belair.be", + "phone": "", + "source": "", + "notes": "", + "status": "Relancé", + "tags": [ + "asbl" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-14", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/prospects/pros_f279cb3e.json b/Data/prospects/pros_f279cb3e.json new file mode 100644 index 0000000..f5cc7e9 --- /dev/null +++ b/Data/prospects/pros_f279cb3e.json @@ -0,0 +1,23 @@ +{ + "id": "pros_f279cb3e", + "name": "Le CAP", + "company": "LE CAP", + "email": "lecape.asbl@gmail.com", + "phone": "", + "source": "Site web", + "notes": "", + "status": "Relancé", + "tags": [ + "enfance", + "aide au devoir", + "jeunesse" + ], + "last_contact": "2025-09-06", + "next_action": "", + "linked_docs": { + "propositions": [] + }, + "created_at": "2025-05-15", + "score": 0, + "last_interaction": null +} \ No newline at end of file diff --git a/Data/tasks/tsk_03615d49.json b/Data/tasks/tsk_03615d49.json new file mode 100644 index 0000000..5b21e54 --- /dev/null +++ b/Data/tasks/tsk_03615d49.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_03615d49", + "title": "Relancer prospects", + "description": "Relancer les prospects qui ont été contactés", + "due_date": "2025-09-06", + "entity_type": "prospect", + "entity_id": null, + "priority": "haute", + "status": "done", + "created_at": "2025-08-23T19:44:46.653800", + "completed_at": "2025-09-06T08:52:31.559975", + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_0ff23eb4.json b/Data/tasks/tsk_0ff23eb4.json new file mode 100644 index 0000000..4dc56cb --- /dev/null +++ b/Data/tasks/tsk_0ff23eb4.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_0ff23eb4", + "title": "Relance prospects", + "description": "relance de prospects. Eviter les plus anciens", + "due_date": "2025-10-04", + "entity_type": "prospect", + "entity_id": null, + "priority": "normale", + "status": "todo", + "created_at": "2025-09-20T09:17:28.542168", + "completed_at": null, + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_319ed20a.json b/Data/tasks/tsk_319ed20a.json new file mode 100644 index 0000000..5523eb4 --- /dev/null +++ b/Data/tasks/tsk_319ed20a.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_319ed20a", + "title": "Reprise de contact", + "description": "Reprendre contact avec Laurent de Art Tech Beton", + "due_date": "2025-09-25", + "entity_type": "prospect", + "entity_id": "pros_38947d7a", + "priority": "haute", + "status": "done", + "created_at": "2025-08-24T09:42:29.386835", + "completed_at": "2025-09-20T09:14:53.369293", + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_8b1b3aa5.json b/Data/tasks/tsk_8b1b3aa5.json new file mode 100644 index 0000000..a6ae697 --- /dev/null +++ b/Data/tasks/tsk_8b1b3aa5.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_8b1b3aa5", + "title": "Freelancer", + "description": "Vérifier si j’ai été sélectionné", + "due_date": "2025-08-25", + "entity_type": "prospect", + "entity_id": "pros_532dfd81", + "priority": "normale", + "status": "done", + "created_at": "2025-08-24T09:45:02.121319", + "completed_at": "2025-08-25T06:03:41.906834", + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_ef6c7f63.json b/Data/tasks/tsk_ef6c7f63.json new file mode 100644 index 0000000..6a23342 --- /dev/null +++ b/Data/tasks/tsk_ef6c7f63.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_ef6c7f63", + "title": "Vérifié prospects Freelancer", + "description": "Vérifier si les prospects freelancer ont été validés ou envoyés.", + "due_date": "2025-08-24", + "entity_type": "prospect", + "entity_id": null, + "priority": "normale", + "status": "done", + "created_at": "2025-08-23T20:53:30.774681", + "completed_at": "2025-08-24T08:16:29.140029", + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_f3ef47f8.json b/Data/tasks/tsk_f3ef47f8.json new file mode 100644 index 0000000..19b7cc9 --- /dev/null +++ b/Data/tasks/tsk_f3ef47f8.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_f3ef47f8", + "title": "Relance nouveau prospects", + "description": "Relancer les nouveau prospects", + "due_date": "2025-09-20", + "entity_type": "prospect", + "entity_id": null, + "priority": "normale", + "status": "done", + "created_at": "2025-09-06T09:19:45.258402", + "completed_at": "2025-09-20T09:16:35.343048", + "metadata": {} +} \ No newline at end of file diff --git a/Data/tasks/tsk_fc8e9bdf.json b/Data/tasks/tsk_fc8e9bdf.json new file mode 100644 index 0000000..6c66427 --- /dev/null +++ b/Data/tasks/tsk_fc8e9bdf.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_fc8e9bdf", + "title": "Freelancer", + "description": "Vérifier si j’ai été sélectionné", + "due_date": "2025-09-02", + "entity_type": "prospect", + "entity_id": "pros_27cfe29d", + "priority": "normale", + "status": "done", + "created_at": "2025-08-24T09:45:33.914886", + "completed_at": "2025-08-25T06:03:21.372698", + "metadata": {} +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c4f212 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Suite Consultance + +Une suite logicielle complète pour la gestion de cabinet de conseil, comprenant : +- CRM (Gestion des clients et prospects) +- Générateur de propositions commerciales +- Générateur de devis +- Gestion des documents + +## Installation + +Pour installer Suite Consultance et ses dépendances, exécutez : + +```bash +python install.py +``` + +Ce script va : +1. Vérifier que votre environnement est compatible +2. Mettre à jour pip +3. Installer toutes les dépendances nécessaires +4. Créer les répertoires requis + +## Lancement + +Pour lancer l'application, exécutez : + +```bash +python run.py +``` + +Par défaut, l'application utilise l'interface graphique Tkinter. + +Pour supprimer l'avertissement de dépréciation de Tk sur macOS, utilisez : + +```bash +python silent_run.py +``` + +### Options de lancement + +- Mode console (CLI) : +```bash +python run.py --cli +``` +## Fonctionnalités + +### CRM +- Gestion complète des clients +- Gestion des prospects avec suivi de statut +- Conversion de prospects en clients +- Accès aux documents liés + +### Communication +- Envoi d'emails aux prospects (individuellement ou en masse) +- Templates d'emails personnalisables +- Suivi des communications avec historique +- Mise à jour automatique du statut des prospects + +### Propositions commerciales +- Création de propositions personnalisées +- Modèles prédéfinis +- Export en PDF + +### Devis +- Génération de devis professionnels +- Calcul automatique des totaux +- Export en PDF + +## Structure des fichiers + +``` +SuiteConsultance/ +├── main.py # Point d'entrée principal (mode CLI) +├── run.py # Script de lancement +├── install.py # Script d'installation +├── requirements.txt # Dépendances +├── core/ # Fonctionnalités de base +├── Data/ # Données des clients et prospects +├── modules/ # Modules fonctionnels +│ ├── crm/ # Gestion de la relation client +│ ├── devis/ # Génération de devis +│ ├── email/ # Gestion des emails +│ └── proposition/ # Génération de propositions +├── output/ # Documents générés +└── Templates/ # Modèles de documents +``` + +## Dépendances + +- Python 3.7+ +- FPDF +- Python-dateutil +- Pillow +- Flask (pour l'interface web) +- smtplib (pour l'envoi d'emails) + +## Notes + +Pour les utilisateurs de macOS : Si vous rencontrez des problèmes d'interface graphique, assurez-vous que votre version de Tk est 8.6 ou supérieure. diff --git a/Suite Consultance.spec b/Suite Consultance.spec new file mode 100644 index 0000000..73fc6b9 --- /dev/null +++ b/Suite Consultance.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + datas=[('static', 'static'), ('Templates', 'Templates')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='Suite Consultance', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icon.icns'], +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='Suite Consultance', +) +app = BUNDLE( + coll, + name='Suite Consultance.app', + icon='icon.icns', + bundle_identifier='org.toine.suiteconsultance', +) diff --git a/Suivi Consultance.spec b/Suivi Consultance.spec new file mode 100644 index 0000000..204e064 --- /dev/null +++ b/Suivi Consultance.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + datas=[('static', 'static'), ('templates', 'templates')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='Suivi Consultance', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icon.icns'], +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='Suivi Consultance', +) +app = BUNDLE( + coll, + name='Suivi Consultance.app', + icon='icon.icns', + bundle_identifier='org.toine.monapp', +) diff --git a/Templates/crm/add_client.html b/Templates/crm/add_client.html new file mode 100644 index 0000000..61afbcf --- /dev/null +++ b/Templates/crm/add_client.html @@ -0,0 +1,136 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Ajouter un client{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Ajouter un nouveau client

+ + Retour au CRM + +
+ +
+
+
+
+ +
+
Informations client
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Informations projet
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Informations complémentaires
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} diff --git a/Templates/crm/add_prospect.html b/Templates/crm/add_prospect.html new file mode 100644 index 0000000..c7f9d60 --- /dev/null +++ b/Templates/crm/add_prospect.html @@ -0,0 +1,129 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Ajouter un prospect{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Ajouter un nouveau prospect

+ + Retour au CRM + +
+ +
+
+
+
+ +
+
Informations de base
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Qualification
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Informations supplémentaires
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} diff --git a/Templates/crm/client_details.html b/Templates/crm/client_details.html new file mode 100644 index 0000000..0483423 --- /dev/null +++ b/Templates/crm/client_details.html @@ -0,0 +1,183 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Détails du client{% endblock %} + +{% block content %} +
+

Détails du client: {{ client.client_name }}

+
+ + Retour au CRM + + +
+
+ +
+
+ +
+
+
Informations du client
+
+
+
+
+
Nom
+

{{ client.client_name }}

+
+
+
Email
+

{{ client.email or 'Non spécifié' }}

+
+
+
Téléphone
+

{{ client.telephone or 'Non spécifié' }}

+
+
+
Adresse
+

{{ client.adresse or 'Non spécifiée' }}

+
+
+
+
+ + +
+
+
Détails du projet
+
+
+
+
+
Nom du projet
+

{{ client.project_name }}

+
+
+
Type de projet
+

{{ client.project_type or 'Non spécifié' }}

+
+
+
Date limite
+

{{ client.deadline or 'Non spécifiée' }}

+
+
+
Budget
+

{{ client.budget or 'Non spécifié' }}

+
+
+ +
+
Description du projet
+

{{ client.project_description or 'Aucune description disponible' }}

+
+ +
+
Fonctionnalités
+ {% if client.features %} +
    + {% for feature in client.features %} +
  • {{ feature.description }}
  • + {% endfor %} +
+ {% else %} +

Aucune fonctionnalité spécifiée

+ {% endif %} +
+ +
+
+
Conditions de paiement
+

{{ client.payment_terms or 'Non spécifiées' }}

+
+
+
Informations de contact
+

{{ client.contact_info or 'Non spécifiées' }}

+
+
+ +
+
Informations additionnelles
+

{{ client.additional_info or 'Aucune information additionnelle' }}

+
+
+
+
+ +
+ +
+
+
Documents liés
+
+
+
+
Propositions
+
    + {% if client.propositions %} + {% for proposition in client.propositions %} +
  • + {{ proposition.name }} + + + +
  • + {% endfor %} + {% else %} +
  • Aucune proposition
  • + {% endif %} +
+
+ +
+
Devis
+
    + {% if client.devis %} + {% for devis in client.devis %} +
  • + {{ devis.name }} + + + +
  • + {% endfor %} + {% else %} +
  • Aucun devis
  • + {% endif %} +
+
+
+
+ + +
+
+
Actions
+
+
+ +
+
+
+
+{% endblock %} diff --git a/Templates/crm/crm.html b/Templates/crm/crm.html new file mode 100644 index 0000000..369724c --- /dev/null +++ b/Templates/crm/crm.html @@ -0,0 +1,283 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - CRM{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Gestion de la Relation Client (CRM)

+ + Ajouter un client + +
+ + + +
+ +
+
+
+
+ + + + + + + + + + + + {% if clients %} + {% for client in clients %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NomEmailTéléphoneProjetActions
{{ client.name }}{{ client.email }}{{ client.phone }}{{ client.company }} + +
Aucun client trouvé
+
+
+
+
+ + +
+
+
+ + +
+ + + + + + + + + + + + {% if prospects %} + {% for prospect in prospects %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NomEmailTéléphoneStatutActions
{{ prospect.name }}{{ prospect.email }}{{ prospect.phone }} + + {{ prospect.status }} + + + +
Aucun prospect trouvé
+
+
+
+
+ + +
+
+
+
+
+
Répartition clients/prospects
+
+
+ +
+
+
{{ dashboard_stats.total_clients }}
+
Total Clients
+
+
+
{{ dashboard_stats.total_prospects }}
+
Total Prospects
+
+
+
+
+
+ +
+
+
+
Statut des prospects
+
+
+ +
+ {% for status, count in dashboard_stats.prospect_status.items() %} +
+
{{ count }}
+
{{ status }}
+
+ {% endfor %} +
+
+
+
+
+
+
+ + +{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/Templates/crm/edit_prospect.html b/Templates/crm/edit_prospect.html new file mode 100644 index 0000000..046372d --- /dev/null +++ b/Templates/crm/edit_prospect.html @@ -0,0 +1,132 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Modifier un prospect{% endblock %} + +{% block content %} +
+

Modifier le prospect: {{ prospect.name }}

+ + Retour aux détails + +
+ +
+
+
+
+ +
+
Informations de base
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Qualification
+
+ +
+
+ + +
+
+ +
+
+ + {# Compatibilité enum/chaîne pour le statut #} + {% set s = prospect.status %} + {% if not (s is string) %}{% set s = s.value %}{% endif %} + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Informations supplémentaires
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} diff --git a/Templates/crm/prospect_details.html b/Templates/crm/prospect_details.html new file mode 100644 index 0000000..959a0bc --- /dev/null +++ b/Templates/crm/prospect_details.html @@ -0,0 +1,199 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Détails du prospect{% endblock %} + +{% block content %} +
+

Détails du prospect: {{ prospect.name }}

+
+ + Retour au CRM + +
+ + Modifier + + + Convertir en client + + {% with entity_type='prospect', entity_id=prospect.id %} + {% include "partials/task_quick_add_button.html" %} + {% endwith %} +
+
+
+ +
+
+ +
+
+
Informations du prospect
+
+
+
+
+
Nom
+

{{ prospect.name }}

+
+
+
Entreprise
+

{{ prospect.company or 'Non spécifiée' }}

+
+
+
Email
+

{{ prospect.email or 'Non spécifié' }}

+
+
+
Téléphone
+

{{ prospect.phone or 'Non spécifié' }}

+
+
+ +
+
+
Source
+

{{ prospect.source or 'Non spécifiée' }}

+
+
+
Statut
+

+ + {{ prospect.status }} + +

+
+
+ +
+
+
Date de création
+

{{ prospect.created_at }}

+
+
+
Dernier contact
+

{{ prospect.last_contact }}

+
+
+ +
+
Tags
+ {% if prospect.tags %} + {% for tag in prospect.tags %} + {{ tag }} + {% endfor %} + {% else %} +

Aucun tag

+ {% endif %} +
+
+
+ + +
+
+
Notes et actions
+
+
+
+
Notes
+

{{ prospect.notes or 'Aucune note disponible' }}

+
+ +
+
Action suivante
+

{{ prospect.next_action or 'Aucune action prévue' }}

+
+
+
+
+ +
+ +
+
+
Documents liés
+
+
+
+
Propositions
+
    + {% if prospect.linked_docs and prospect.linked_docs.propositions %} + {% for proposition in prospect.linked_docs.propositions %} +
  • + {{ proposition.name }} + + + +
  • + {% endfor %} + {% else %} +
  • Aucune proposition
  • + {% endif %} +
+
+
+
+ + +
+
+
Derniers emails
+
+
+ {% if email_history and email_history|length > 0 %} +
    + {% for email in email_history[:5] %} +
  • +
    + {{ email.subject }} +
    + {{ email.timestamp|datetime }} +
    + + {% if email.success %}Envoyé{% else %}Échec{% endif %} + +
  • + {% endfor %} +
+ {% if email_history|length > 5 %} + + {% endif %} + {% else %} +

Aucun email envoyé à ce prospect.

+ {% endif %} +
+
+ + + +
+
+{% endblock %} diff --git a/Templates/devis.json b/Templates/devis.json new file mode 100644 index 0000000..0f69ed3 --- /dev/null +++ b/Templates/devis.json @@ -0,0 +1,14 @@ +{ + "title": "Devis de service", + "numero": "{numero}", + "date": "{date}", + "client": { + "nom": "{client_name}", + "email": "{client_email}", + "telephone": "{client_phone}", + "adresse": "{client_adress}" + }, + "services": "{services}", + "paiemnt_terms": "{payment_terms}", + "total": "{total}" +} \ No newline at end of file diff --git a/Templates/devis/create_devis.html b/Templates/devis/create_devis.html new file mode 100644 index 0000000..d6cbad7 --- /dev/null +++ b/Templates/devis/create_devis.html @@ -0,0 +1,218 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Créer un devis{% endblock %} +{% block module_name %}devis{% endblock %} + +{% block content %} +
+

Créer un devis

+ + Retour aux devis + +
+ +
+
+
+
+ +
+
Sélection du client
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Détails du devis
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
Prestations
+ +
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+ Total HT: + 0.00 € +
+
+ TVA (20%): + 0.00 € +
+
+ Total TTC: + 0.00 € +
+
+
+
+
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/devis/devis.html b/Templates/devis/devis.html new file mode 100644 index 0000000..4237059 --- /dev/null +++ b/Templates/devis/devis.html @@ -0,0 +1,61 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Devis{% endblock %} +{% block module_name %}devis{% endblock %} + +{% block content %} +
+

Devis clients

+ + Nouveau devis + +
+ +
+
+
+ + + + + + + + + + + {% if devis %} + {% for d in devis %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
ClientDate de créationFichierActions
{{ d.client_name }}{{ d.date }}{{ d.filename }} +
+ + + + + + +
+
Aucun devis trouvé
+
+
+
+{% endblock %} diff --git a/Templates/email/bulk_email.html b/Templates/email/bulk_email.html new file mode 100644 index 0000000..42ffbd2 --- /dev/null +++ b/Templates/email/bulk_email.html @@ -0,0 +1,323 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Campagne d'email{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Campagne d'email aux prospects

+ + Retour au CRM + +
+ +
+
+
+
+ +
+
Sélection des prospects
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + + Séparer les tags par des virgules +
+
+ +
+
+ + + + + + + + + + + + {% for prospect in prospects %} + {# Convertit le statut en chaîne si enum #} + {% set s = prospect.status %} + {% if not (s is string) %}{% set s = s.value %}{% endif %} + + + + + + + + {% endfor %} + +
+
+ +
+
NomEmailStatutTags
+
+ +
+
{{ prospect.name }}{{ prospect.email }} + {# Mapping badge #} + {% set cls = 'secondary' %} + {% if s|lower == 'nouveau' %} + {% set cls = 'primary' %} + {% elif s|lower == 'contacté' or s|lower == 'contacte' %} + {% set cls = 'info' %} + {% elif s|lower == 'qualifié' or s|lower == 'qualifie' %} + {% set cls = 'success' %} + {% elif s|lower == 'proposition' %} + {% set cls = 'warning' %} + {% endif %} + {{ s }} + + {% for tag in prospect.tags %} + {{ tag }} + {% endfor %} +
+
+
+ + +
+
Contenu de l'email
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + + Variables disponibles: {{nom}}, {{entreprise}}, {{email}}. Vous pouvez utiliser du texte formaté HTML. +
+
+ +
+
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/email/config.html b/Templates/email/config.html new file mode 100644 index 0000000..dc81ac9 --- /dev/null +++ b/Templates/email/config.html @@ -0,0 +1,141 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Configuration Email{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Configuration Email

+ + Retour au CRM + +
+ +
+
+
+
+ +
+
Configuration SMTP
+
+ +
+
+ + + Exemple: smtp.gmail.com +
+
+ +
+
+ + + Exemple: 587 (TLS) ou 465 (SSL) +
+
+ + +
+
Authentification
+
+ +
+
+ + +
+
+ +
+
+ + + Pour Gmail, utilisez un "mot de passe d'application" +
+
+ + +
+
Informations d'expéditeur
+
+ +
+
+ + +
+
+ +
+
+ + + Doit généralement correspondre à l'adresse email d'authentification +
+
+ +
+ + + Annuler + + +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/email/edit_template.html b/Templates/email/edit_template.html new file mode 100644 index 0000000..480e3c1 --- /dev/null +++ b/Templates/email/edit_template.html @@ -0,0 +1,128 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - {% if template %}Modifier{% else %}Créer{% endif %} un template d'email{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

{% if template %}Modifier{% else %}Créer{% endif %} un template d'email

+ + Retour aux templates + +
+ +
+
+
+
+ +
+
Informations du template
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Contenu du template
+
+ +
+
+ + +
+
+ +
+
+ + + Variables disponibles: {{name}}, {{company}}, {{email}}, {{phone}}. Vous pouvez utiliser du texte formaté HTML. +
+
+ +
+ + + Annuler + +
+
+
+
+
+ + +
+
+
Aperçu du template
+
+
+
+
Sujet
+

{{ template.subject if template else 'Votre sujet apparaîtra ici' }}

+
+
+
Contenu
+
+ {{ template.content|safe if template else '

Votre contenu apparaîtra ici

' }} +
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/email/email_history.html b/Templates/email/email_history.html new file mode 100644 index 0000000..2a88528 --- /dev/null +++ b/Templates/email/email_history.html @@ -0,0 +1,112 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Historique des emails{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Historique des emails de {{ prospect.name }}

+ + Retour au prospect + +
+ +
+
+ {% if emails %} +
+ + + + + + + + + + + {% for email in emails %} + + + + + + + {% endfor %} + +
DateSujetStatutActions
{{ email.timestamp|datetime }}{{ email.subject }} + {% if email.success %} + Envoyé + {% else %} + Échec + {% endif %} + + +
+
+ {% else %} +
+ Aucun email n'a été envoyé à ce prospect. +
+ {% endif %} +
+
+ + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/email/new_scraping.html b/Templates/email/new_scraping.html new file mode 100644 index 0000000..af7f49f --- /dev/null +++ b/Templates/email/new_scraping.html @@ -0,0 +1,96 @@ +{% extends "layouts/base.html" %} + +{% block title %}Nouveau scraping d'emails{% endblock %} + +{% block content %} +
+
+
+
+
+
+ Nouveau scraping d'emails +
+
+
+
+
+ + +
+ Entrez l'URL complète du site web à analyser pour extraire les adresses email. +
+
+ +
+
+
+ + +
+ Nombre de pages à analyser avec gestion automatique de la pagination. +
+
+
+
+
+
+
+
+ +
+
+ + +
+ Les contacts trouvés seront automatiquement ajoutés comme nouveaux prospects. +
+
+
+ +
+ + Informations importantes : +
    +
  • Le scrappeur analyse les pages avec gestion automatique de la pagination
  • +
  • Spécialement conçu pour les annuaires d'entreprises et pages de résultats
  • +
  • Extraction automatique des données : nom, entreprise, email, téléphone, localité
  • +
  • Un délai de 2 secondes est appliqué entre chaque page pour éviter la surcharge
  • +
  • Respectez les politiques du site web et les conditions d'utilisation
  • +
+
+ +
+ + Retour + + +
+ +
+ + + + + + +{% endblock %} diff --git a/Templates/email/scraper.html b/Templates/email/scraper.html new file mode 100644 index 0000000..5f4c366 --- /dev/null +++ b/Templates/email/scraper.html @@ -0,0 +1,91 @@ +{% extends "layouts/base.html" %} + +{% block title %}Scrapping d'emails{% endblock %} + +{% block content %} +
+
+
+
+

Scrapping d'emails

+ + Nouveau scraping + +
+ + {% if scrapings %} +
+
+
Historique des scrapings
+
+
+
+ + + + + + + + + + + + + {% for scraping in scrapings %} + + + + + + + + + {% endfor %} + +
URLEmails trouvésPages scrapéesDateStatutActions
+ + {{ scraping.url[:50] }}{% if scraping.url|length > 50 %}...{% endif %} + + + {{ scraping.emails_count }} + + {{ scraping.pages_count }} + + {{ scraping.start_time | datetime('%d/%m/%Y %H:%M') }} + + {% if scraping.errors_count > 0 %} + {{ scraping.errors_count }} erreur(s) + {% else %} + Succès + {% endif %} + + +
+
+
+
+ {% else %} +
+ +

Aucun scraping d'email effectué

+

Commencez par créer votre premier scraping d'emails.

+ + Nouveau scraping + +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/Templates/email/scraping_results.html b/Templates/email/scraping_results.html new file mode 100644 index 0000000..a203498 --- /dev/null +++ b/Templates/email/scraping_results.html @@ -0,0 +1,289 @@ +{% extends "layouts/base.html" %} + +{% block title %}Résultats du scraping{% endblock %} + +{% block content %} +
+
+
+
+

Résultats du scraping

+ + Retour + +
+ + +
+
+
+
+

{{ results.contacts|length }}

+

Contacts trouvés

+
+
+
+
+
+
+

{{ results.pages_scraped|length }}

+

Pages scrapées

+
+
+
+
+
+
+

{{ results.errors|length }}

+

Erreurs

+
+
+
+
+
+
+ Durée +

+ {% if results.start_time and results.end_time %} + {% set start = results.start_time | parse_datetime %} + {% set end = results.end_time | parse_datetime %} + {{ ((end - start).total_seconds() / 60) | round(1) }} min + {% else %} + N/A + {% endif %} +

+
+
+
+
+ + +
+
+
URL source
+
+ +
+ + + {% if results.contacts %} +
+
+
Contacts trouvés ({{ results.contacts|length }})
+
+ + +
+
+
+
+ + + + + + + + + + + + + {% for contact in results.contacts %} + + + + + + + + + {% endfor %} + +
EmailNomEntrepriseTéléphoneLocalité
+
+ +
+
+ {{ contact.email }} + + {% if contact.first_name or contact.last_name %} + {{ contact.first_name }} {{ contact.last_name }} + {% elif contact.name %} + {{ contact.name }} + {% else %} + - + {% endif %} + + {% if contact.company %} + {{ contact.company }} + {% else %} + - + {% endif %} + + {% if contact.phone %} + {{ contact.phone }} + {% else %} + - + {% endif %} + + {% if contact.location %} + {{ contact.location }} + {% else %} + - + {% endif %} +
+
+
+
+ {% endif %} + + +
+
+
Pages scrapées ({{ results.pages_scraped|length }})
+
+
+
+ + + + + + + + + + + + {% for page in results.pages_scraped %} + + + + + + + + {% endfor %} + +
URLNiveauContacts trouvésStatutHeure
+ + {{ page.url[:60] }}{% if page.url|length > 60 %}...{% endif %} + + + {{ page.depth }} + + {% if page.contacts_found %} + {{ page.contacts_found }} + {% if page.contacts and page.contacts|length > 0 %} + + {% for contact in page.contacts[:3] %} + {{ contact.email }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% if page.contacts|length > 3 %}...{% endif %} + + {% endif %} + {% else %} + 0 + {% endif %} + + {% if page.status == 'success' %} + Succès + {% else %} + Erreur + {% if page.error %} + {{ page.error }} + {% endif %} + {% endif %} + + + {{ page.timestamp | datetime('%H:%M:%S') }} + +
+
+
+
+ + + {% if results.errors %} +
+
+
Erreurs ({{ results.errors|length }})
+
+
+ {% for error in results.errors %} +
{{ error }}
+ {% endfor %} +
+
+ {% endif %} +
+
+
+ + +{% endblock %} diff --git a/Templates/email/send_email.html b/Templates/email/send_email.html new file mode 100644 index 0000000..fbef3fd --- /dev/null +++ b/Templates/email/send_email.html @@ -0,0 +1,155 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Envoyer un email{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Envoyer un email à {{ prospect.name }}

+ + Retour au prospect + +
+ +
+
+
+
+ +
+
Destinataire
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Contenu de l'email
+
+ +
+
+ + +
+
+ +
+
+ + + Vous pouvez utiliser du texte formaté HTML. +
+
+ +
+
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/email/templates.html b/Templates/email/templates.html new file mode 100644 index 0000000..f8265ec --- /dev/null +++ b/Templates/email/templates.html @@ -0,0 +1,60 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Templates d'email{% endblock %} +{% block module_name %}crm{% endblock %} + +{% block content %} +
+

Templates d'email

+
+ + Retour au CRM + + + Créer un template + +
+
+ +
+
+
+ + + + + + + + + + + {% if templates %} + {% for template in templates %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NomSujetDescriptionActions
{{ template.name }}{{ template.subject }}{{ template.description }} + +
Aucun template trouvé
+
+
+
+{% endblock %} diff --git a/Templates/index.html b/Templates/index.html new file mode 100644 index 0000000..4b762a3 --- /dev/null +++ b/Templates/index.html @@ -0,0 +1,172 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Accueil{% endblock %} + +{% block content %} +
+

Bienvenue sur Suite Consultance

+

Solution complète pour la gestion de client et de prospects

+
+

Gérer des clients, créer des propositions commerciales et générer des devis en quelques clics.

+ +
+
+
+
+ +

CRM

+

Gérer mes clients et prospects efficacement.

+ Accéder au CRM +
+
+
+ +
+
+
+ +

Propositions

+

Créer des propositions commerciales personnalisées.

+ Gérer les propositions +
+
+
+ +
+
+
+ +

Devis

+

Générer et suivre mes devis clients.

+ Gérer les devis +
+
+
+
+ + +
+
+
+
+
Dernières activités
+
+
+
    + {% if recent_documents %} + {% for doc in recent_documents %} +
  • +
    + + {{ doc.type }} pour {{ doc.client }} +
    + {{ doc.date|datetime('%d/%m/%Y') }} +
  • + {% endfor %} + {% else %} +
  • + Aucune activité récente +
  • + {% endif %} +
+
+
+
+ +
+
+
+
Statistiques
+
+
+
+
+

{{ clients_count }}

+

Clients

+
+
+

{{ prospects_count }}

+

Prospects

+
+
+

{{ propositions_count }}

+

Propositions

+
+
+

{{ devis_count }}

+

Devis

+
+
+ + {% if prospect_status %} +
Statuts des prospects
+
+ {% for status, count in prospect_status.items() %} +
+ {{ status }} +
{{ count }}
+
+ {% endfor %} +
+ {% endif %} +
+
+
+
+ +
+
+
+
+
Prospects récents
+
+
+
+ + + + + + + + + + + + + {% if recent_prospects %} + {% for prospect in recent_prospects %} + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NomEntrepriseEmailStatutDernier contactActions
{{ prospect.name }}{{ prospect.company }}{{ prospect.email }} + + {{ prospect.status }} + + {{ prospect.last_contact|datetime('%d/%m/%Y') if prospect.last_contact else 'N/A' }} + + + +
Aucun prospect récent
+
+
+
+
+
+{% endblock %} diff --git a/Templates/layouts/base.html b/Templates/layouts/base.html new file mode 100644 index 0000000..df084ef --- /dev/null +++ b/Templates/layouts/base.html @@ -0,0 +1,324 @@ + + + + + + {% block title %}Suite Consultance{% endblock %} + + + + + + + + {% block extra_css %}{% endblock %} + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
+ + +
+
+
+ {% block content %}{% endblock %} +
+
+
+ + + + + {% include 'partials/tasks_sidebar.html' %} + {% include 'partials/task_create_modal.html' %} + + + + + + + + + + + + {% block extra_js %}{% endblock %} + + diff --git a/Templates/partials/task_create_modal.html b/Templates/partials/task_create_modal.html new file mode 100644 index 0000000..92a3301 --- /dev/null +++ b/Templates/partials/task_create_modal.html @@ -0,0 +1,54 @@ + diff --git a/Templates/partials/task_quick_add_button.html b/Templates/partials/task_quick_add_button.html new file mode 100644 index 0000000..ad61e5b --- /dev/null +++ b/Templates/partials/task_quick_add_button.html @@ -0,0 +1,14 @@ +{# Bouton réutilisable pour créer une tâche #} +{# Il lie automatiquement si entity_type et entity_id sont disponibles dans le contexte #} +{% set et = (entity_type | default(request.args.get('entity_type'))) %} +{% set eid = (entity_id | default(request.args.get('entity_id'))) %} + +{% if et and eid %} + + Ajouter une tâche + +{% else %} + + Nouvelle tâche + +{% endif %} diff --git a/Templates/partials/task_quick_add_form.html b/Templates/partials/task_quick_add_form.html new file mode 100644 index 0000000..d334dd5 --- /dev/null +++ b/Templates/partials/task_quick_add_form.html @@ -0,0 +1,31 @@ +
+ + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
diff --git a/Templates/partials/tasks_list.html b/Templates/partials/tasks_list.html new file mode 100644 index 0000000..878f1da --- /dev/null +++ b/Templates/partials/tasks_list.html @@ -0,0 +1,23 @@ +{} +{# Partial: affiche une table de tâches à partir de la variable `tasks` #} +{% import "tasks/macros.html" as tsk %} +
+
+

Tâches

+
+ Total: {{ tasks|length if tasks else 0 }} +
+
+ {{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }} +
+{# Partial: affiche une table de tâches à partir de la variable `tasks` #} +{% import "tasks/macros.html" as tsk %} +
+
+

Tâches

+
+ Total: {{ tasks|length if tasks else 0 }} +
+
+ {{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }} +
\ No newline at end of file diff --git a/Templates/partials/tasks_sidebar.html b/Templates/partials/tasks_sidebar.html new file mode 100644 index 0000000..de877b9 --- /dev/null +++ b/Templates/partials/tasks_sidebar.html @@ -0,0 +1,74 @@ +{# Sidebar offcanvas gauche pour afficher les tâches du jour #} +
+
+
+ Tâches +
+ + + +
+
+ +
+
+
+
Chargement des tâches...
+
+
+
+
+
Chargement des tâches...
+
+
+
+
+ +
+ diff --git a/Templates/partials/tasks_today_block.html b/Templates/partials/tasks_today_block.html new file mode 100644 index 0000000..a8da17d --- /dev/null +++ b/Templates/partials/tasks_today_block.html @@ -0,0 +1,11 @@ +{# Partial: bloc des tâches du jour, attend `today_tasks` dans le contexte #} +{% import "tasks/macros.html" as tsk %} +
+
+

Tâches du jour

+
+ {{ today_tasks|length if today_tasks else 0 }} tâche(s) +
+
+ {{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }} +
\ No newline at end of file diff --git a/Templates/projects/all_projects.html b/Templates/projects/all_projects.html new file mode 100644 index 0000000..d7764eb --- /dev/null +++ b/Templates/projects/all_projects.html @@ -0,0 +1,83 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Tous les projets{% endblock %} + +{% block content %} +
+

Projets

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +{% if projects and projects|length > 0 %} +
+ + + + + + + + + + + + + + {% for p in projects %} + + + + + + + + + + {% endfor %} + +
ProjetClientStatutDébutFinBudgetActions
{{ p.name }}{{ p.client_name }}{{ p.status }}{{ p.start_date or '—' }}{{ p.end_date or '—' }} + {% if p.budget is not none %} + {{ "%.2f"|format(p.budget) }} € + {% else %} — {% endif %} + + + + + + + +
+ +
+
+
+{% else %} +
+ Aucun projet trouvé avec ces critères. +
+{% endif %} +{% endblock %} diff --git a/Templates/projects/edit_project.html b/Templates/projects/edit_project.html new file mode 100644 index 0000000..9f55c70 --- /dev/null +++ b/Templates/projects/edit_project.html @@ -0,0 +1,57 @@ +{% extends 'layouts/base.html' %} + +{% block title %}{% if project %}Éditer le projet{% else %}Nouveau projet{% endif %} - {{ client_name }}{% endblock %} + +{% block content %} +
+

{% if project %}Éditer le projet{% else %}Nouveau projet{% endif %} - {{ client_name }}

+
+ + Retour aux projets + +
+
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+{% endblock %} diff --git a/Templates/projects/list_projects.html b/Templates/projects/list_projects.html new file mode 100644 index 0000000..ab74f3a --- /dev/null +++ b/Templates/projects/list_projects.html @@ -0,0 +1,68 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Projets - {{ client_name }}{% endblock %} + +{% block content %} +
+

Projets de {{ client_name }}

+
+ + Détails client + + + Nouveau projet + +
+
+ +{% if projects and projects|length > 0 %} +
+ + + + + + + + + + + + + {% for project in projects %} + + + + + + + + + {% endfor %} + +
NomStatutDébutFinBudgetActions
{{ project.name }}{{ project.status }}{{ project.start_date or '—' }}{{ project.end_date or '—' }} + {% if project.budget is not none %} + {{ "%.2f"|format(project.budget) }} € + {% else %} + — + {% endif %} + + + + + + + +
+ +
+
+
+{% else %} +
+ Aucun projet pour ce client. Créez-en un avec le bouton "Nouveau projet". +
+{% endif %} +{% endblock %} diff --git a/Templates/projects/project_details.html b/Templates/projects/project_details.html new file mode 100644 index 0000000..9ce4a52 --- /dev/null +++ b/Templates/projects/project_details.html @@ -0,0 +1,47 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Projet {{ project.name }} - {{ client_name }}{% endblock %} + +{% block content %} +
+

Projet: {{ project.name }}

+
+ + Retour aux projets + + + Éditer + +
+
+ +
+
+
+
Statut
+
{{ project.status }}
+ +
Date de début
+
{{ project.start_date or '—' }}
+ +
Date de fin
+
{{ project.end_date or '—' }}
+ +
Budget
+
+ {% if project.budget is not none %} + {{ "%.2f"|format(project.budget) }} € + {% else %} + — + {% endif %} +
+ +
Description
+
{{ project.description or '—' }}
+ +
ID du projet
+
{{ project.id }}
+
+
+
+{% endblock %} diff --git a/Templates/proposition_commercial.json b/Templates/proposition_commercial.json new file mode 100644 index 0000000..01542b6 --- /dev/null +++ b/Templates/proposition_commercial.json @@ -0,0 +1,37 @@ +{ + "title": "Proposition de service", + "sections": [ + { + "header": "Informations générales", + "content": [ + "Client : {client_name}", + "Projet : {project_name}", + "Type : {project_type}", + "Date limite : {deadline}" + ] + }, + { + "header": "Description du projet", + "content": ["{project_description}"] + }, + { + "header": "Fonctionnalités demandées", + "content": "{features}" + }, + { + "header": "Budget", + "content": ["Montant estimé : {budget}"] + }, + { + "header": "Modalités", + "content": [ + "Paiement : {payment_terms}", + "Contact : {contact_info}" + ] + }, + { + "header": "Informations complémentaires", + "content": ["{additional_info}"] + } + ] +} \ No newline at end of file diff --git a/Templates/propositions/create_proposition.html b/Templates/propositions/create_proposition.html new file mode 100644 index 0000000..bd76693 --- /dev/null +++ b/Templates/propositions/create_proposition.html @@ -0,0 +1,137 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Créer une proposition{% endblock %} +{% block module_name %}propositions{% endblock %} + +{% block content %} +
+

Créer une proposition commerciale

+ + Retour aux propositions + +
+ +
+
+
+
+ +
+
Informations client
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
Informations projet
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + + Séparez les fonctionnalités par des virgules +
+
+ + +
+
Informations complémentaires
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + + Annuler + +
+
+
+
+
+{% endblock %} diff --git a/Templates/propositions/propositions.html b/Templates/propositions/propositions.html new file mode 100644 index 0000000..bb7c5cb --- /dev/null +++ b/Templates/propositions/propositions.html @@ -0,0 +1,58 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Propositions{% endblock %} +{% block module_name %}propositions{% endblock %} + +{% block content %} +
+

Propositions commerciales

+ + Nouvelle proposition + +
+ +
+
+
+ + + + + + + + + + + {% if propositions %} + {% for proposition in propositions %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
ClientDate de créationFichierActions
{{ proposition.client_name }}{{ proposition.date }}{{ proposition.filename }} +
+ + + + + +
+
Aucune proposition trouvée
+
+
+
+{% endblock %} diff --git a/Templates/prospect_details.html b/Templates/prospect_details.html new file mode 100644 index 0000000..7108192 --- /dev/null +++ b/Templates/prospect_details.html @@ -0,0 +1,10 @@ + + Modifier + + + Convertir en client + + {% with entity_type='prospect', entity_id=prospect.id %} + {% include "partials/task_quick_add_button.html" %} + {% endwith %} + diff --git a/Templates/tasks/email_drafts.html b/Templates/tasks/email_drafts.html new file mode 100644 index 0000000..82d0e0c --- /dev/null +++ b/Templates/tasks/email_drafts.html @@ -0,0 +1,45 @@ +{% extends "layouts/base.html" %} + +{% block title %}Brouillons d'emails{% endblock %} + +{% block content %} +
+
+

Brouillons d'emails à envoyer

+
+
+ +
+ {% include "partials/task_quick_add_button.html" %} +
+
+ + {% if drafts %} + {% for d in drafts %} +
+
+
+
{{ d.subject }}
+
+ + +
+
+ +

À: {{ d.to_email }}

+
+ {{ d.content | safe }} +
+ Prospect: {{ d.prospect_id }} | Template: {{ d.template_id or '-' }} +
+
+ {% endfor %} + {% else %} +

Aucun brouillon à envoyer.

+ {% endif %} + + +
+{% endblock %} diff --git a/Templates/tasks/list.html b/Templates/tasks/list.html new file mode 100644 index 0000000..87855dc --- /dev/null +++ b/Templates/tasks/list.html @@ -0,0 +1,10 @@ +{% import "tasks/macros.html" as tsk %} +
+

Liste des tâches

+ {{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }} +
+{% import "tasks/macros.html" as tsk %} +
+

Liste des tâches

+ {{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }} +
\ No newline at end of file diff --git a/Templates/tasks/macros.html b/Templates/tasks/macros.html new file mode 100644 index 0000000..f9dcb23 --- /dev/null +++ b/Templates/tasks/macros.html @@ -0,0 +1,413 @@ +{# Macros de rendu pour les tâches #} +{% macro priority_badge(priority) -%} + {% set classes = { + 'haute': 'background:#ffe5e5;color:#b30000;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'normale': 'background:#e8f1ff;color:#003a8c;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'basse': 'background:#e9f7ef;color:#1b5e20;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;' + } %} + {{ priority|capitalize }} +{%- endmacro %} + +{% macro status_badge(status) -%} + {% set classes = { + 'todo': 'background:#fff3cd;color:#8a6d3b;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'done': 'background:#e6ffed;color:#155724;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'canceled': 'background:#f8d7da;color:#721c24;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;' + } %} + {% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %} + {{ labels.get(status, status) }} +{%- endmacro %} +{# Macros de rendu pour les tâches #} +{% macro priority_badge(priority) -%} + {% set classes = { + 'haute': 'background:#ffe5e5;color:#b30000;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'normale': 'background:#e8f1ff;color:#003a8c;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'basse': 'background:#e9f7ef;color:#1b5e20;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;' + } %} + {{ priority|capitalize }} +{%- endmacro %} + +{% macro status_badge(status) -%} + {% set classes = { + 'todo': 'background:#fff3cd;color:#8a6d3b;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'done': 'background:#e6ffed;color:#155724;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;', + 'canceled': 'background:#f8d7da;color:#721c24;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;' + } %} + {% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %} + {{ labels.get(status, status) }} +{%- endmacro %} + +{% macro entity_badge(entity_type) -%} + {% if entity_type %} + {% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %} + + {{ labels.get(entity_type, entity_type|capitalize) }} + + {% endif %} +{%- endmacro %} + +{% macro render_task_row(task, show_entity=True, show_due_date=True) -%} + + + {{ status_badge(task.status) }} + + +
{{ task.title }}
+ {% if task.description %} +
{{ task.description }}
+ {% endif %} + {% if task.metadata and task.metadata.ref %} +
Ref: {{ task.metadata.ref }}
+ {% endif %} + + {% if show_entity %} + + {{ entity_badge(task.entity_type) }} + {% if task.entity_id %} + {{ task.entity_id }} + {% endif %} + + {% endif %} + {% if show_due_date %} + + {{ task.due_date }} + {% if task.completed_at and task.status == 'done' %} +
Clôturée: {{ task.completed_at }}
+ {% endif %} + + {% endif %} + + {{ priority_badge(task.priority) }} + + +{%- endmacro %} +{# Macros de rendu pour les tâches #} +{% macro priority_badge(priority) -%} + {% set cls = {'haute':'bg-danger','normale':'bg-primary','basse':'bg-success'} %} + {{ priority|capitalize }} +{%- endmacro %} + +{% macro status_badge(status) -%} + {% set cls = {'todo':'bg-warning text-dark','done':'bg-success','canceled':'bg-danger'} %} + {% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %} + {{ labels.get(status, status) }} +{%- endmacro %} + +{% macro entity_badge(entity_type) -%} + {% if entity_type %} + {% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %} + {{ labels.get(entity_type, entity_type|capitalize) }} + {% endif %} +{%- endmacro %} + +{% macro render_task_row(task, show_entity=True, show_due_date=True, show_actions=False) -%} + + + {{ status_badge(task.status) }} + + +
{{ task.title }}
+ {% if task.description %} +
{{ task.description }}
+ {% endif %} + {% if task.metadata and task.metadata.ref %} +
Ref: {{ task.metadata.ref }}
+ {% endif %} + + {% if show_entity %} + + {{ entity_badge(task.entity_type) }} + {% if task.entity_id %} +
{{ task.entity_id }}
+ {% endif %} + + {% endif %} + {% if show_due_date %} + + {{ task.due_date }} + {% if task.completed_at and task.status == 'done' %} +
Clôturée: {{ task.completed_at }}
+ {% endif %} + + {% endif %} + + {{ priority_badge(task.priority) }} + + {% if show_actions %} + +
+ + +
+ + +
+ +
+ + +
+
+ + {% endif %} + +{%- endmacro %} + +{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche", show_actions=False, today=None) -%} +
+ + {% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + (1 if show_actions else 0) %} + + + + + {% if show_entity %}{% endif %} + {% if show_due_date %}{% endif %} + + {% if show_actions %}{% endif %} + + + + {% if tasks and tasks|length > 0 %} + {% for task in tasks %} + {{ render_task_row(task, show_entity=show_entity, show_due_date=show_due_date, show_actions=show_actions) }} + {% endfor %} + {% else %} + + + + {% endif %} + +
StatutTâcheLiée àÉchéancePrioritéActions
{{ empty_text }}
+
+{%- endmacro %} +{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche", show_actions=False, today=None) -%} +
+ + {% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + (1 if show_actions else 0) %} + + + + + {% if show_entity %}{% endif %} + {% if show_due_date %}{% endif %} + + {% if show_actions %}{% endif %} + + + + {% if tasks and tasks|length > 0 %} + {% for task in tasks %} + + + + {% if show_entity %} + + {% endif %} + {% if show_due_date %} + + {% endif %} + + {% if show_actions %} + + {% endif %} + + {% endfor %} + {% else %} + + + + {% endif %} + +
StatutTâcheLiée àÉchéancePrioritéActions
+ {{ status_badge(task.status) }} + +
{{ task.title }}
+ {% if task.description %} +
{{ task.description }}
+ {% endif %} + {% if task.metadata and task.metadata.ref %} +
Ref: {{ task.metadata.ref }}
+ {% endif %} +
+ {{ entity_badge(task.entity_type) }} + {% if task.entity_id %} + {{ task.entity_id }} + {% endif %} + + {{ task.due_date }} + {% if task.completed_at and task.status == 'done' %} +
Clôturée: {{ task.completed_at }}
+ {% endif %} +
+ {{ priority_badge(task.priority) }} + +
+ + +
+ + +
+ +
+ + +
+
+
+ {{ empty_text }} +
+
+{%- endmacro %} +{% macro entity_badge(entity_type) -%} + {% if entity_type %} + {% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %} + + {{ labels.get(entity_type, entity_type|capitalize) }} + + {% endif %} +{%- endmacro %} + +{% macro render_task_row(task, show_entity=True, show_due_date=True) -%} + + + {{ status_badge(task.status) }} + + +
{{ task.title }}
+ {% if task.description %} +
{{ task.description }}
+ {% endif %} + {% if task.metadata and task.metadata.ref %} +
Ref: {{ task.metadata.ref }}
+ {% endif %} + + {% if show_entity %} + + {{ entity_badge(task.entity_type) }} + {% if task.entity_id %} + {{ task.entity_id }} + {% endif %} + + {% endif %} + {% if show_due_date %} + + {{ task.due_date }} + {% if task.completed_at and task.status == 'done' %} +
Clôturée: {{ task.completed_at }}
+ {% endif %} + + {% endif %} + + {{ priority_badge(task.priority) }} + + +{%- endmacro %} + +{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche") -%} +
+ + + + + + {% if show_entity %}{% endif %} + {% if show_due_date %}{% endif %} + + + + + {% if tasks and tasks|length > 0 %} + {% for task in tasks %} + {{ render_task_row(task, show_entity=show_entity, show_due_date=show_due_date) }} + {% endfor %} + {% else %} + + + + {% endif %} + +
StatutTâcheLiée àÉchéancePriorité
+ {{ empty_text }} +
+
+{%- endmacro %} diff --git a/Templates/tasks/manage.html b/Templates/tasks/manage.html new file mode 100644 index 0000000..868cc1e --- /dev/null +++ b/Templates/tasks/manage.html @@ -0,0 +1,127 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Suite Consultance - Tasks{% endblock %} +{% block module_name %}Tasks{% endblock %} + +{% block content %} +{% import "tasks/macros.html" as tsk %} +{% import "tasks/manage_macros.html" as tskm %} +
+
+

Gestion des tâches

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ {{ tskm.render_tasks_table_actions(tasks, show_entity=True, show_due_date=True) }} +
+
+ +{# Modal de création global #} +{% include 'partials/task_create_modal.html' %} + +{# Modal d'édition de tâche #} + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/Templates/tasks/manage_macros.html b/Templates/tasks/manage_macros.html new file mode 100644 index 0000000..faaab91 --- /dev/null +++ b/Templates/tasks/manage_macros.html @@ -0,0 +1,133 @@ +{# Macros dédiés à la page de gestion des tâches (évite les collisions de signatures) #} +{% macro _priority_badge(priority) -%} + {% set cls = {'haute':'bg-danger','normale':'bg-primary','basse':'bg-success'} %} + {{ priority|capitalize }} +{%- endmacro %} + +{% macro _status_badge(status) -%} + {% set cls = {'todo':'bg-warning text-dark','done':'bg-success','canceled':'bg-danger'} %} + {% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %} + {{ labels.get(status, status) }} +{%- endmacro %} + +{% macro _entity_badge(entity_type) -%} + {% if entity_type %} + {% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %} + {{ labels.get(entity_type, entity_type|capitalize) }} + {% endif %} +{%- endmacro %} + +{% macro render_tasks_table_actions(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche") -%} +
+ + {% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + 1 %} + + + + + {% if show_entity %}{% endif %} + {% if show_due_date %}{% endif %} + + + + + + {% if tasks and tasks|length > 0 %} + {% for task in tasks %} + + + + {% if show_entity %} + + {% endif %} + {% if show_due_date %} + + {% endif %} + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
StatutTâcheLiée àÉchéancePrioritéActions
+ {{ _status_badge(task.status) }} + +
{{ task.title }}
+ {% if task.description %} +
{{ task.description }}
+ {% endif %} + {% if task.metadata and task.metadata.ref %} +
Ref: {{ task.metadata.ref }}
+ {% endif %} +
+ {{ _entity_badge(task.entity_type) }} + {% if task.entity_id %} +
{{ task.entity_id }}
+ {% endif %} +
+ {{ task.due_date }} + {% if task.completed_at and task.status == 'done' %} +
Clôturée: {{ task.completed_at }}
+ {% endif %} +
+ {{ _priority_badge(task.priority) }} + +
+ + +
+ + +
+ +
+ + +
+
+
{{ empty_text }}
+
+{%- endmacro %} diff --git a/Templates/tasks/quick_add.html b/Templates/tasks/quick_add.html new file mode 100644 index 0000000..428c389 --- /dev/null +++ b/Templates/tasks/quick_add.html @@ -0,0 +1,16 @@ +{% extends "layouts/base.html" %} + +{% block title %}Nouvelle tâche liée{% endblock %} + +{% block content %} +
+

Ajouter une tâche liée

+

Cette tâche sera liée à l'entité: {{ entity_type }} (ID: {{ entity_id }})

+ + {% include "partials/task_quick_add_form.html" %} + + +
+{% endblock %} diff --git a/Templates/tasks/today.html b/Templates/tasks/today.html new file mode 100644 index 0000000..9e4060a --- /dev/null +++ b/Templates/tasks/today.html @@ -0,0 +1,10 @@ +{% import "tasks/macros.html" as tsk %} +
+

Tâches du jour

+ {{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }} +
+{% import "tasks/macros.html" as tsk %} +
+

Tâches du jour

+ {{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }} +
\ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..af281ec --- /dev/null +++ b/app.py @@ -0,0 +1,1188 @@ +#!/usr/bin/env python3 +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory +import os +import json +from werkzeug.utils import secure_filename +from datetime import datetime +from typing import List, Dict + +# Import des modules existants +from core.form import Form +from core.generator import Generator +from core.data import Data +from modules.crm.handler import ClientHandler +from modules.crm.prospect_handler import ProspectHandler +from modules.proposition.fields import fields as proposition_fields +from modules.email.email_manager import EmailSender, EmailTemplate, EmailHistory +from modules.email.email_scraper import EmailScraper, EmailScrapingHistory +from datetime import datetime, date +from modules.projects.project_handler import ProjectHandler +from modules.projects.project import Project +from modules.projects.routes import projects_bp + +# Création de l'application Flask +app = Flask(__name__) +app.register_blueprint(projects_bp) +app.secret_key = 'suite_consultance_secretkey' # Clé secrète pour Flask + +# Ajouter un filtre personnalisé pour parser les dates ISO +@app.template_filter('parse_datetime') +def parse_datetime_filter(s): + """Convertir une chaîne de date ISO en objet datetime""" + try: + return datetime.fromisoformat(s.replace('Z', '+00:00')) + except: + return datetime.now() + +# Configuration des dossiers +app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Data') +app.config['OUTPUT_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'output') +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limite de taille pour les uploads (16MB) + +# Initialisation des gestionnaires +client_handler = ClientHandler() +prospect_handler = ProspectHandler() +project_handler = ProjectHandler() +email_sender = EmailSender() +email_template = EmailTemplate() +email_history = EmailHistory() +email_scraper = EmailScraper() +email_scraping_history = EmailScrapingHistory() + +# Filtres personnalisés pour Jinja2 +@app.template_filter('datetime') +def format_datetime(value, format='%d/%m/%Y %H:%M'): + if isinstance(value, str): + try: + value = datetime.fromisoformat(value) + except ValueError: + return value + return value.strftime(format) + +@app.template_filter('tojson') +def to_json(value): + import json + return json.dumps(value) + +# Route principale - Page d'accueil +@app.route('/') +def index(): + # Statistiques pour l'accueil + clients = client_handler.load_clients() + prospects = prospect_handler.load_prospects() + + # Nombre de propositions commerciales + propositions_path = os.path.join(app.config['OUTPUT_FOLDER'], 'propositions') + propositions_count = 0 + if os.path.exists(propositions_path): + propositions_count = len([f for f in os.listdir(propositions_path) if f.endswith('.pdf')]) + + # Nombre de devis + devis_path = os.path.join(app.config['OUTPUT_FOLDER'], 'devis') + devis_count = 0 + if os.path.exists(devis_path): + devis_count = len([f for f in os.listdir(devis_path) if f.endswith('.pdf')]) + + # Répartition des prospects par statut + prospect_status = {} + for prospect in prospects: + status = prospect.status if hasattr(prospect, 'status') else 'Nouveau' + prospect_status[status] = prospect_status.get(status, 0) + 1 + + # Activités récentes (derniers prospects et clients) + recent_prospects = sorted(prospects, key=lambda p: p.last_contact if hasattr(p, 'last_contact') else "", reverse=True)[:5] + + # Liste des derniers devis et propositions + recent_documents = [] + + if os.path.exists(propositions_path): + for filename in os.listdir(propositions_path): + if filename.endswith('.pdf'): + client_name = filename.split('_proposition')[0].replace('_', ' ').title() + date_created = datetime.fromtimestamp(os.path.getctime(os.path.join(propositions_path, filename))) + recent_documents.append({ + 'type': 'Proposition', + 'client': client_name, + 'date': date_created, + 'filename': filename + }) + + if os.path.exists(devis_path): + for filename in os.listdir(devis_path): + if filename.endswith('.pdf'): + client_name = filename.split('_devis')[0].replace('_', ' ').title() + date_created = datetime.fromtimestamp(os.path.getctime(os.path.join(devis_path, filename))) + recent_documents.append({ + 'type': 'Devis', + 'client': client_name, + 'date': date_created, + 'filename': filename + }) + + # Trier les documents par date (les plus récents d'abord) + recent_documents = sorted(recent_documents, key=lambda d: d['date'], reverse=True)[:5] + + return render_template('index.html', + clients_count=len(clients), + prospects_count=len(prospects), + propositions_count=propositions_count, + devis_count=devis_count, + prospect_status=prospect_status, + recent_prospects=recent_prospects, + recent_documents=recent_documents) + +# Routes pour le CRM +@app.route('/crm') +def crm(): + clients = client_handler.load_clients() + prospects = prospect_handler.load_prospects() + + # Statistiques pour le tableau de bord + dashboard_stats = { + 'total_clients': len(clients), + 'total_prospects': len(prospects), + 'prospect_status': {} + } + + # Compter les statuts des prospects + for prospect in prospects: + status = prospect.status if hasattr(prospect, 'status') else 'Nouveau' + dashboard_stats['prospect_status'][status] = dashboard_stats['prospect_status'].get(status, 0) + 1 + + return render_template('crm/crm.html', clients=clients, prospects=prospects, dashboard_stats=dashboard_stats) + +@app.route('/client/') +def client_details(client_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if os.path.exists(client_path): + data_manager = Data(client_path) + client_data = data_manager.load_data() + return render_template('crm/client_details.html', client=client_data) + else: + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + +@app.route('/client//edit', methods=['GET', 'POST']) +def edit_client(client_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + + data_manager = Data(client_path) + client_data = data_manager.load_data() + + if request.method == 'POST': + # Récupérer les données du formulaire + client_data.update({ + 'client_name': request.form.get('client_name'), + 'email': request.form.get('email'), + 'telephone': request.form.get('telephone'), + 'adresse': request.form.get('adresse'), + 'project_name': request.form.get('project_name'), + 'project_type': request.form.get('project_type'), + 'deadline': request.form.get('deadline'), + 'project_description': request.form.get('project_description'), + 'budget': request.form.get('budget'), + 'payment_terms': request.form.get('payment_terms'), + 'contact_info': request.form.get('contact_info'), + 'additional_info': request.form.get('additional_info') + }) + + # Traitement des fonctionnalités + features = request.form.get('features', '').split(',') + client_data['features'] = [{"description": feature.strip()} for feature in features if feature.strip()] + + # Validation des champs requis + if not client_data['client_name']: + flash('Le nom du client est obligatoire', 'error') + return render_template('crm/edit_client.html', client=client_data) + + # Sauvegarder les modifications + data_manager.save_data(client_data) + + flash('Client mis à jour avec succès', 'success') + return redirect(url_for('client_details', client_id=client_id)) + + return render_template('crm/edit_client.html', client=client_data) + +@app.route('/client//delete', methods=['POST', 'GET']) +def delete_client(client_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + + try: + # Delete client file + os.remove(client_path) + + # Delete associated projects + project_handler.delete_all_client_projects(client_id) + + # Delete associated documents + client_propositions = os.path.join(app.config['OUTPUT_FOLDER'], 'propositions', f"{client_id}_*.pdf") + client_devis = os.path.join(app.config['OUTPUT_FOLDER'], 'devis', f"{client_id}_*.pdf") + + for file_pattern in [client_propositions, client_devis]: + for file in glob.glob(file_pattern): + try: + os.remove(file) + except: + pass + + flash('Client supprimé avec succès', 'success') + except Exception as e: + flash(f'Erreur lors de la suppression du client: {str(e)}', 'error') + + return redirect(url_for('crm')) + + +# --------- Projets par client --------- +@app.route('/client//projects') +def list_client_projects(client_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + client_data = Data(client_path).load_data() + projects = project_handler.list_projects(client_id) + return render_template('projects/list_projects.html', client_id=client_id, client_name=client_data.get('client_name', client_id), projects=projects) + +@app.route('/client//projects/add', methods=['GET', 'POST']) +def add_client_project(client_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + client_data = Data(client_path).load_data() + + if request.method == 'POST': + name = request.form.get('name') + status = request.form.get('status', 'Nouveau') + start_date = request.form.get('start_date') or None + end_date = request.form.get('end_date') or None + description = request.form.get('description', '') + budget_raw = request.form.get('budget', '').strip() + budget = float(budget_raw.replace(',', '.')) if budget_raw else None + + if not name: + flash('Le nom du projet est obligatoire', 'error') + return render_template('projects/edit_project.html', client_id=client_id, client_name=client_data.get('client_name', client_id), project=None) + + project = Project( + client_id=client_id, + name=name, + status=status, + start_date=start_date, + end_date=end_date, + description=description, + budget=budget + ) + project_handler.add_project(project) + flash('Projet créé avec succès', 'success') + return redirect(url_for('list_client_projects', client_id=client_id)) + + return render_template('projects/edit_project.html', client_id=client_id, client_name=client_data.get('client_name', client_id), project=None) + +@app.route('/client//projects/') +def project_details(client_id, project_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + + project = project_handler.get_project(client_id, project_id) + if not project: + flash('Projet non trouvé', 'error') + return redirect(url_for('list_client_projects', client_id=client_id)) + + client_name = Data(client_path).load_data().get('client_name', client_id) + return render_template('projects/project_details.html', client_id=client_id, client_name=client_name, project=project) + +@app.route('/client//projects//edit', methods=['GET', 'POST']) +def edit_client_project(client_id, project_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + + project = project_handler.get_project(client_id, project_id) + if not project: + flash('Projet non trouvé', 'error') + return redirect(url_for('list_client_projects', client_id=client_id)) + + if request.method == 'POST': + project.name = request.form.get('name') + project.status = request.form.get('status', project.status) + project.start_date = request.form.get('start_date') or None + project.end_date = request.form.get('end_date') or None + project.description = request.form.get('description', '') + budget_raw = request.form.get('budget', '').strip() + project.budget = float(budget_raw.replace(',', '.')) if budget_raw else None + + if not project.name: + flash('Le nom du projet est obligatoire', 'error') + client_name = Data(client_path).load_data().get('client_name', client_id) + return render_template('projects/edit_project.html', client_id=client_id, client_name=client_name, project=project) + + project_handler.update_project(project) + flash('Projet mis à jour avec succès', 'success') + return redirect(url_for('project_details', client_id=client_id, project_id=project.id)) + + client_name = Data(client_path).load_data().get('client_name', client_id) + return render_template('projects/edit_project.html', client_id=client_id, client_name=client_name, project=project) + +@app.route('/client//projects//delete', methods=['POST', 'GET']) +def delete_client_project(client_id, project_id): + client_path = os.path.join(app.config['UPLOAD_FOLDER'], 'clients', f"{client_id}.json") + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('crm')) + + ok = project_handler.delete_project(client_id, project_id) + if ok: + flash('Projet supprimé avec succès', 'success') + else: + flash('Erreur lors de la suppression du projet', 'error') + return redirect(url_for('list_client_projects', client_id=client_id)) +# --------- Fin projets --------- + +@app.route('/client/add', methods=['GET', 'POST']) +def add_client(): + if request.method == 'POST': + client_data = { + 'client_name': request.form.get('client_name'), + 'email': request.form.get('email'), + 'telephone': request.form.get('telephone'), + 'adresse': request.form.get('adresse'), + 'project_name': request.form.get('project_name'), + 'project_type': request.form.get('project_type'), + 'deadline': request.form.get('deadline'), + 'project_description': request.form.get('project_description'), + 'budget': request.form.get('budget'), + 'payment_terms': request.form.get('payment_terms'), + 'contact_info': request.form.get('contact_info'), + 'additional_info': request.form.get('additional_info') + } + + # Traitement des fonctionnalités + features = request.form.get('features', '').split(',') + client_data['features'] = [{"description": feature.strip()} for feature in features if feature.strip()] + + # Sauvegarde du client + client_name = client_data['client_name'].replace(' ', '_').lower() + data_manager = Data(f"Data/clients/{client_name}.json") + data_manager.save_data(client_data) + + flash('Client ajouté avec succès', 'success') + return redirect(url_for('crm')) + + return render_template('crm/add_client.html', fields=proposition_fields()) + +# Routes pour les prospects +@app.route('/prospect/add', methods=['GET', 'POST']) +def add_prospect(): + from datetime import date + + if request.method == 'POST': + # Récupérer les données du formulaire + name = request.form.get('name') + company = request.form.get('company', '') + email = request.form.get('email', '') + phone = request.form.get('phone', '') + source = request.form.get('source', '') + notes = request.form.get('notes', '') + status = request.form.get('status', 'Nouveau') + + # Traitement des tags + tags = request.form.get('tags', '').split(',') + tags = [tag.strip() for tag in tags if tag.strip()] + + last_contact = request.form.get('last_contact', str(date.today())) + next_action = request.form.get('next_action', '') + + # Créer un objet Prospect + from modules.crm.prospect import Prospect + prospect = Prospect( + name=name, + company=company, + email=email, + phone=phone, + source=source, + notes=notes, + status=status, + tags=tags, + last_contact=last_contact, + next_action=next_action + ) + + # Ajouter le prospect + prospect_handler.add_prospect(prospect) + + flash('Prospect ajouté avec succès', 'success') + return redirect(url_for('crm')) + + # Pour l'affichage du formulaire, on passe la date du jour + today = date.today().strftime('%Y-%m-%d') + return render_template('crm/add_prospect.html', today=today) + +@app.route('/prospect/') +def prospect_details(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if prospect: + # Récupérer l'historique des emails + emails = email_history.get_prospect_email_history(prospect_id) + return render_template('crm/prospect_details.html', prospect=prospect, email_history=emails) + else: + flash('Prospect non trouvé', 'error') + return redirect(url_for('crm')) + +@app.route('/prospect/edit/', methods=['GET', 'POST']) +def edit_prospect(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if not prospect: + flash('Prospect non trouvé', 'error') + return redirect(url_for('crm')) + + if request.method == 'POST': + # Mettre à jour les données du prospect + prospect.name = request.form.get('name') + prospect.company = request.form.get('company', '') + prospect.email = request.form.get('email', '') + prospect.phone = request.form.get('phone', '') + prospect.source = request.form.get('source', '') + prospect.notes = request.form.get('notes', '') + prospect.status = request.form.get('status', 'Nouveau') + + # Traitement des tags + tags = request.form.get('tags', '').split(',') + prospect.tags = [tag.strip() for tag in tags if tag.strip()] + + prospect.last_contact = request.form.get('last_contact', prospect.last_contact) + prospect.next_action = request.form.get('next_action', '') + + # Mettre à jour le prospect + prospect_handler.update_prospect(prospect) + + flash('Prospect mis à jour avec succès', 'success') + return redirect(url_for('prospect_details', prospect_id=prospect_id)) + + return render_template('crm/edit_prospect.html', prospect=prospect) + +@app.route('/prospect/delete/') +def delete_prospect(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if prospect: + prospect_handler.delete_prospect(prospect_id) + flash('Prospect supprimé avec succès', 'success') + else: + flash('Prospect non trouvé', 'error') + + return redirect(url_for('crm')) + +@app.route('/prospect/convert/') +def convert_prospect(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if prospect: + # Convertir le prospect en client + from modules.crm.client import Client + + # Créer un dictionnaire de données client à partir du prospect + client_data = { + "client_name": prospect.name, + "email": prospect.email, + "telephone": prospect.phone, + "adresse": "", + "project_name": prospect.company, + "project_type": "", + "project_description": prospect.notes, + "additional_info": f"Converti depuis le prospect {prospect.id}. Tags: {', '.join(prospect.tags)}" + } + + # Sauvegarder le client + client_name = client_data['client_name'].replace(' ', '_').lower() + data_manager = Data(f"Data/clients/{client_name}.json") + data_manager.save_data(client_data) + + # Supprimer le prospect + prospect_handler.delete_prospect(prospect_id) + + flash('Prospect converti en client avec succès', 'success') + else: + flash('Prospect non trouvé', 'error') + + return redirect(url_for('crm')) + +# Routes pour les propositions commerciales +@app.route('/propositions') +def propositions(): + # Liste des propositions existantes + propositions_path = os.path.join(app.config['OUTPUT_FOLDER'], 'propositions') + propositions = [] + + if os.path.exists(propositions_path): + for filename in os.listdir(propositions_path): + if filename.endswith('.pdf'): + client_name = filename.split('_proposition')[0].replace('_', ' ').title() + date_created = datetime.fromtimestamp(os.path.getctime(os.path.join(propositions_path, filename))) + propositions.append({ + 'filename': filename, + 'client_name': client_name, + 'date': date_created.strftime('%d/%m/%Y') + }) + + return render_template('propositions/propositions.html', propositions=propositions) + +@app.route('/proposition/create', methods=['GET', 'POST']) +def create_proposition(): + if request.method == 'POST': + # Récupérer les données du formulaire pour tous les champs + form_data = { + 'client_name': request.form.get('client_name', ''), + 'email': request.form.get('email', ''), + 'telephone': request.form.get('telephone', ''), + 'adresse': request.form.get('adresse', ''), + 'project_name': request.form.get('project_name', ''), + 'project_type': request.form.get('project_type', ''), + 'deadline': request.form.get('deadline', ''), + 'project_description': request.form.get('project_description', ''), + 'budget': request.form.get('budget', ''), + 'payment_terms': request.form.get('payment_terms', ''), + 'contact_info': request.form.get('contact_info', ''), + 'additional_info': request.form.get('additional_info', ''), + 'features': request.form.get('features', '') + } + + # Validation des champs obligatoires + if not form_data['client_name'] or not form_data['project_name']: + flash('Veuillez remplir tous les champs obligatoires', 'error') + return render_template('propositions/create_proposition.html', fields=proposition_fields(), form_data=form_data) + + # Traitement des fonctionnalités + features = form_data.get('features', '').split(',') + form_data['features'] = [{"description": feature.strip()} for feature in features if feature.strip()] + + # Sauvegarde du client + client_name = form_data['client_name'].replace(' ', '_').lower() + data_manager = Data(f"Data/clients/{client_name}.json") + data_manager.save_data(form_data) + + # Génération de la proposition + generator = Generator(form_data) + generator.generate_pdf("propositions") + + # Option: créer un projet associé si demandé + create_project_flag = request.form.get('create_project') + if create_project_flag: + # Utiliser les champs de la proposition pour initialiser le projet + proj_name = request.form.get('project_name') or f"Projet - {form_data.get('client_name', client_name)}" + description = form_data.get('project_description', '') + # Convertir le budget si fourni + budget_val = None + try: + if form_data.get('budget'): + budget_val = float(str(form_data['budget']).replace(',', '.')) + except Exception: + budget_val = None + try: + project = Project( + client_id=client_name, + name=proj_name, + status='En cours', + start_date=None, + end_date=form_data.get('deadline') or None, + description=description, + budget=budget_val + ) + project_handler.add_project(project) + flash('Projet créé avec succès depuis la proposition', 'info') + except Exception as e: + flash(f'Création du projet échouée: {e}', 'warning') + + flash('Proposition créée avec succès', 'success') + return redirect(url_for('propositions')) + + return render_template('propositions/create_proposition.html', fields=proposition_fields()) + +# Routes pour les devis +@app.route('/devis') +def devis(): + # Liste des devis existants + devis_path = os.path.join(app.config['OUTPUT_FOLDER'], 'devis') + devis_list = [] + + if os.path.exists(devis_path): + for filename in os.listdir(devis_path): + if filename.endswith('.pdf'): + client_name = filename.split('_devis')[0].replace('_', ' ').title() + date_created = datetime.fromtimestamp(os.path.getctime(os.path.join(devis_path, filename))) + devis_list.append({ + 'filename': filename, + 'client_name': client_name, + 'date': date_created.strftime('%d/%m/%Y') + }) + + return render_template('devis/devis.html', devis=devis_list) + +@app.route('/devis/create', methods=['GET', 'POST']) +def create_devis(): + if request.method == 'POST': + client_name = request.form.get('client_name', '').replace(' ', '_').lower() + client_path = f"Data/clients/{client_name}.json" + + # Vérifier si le client existe + if not os.path.exists(client_path): + flash('Client non trouvé', 'error') + return redirect(url_for('create_devis')) + + # Charger les données du client + data_manager = Data(client_path) + client_data = data_manager.load_data() + + # Données du devis + devis_data = client_data.copy() + devis_data['numero'] = request.form.get('numero', '') + + # Génération du devis + generator = Generator(devis_data) + content = generator.generate_pdf("devis") + + # Option: créer un projet associé si demandé + create_project_flag = request.form.get('create_project') + if create_project_flag: + # Nom du projet: champ explicite sinon reprendre le nom de projet du client ou fallback + proj_name = request.form.get('project_name') or client_data.get('project_name') or f"Projet - {client_data.get('client_name', client_name)}" + # Description minimale liée au devis + description = f"Projet créé automatiquement via devis {devis_data.get('numero', '')}." + try: + project = Project( + client_id=client_name, + name=proj_name, + status='En cours', + description=description + ) + project_handler.add_project(project) + flash('Projet créé avec succès depuis le devis', 'info') + except Exception as e: + flash(f'Création du projet échouée: {e}', 'warning') + + flash('Devis créé avec succès', 'success') + return redirect(url_for('devis')) + + # Liste des clients pour sélection + clients = [] + client_folder = os.path.join(app.config['UPLOAD_FOLDER'], 'clients') + if os.path.exists(client_folder): + for filename in os.listdir(client_folder): + if filename.endswith('.json'): + client_name = filename.split('.')[0].replace('_', ' ').title() + clients.append({ + 'id': filename.split('.')[0], + 'name': client_name + }) + + return render_template('devis/create_devis.html', clients=clients) + +# Route pour télécharger les fichiers PDF +@app.route('/download/') +def download_file(filename): + directory = os.path.dirname(filename) + file = os.path.basename(filename) + return send_from_directory(os.path.join(app.config['OUTPUT_FOLDER'], directory), file, as_attachment=True) + +# Ajout de route pour la génération de données de test +@app.route('/generate-test-data') +def generate_test_data(): + # Fonction pour générer des données factices + from main import fake_data + fake_data() + flash('Données de test générées avec succès', 'success') + return redirect(url_for('index')) + +# Routes pour les emails +@app.route('/prospect//email', methods=['GET', 'POST']) +def send_prospect_email(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if not prospect: + flash('Prospect non trouvé', 'error') + return redirect(url_for('crm')) + + templates = email_template.get_all_templates() + + if request.method == 'POST': + to_email = request.form.get('to_email') + subject = request.form.get('subject') + body = request.form.get('body') + template_id = request.form.get('template_id') + update_status = request.form.get('update_status') == 'on' + new_status = request.form.get('new_status') + + # Vérifier les données obligatoires + if not to_email or not subject or not body: + flash('Veuillez remplir tous les champs obligatoires', 'error') + return render_template('email/send_email.html', prospect=prospect, templates=templates) + + # Envoyer l'email + if template_id: + # Contexte pour le template + context = { + 'name': prospect.name, + 'company': prospect.company, + 'email': prospect.email, + 'phone': prospect.phone + } + result = email_sender.send_templated_email(to_email, template_id, context) + else: + result = email_sender.send_email(to_email, subject, body) + + # Enregistrer l'historique + email_data = { + 'to': to_email, + 'subject': subject, + 'content': body, + 'success': result.get('success', False), + 'error': result.get('error', None) + } + email_history.add_email_record(prospect_id, email_data) + + # Mettre à jour le statut du prospect si demandé + if update_status and result.get('success', False): + prospect.status = new_status + prospect.last_contact = str(date.today()) + prospect_handler.update_prospect(prospect) + + if result.get('success', False): + flash('Email envoyé avec succès', 'success') + else: + flash(f'Erreur lors de l\'envoi de l\'email: {result.get("error", "Erreur inconnue")}', 'error') + + return redirect(url_for('prospect_details', prospect_id=prospect_id)) + + return render_template('email/send_email.html', prospect=prospect, templates=templates) + +@app.route('/prospect//email/history') +def prospect_email_history(prospect_id): + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if not prospect: + flash('Prospect non trouvé', 'error') + return redirect(url_for('crm')) + + emails = email_history.get_prospect_email_history(prospect_id) + + return render_template('email/email_history.html', prospect=prospect, emails=emails) + +@app.route('/email/bulk', methods=['GET', 'POST']) +def send_bulk_email(): + prospects = prospect_handler.load_prospects() + templates = email_template.get_all_templates() + + if request.method == 'POST': + prospect_ids = request.form.getlist('prospect_ids[]') + subject = request.form.get('subject') + body = request.form.get('body') + template_id = request.form.get('template_id') + update_status = request.form.get('update_status') == 'on' + new_status = request.form.get('new_status') + + # Vérifier les données obligatoires + if not prospect_ids or not subject or not body: + flash('Veuillez remplir tous les champs obligatoires et sélectionner au moins un prospect', 'error') + return render_template('email/bulk_email.html', prospects=prospects, templates=templates) + + # Préparer les destinataires + success_count = 0 + error_count = 0 + + for prospect_id in prospect_ids: + prospect = prospect_handler.get_prospect_by_id(prospect_id) + if not prospect or not prospect.email: + error_count += 1 + continue + + # Contexte pour personnaliser l'email + context = { + 'name': prospect.name, + 'company': prospect.company, + 'email': prospect.email, + 'phone': prospect.phone + } + + # Remplacer les variables dans le sujet et le corps + personalized_subject = subject + personalized_body = body + + for key, value in context.items(): + placeholder = f"{{{{{key}}}}}" + personalized_subject = personalized_subject.replace(placeholder, str(value) if value else "") + personalized_body = personalized_body.replace(placeholder, str(value) if value else "") + + # Envoyer l'email + if template_id: + result = email_sender.send_templated_email(prospect.email, template_id, context) + else: + result = email_sender.send_email(prospect.email, personalized_subject, personalized_body) + + # Enregistrer l'historique + email_data = { + 'to': prospect.email, + 'subject': personalized_subject, + 'content': personalized_body, + 'success': result.get('success', False), + 'error': result.get('error', None) + } + email_history.add_email_record(prospect_id, email_data) + + # Mettre à jour le statut du prospect si demandé + if update_status and result.get('success', False): + prospect.status = new_status + prospect.last_contact = str(date.today()) + prospect_handler.update_prospect(prospect) + + if result.get('success', False): + success_count += 1 + else: + error_count += 1 + + if success_count > 0: + flash(f'Emails envoyés avec succès à {success_count} prospect(s)', 'success') + if error_count > 0: + flash(f'Échec d\'envoi pour {error_count} prospect(s)', 'warning') + + return redirect(url_for('crm')) + + return render_template('email/bulk_email.html', prospects=prospects, templates=templates) + +@app.route('/email/templates') +def email_templates(): + templates = email_template.get_all_templates() + return render_template('email/templates.html', templates=templates) + +@app.route('/email/template/create', methods=['GET', 'POST']) +def create_email_template(): + if request.method == 'POST': + template_data = { + 'name': request.form.get('name'), + 'description': request.form.get('description', ''), + 'subject': request.form.get('subject'), + 'content': request.form.get('content') + } + + # Vérifier les données obligatoires + if not template_data['name'] or not template_data['subject'] or not template_data['content']: + flash('Veuillez remplir tous les champs obligatoires', 'error') + return render_template('email/edit_template.html', template=template_data) + + # Enregistrer le template + email_template.save_template(template_data) + + flash('Template d\'email créé avec succès', 'success') + return redirect(url_for('email_templates')) + + return render_template('email/edit_template.html', template=None) + +@app.route('/email/template/edit/', methods=['GET', 'POST']) +def edit_email_template(template_id): + template = email_template.get_template_by_id(template_id) + if not template: + flash('Template non trouvé', 'error') + return redirect(url_for('email_templates')) + + if request.method == 'POST': + template_data = { + 'id': template_id, + 'name': request.form.get('name'), + 'description': request.form.get('description', ''), + 'subject': request.form.get('subject'), + 'content': request.form.get('content') + } + + # Vérifier les données obligatoires + if not template_data['name'] or not template_data['subject'] or not template_data['content']: + flash('Veuillez remplir tous les champs obligatoires', 'error') + return render_template('email/edit_template.html', template=template_data) + + # Enregistrer le template + email_template.save_template(template_data) + + flash('Template d\'email mis à jour avec succès', 'success') + return redirect(url_for('email_templates')) + + return render_template('email/edit_template.html', template=template) + +@app.route('/email/template/delete/') +def delete_email_template(template_id): + if email_template.delete_template(template_id): + flash('Template d\'email supprimé avec succès', 'success') + else: + flash('Erreur lors de la suppression du template', 'error') + + return redirect(url_for('email_templates')) + +@app.route('/email/config', methods=['GET', 'POST']) +def email_config(): + config = email_sender._load_config() + + if request.method == 'POST': + config_data = { + 'smtp_server': request.form.get('smtp_server'), + 'smtp_port': int(request.form.get('smtp_port')), + 'username': request.form.get('username'), + 'password': request.form.get('password'), + 'sender_name': request.form.get('sender_name'), + 'sender_email': request.form.get('sender_email') + } + + # Vérifier les données obligatoires + for key, value in config_data.items(): + if not value and key != 'password': + flash('Veuillez remplir tous les champs obligatoires', 'error') + return render_template('email/config.html', config=config_data) + + # Si le mot de passe est vide, conserver l'ancien + if not config_data['password'] and 'password' in config: + config_data['password'] = config['password'] + + # Enregistrer la configuration + email_sender.save_config(config_data) + + flash('Configuration email enregistrée avec succès', 'success') + return redirect(url_for('email_config')) + + return render_template('email/config.html', config=config) + +# Routes pour le scrapping d'emails +@app.route('/email/scraper') +def email_scraper_page(): + scrapings = email_scraping_history.get_all_scrapings() + return render_template('email/scraper.html', scrapings=scrapings) + +@app.route('/email/scraper/new', methods=['GET', 'POST']) +def new_email_scraping(): + if request.method == 'POST': + url = request.form.get('url') + max_pages = int(request.form.get('max_pages', 10)) + auto_create_prospects = request.form.get('auto_create_prospects') == 'on' + + # Validation + if not url: + flash('Veuillez entrer une URL', 'error') + return render_template('email/new_scraping.html') + + try: + # Lancer le scrapping + results = email_scraper.scrape_page(url, max_pages) + + # Sauvegarder les résultats + filename = email_scraper.save_results(results) + + # Créer automatiquement des prospects si demandé + if auto_create_prospects and results['contacts']: + created_count, existing_count = _create_prospects_from_contacts(results['contacts'], url) + + if created_count > 0: + flash(f'{created_count} prospect(s) créé(s) automatiquement', 'info') + if existing_count > 0: + flash(f'{existing_count} email(s) déjà existant(s) ignoré(s)', 'info') + + flash(f'Scraping terminé: {len(results["contacts"])} contact(s) trouvé(s) sur {len(results["pages_scraped"])} page(s)', 'success') + return redirect(url_for('scraping_results', filename=os.path.basename(filename))) + + except Exception as e: + flash(f'Erreur lors du scraping: {str(e)}', 'error') + return render_template('email/new_scraping.html') + + return render_template('email/new_scraping.html') + +@app.route('/email/scraper/results/') +def scraping_results(filename): + results = email_scraping_history.get_scraping_details(filename) + if not results: + flash('Résultats de scraping non trouvés', 'error') + return redirect(url_for('email_scraper_page')) + + return render_template('email/scraping_results.html', results=results, filename=filename) + +@app.route('/email/scraper/delete/') +def delete_scraping(filename): + if email_scraping_history.delete_scraping(filename): + flash('Résultats de scraping supprimés avec succès', 'success') + else: + flash('Erreur lors de la suppression', 'error') + + return redirect(url_for('email_scraper_page')) + +@app.route('/api/email_template/', methods=['GET']) +def api_get_email_template(template_id): + """API pour récupérer un template d'email par son ID""" + try: + template = email_template.get_template_by_id(template_id) + if template: + return jsonify({ + 'success': True, + 'template': { + 'id': template.get('id'), + 'name': template.get('name'), + 'subject': template.get('subject'), + 'content': template.get('content'), + 'description': template.get('description') + } + }) + else: + return jsonify({ + 'success': False, + 'error': 'Template non trouvé' + }), 404 + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Erreur lors de la récupération du template: {str(e)}' + }), 500 + +@app.route('/api/scraping/create_prospects', methods=['POST']) +def api_create_prospects_from_scraping(): + """API pour créer des prospects à partir des contacts d'un scraping""" + data = request.json + filename = data.get('filename') + selected_contacts = data.get('contacts', []) + + if not filename or not selected_contacts: + return jsonify({'success': False, 'error': 'Données manquantes'}) + + results = email_scraping_history.get_scraping_details(filename) + if not results: + return jsonify({'success': False, 'error': 'Résultats de scraping non trouvés'}) + + # Filtrer les contacts sélectionnés + contacts_to_create = [] + for contact_email in selected_contacts: + contact = next((c for c in results['contacts'] if c['email'] == contact_email), None) + if contact: + contacts_to_create.append(contact) + + created_count, existing_count = _create_prospects_from_contacts(contacts_to_create, results['url']) + + return jsonify({ + 'success': True, + 'created': created_count, + 'existing': existing_count + }) + +# API pour tester la configuration email +@app.route('/api/test_email_config', methods=['POST']) +def api_test_email_config(): + config_data = request.json + + # Créer un EmailSender temporaire avec la configuration de test + temp_sender = EmailSender() + temp_sender.config = config_data + + try: + # Envoyer un email de test + result = temp_sender.send_email( + config_data['username'], + 'Test de configuration email', + '

Ceci est un email de test pour vérifier la configuration SMTP.

' + ) + + if result.get('success', False): + return jsonify({ + 'success': True + }) + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Erreur inconnue') + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +def _create_prospects_from_contacts(contacts: List[Dict], source_url: str) -> tuple: + """ + Créer des prospects à partir des contacts scrapés + Returns: (created_count, existing_count) + """ + created_count = 0 + existing_count = 0 + + existing_prospects = prospect_handler.load_prospects() + existing_emails = {p.email.lower() for p in existing_prospects if p.email} + + for contact in contacts: + email = contact.get('email', '').lower() + if not email: + continue + + # Vérifier si le prospect existe déjà + if email in existing_emails: + existing_count += 1 + continue + + # Préparer les données du prospect + name = contact.get('name', '') + if not name and contact.get('first_name'): + name = f"{contact.get('first_name', '')} {contact.get('last_name', '')}".strip() + if not name: + # Générer un nom à partir de l'email + local_part = email.split('@')[0] + name = local_part.replace('.', ' ').replace('_', ' ').title() + + company = contact.get('company', '') + if not company: + # Générer le nom d'entreprise à partir du domaine + domain = email.split('@')[1] + company = domain.split('.')[0].title() + + # Préparer les notes + notes_parts = [f"Contact trouvé automatiquement lors du scraping de {source_url}"] + + if contact.get('location'): + notes_parts.append(f"Localisation: {contact['location']}") + + if contact.get('source_url') and contact['source_url'] != source_url: + notes_parts.append(f"Page source: {contact['source_url']}") + + if contact.get('notes'): + notes_parts.append(contact['notes']) + + notes = ' | '.join(notes_parts) + + # Créer le prospect + from modules.crm.prospect import Prospect + prospect = Prospect( + name=name, + company=company, + email=email, + phone=contact.get('phone', ''), + source=f"Scraping web", + status='Nouveau', + notes=notes, + tags=['scraping', 'web'] + ) + + prospect_handler.add_prospect(prospect) + existing_emails.add(email) # Ajouter à la liste pour éviter les doublons dans le même batch + created_count += 1 + + return created_count, existing_count + +if __name__ == '__main__': + # Création des dossiers nécessaires s'ils n'existent pas + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'clients'), exist_ok=True) + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'prospects'), exist_ok=True) + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'projects'), exist_ok=True) + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'projects'), exist_ok=True) + os.makedirs(os.path.join(app.config['OUTPUT_FOLDER'], 'devis'), exist_ok=True) + os.makedirs(os.path.join(app.config['OUTPUT_FOLDER'], 'propositions'), exist_ok=True) + + # Dossiers pour les emails + os.makedirs('Data/email_templates', exist_ok=True) + os.makedirs('Data/email_history', exist_ok=True) + os.makedirs('Data/email_scraping', exist_ok=True) + os.makedirs('config', exist_ok=True) + + # Lancement de l'application en mode debug + app.run(debug=True, host='0.0.0.0', port=8888) diff --git a/config/email_config.json b/config/email_config.json new file mode 100644 index 0000000..196ea18 --- /dev/null +++ b/config/email_config.json @@ -0,0 +1,8 @@ +{ + "smtp_server": "smtp.gmail.com", + "smtp_port": 587, + "username": "violet.anthony90@gmail.com", + "password": "gfmodsysruaunlip", + "sender_name": "Anthony VIOLET", + "sender_email": "violet.anthony90@gmail.com" +} diff --git a/core/data.py b/core/data.py new file mode 100644 index 0000000..62593a9 --- /dev/null +++ b/core/data.py @@ -0,0 +1,43 @@ +import json +import os + +class Data: + def __init__(self, file_path): + """ + Initialise la classe Data à partir d'un fichier JSON. + + :param file_path: Chemin du fichier JSON. + """ + self.file_path = file_path + + def save_data(self, data): + """ + Enregistre les données dans le fichier JSON. + + :param data: Données à enregistrer. + """ + try: + with open(self.file_path, 'w', encoding='utf-8') as file: + # On remplace les caractères spéciaux par des espaces + json.dump(data, file, ensure_ascii=False, indent=4) + except Exception as e: + print(f"Erreur lors de l'enregistrement des données : {e}") + + def load_data(self): + """ + Charge les données à partir du fichier JSON. + + :return: Données chargées. + """ + if not os.path.exists(self.file_path): + print(f"Le fichier {self.file_path} n'existe pas.") + return None + + try: + with open(self.file_path, 'r', encoding='utf-8') as file: + data = json.load(file) + print(f"Données chargées avec succès depuis {self.file_path}") + return data + except Exception as e: + print(f"Erreur lors du chargement des données : {e}") + return None \ No newline at end of file diff --git a/core/form.py b/core/form.py new file mode 100644 index 0000000..96c8dab --- /dev/null +++ b/core/form.py @@ -0,0 +1,55 @@ +class Form: + def __init__(self, fields: list[dict]): + """ + Initialise la classe Form à partir d'une liste de champs. + + :param fields: Liste de dictionnaire, chaque champs est défini comme : + { + "name": "client_name", + "label": "Nom du client", + "type": "text", # ou "number", "email", "select"... + "options": ["Oui", "Non"], # facultatif, utilisé pour les champs de type "select" + } + """ + self.fields = fields + self.data = {} + + def ask(self): + print("Remplissez les informations suivantes :\n") + for field in self.fields: + name = field["name"] + label = field.get("label", name.replace("_", " ").capitalize()) + field_type = field.get("type", "text") + + value = None + + if field_type == "select": + options = field.get("options", []) + print(f"{label} :") + for i, option in enumerate(options): + print(f" {i + 1}. {option}") + while True: + choice = input(f"Votre choix (numéro) : ") + if choice.isdigit() and 1 <= int(choice) <= len(options): + value = options[int(choice) - 1] + break + print("Choix invalide. Veuillez réessayer.") + + elif field_type == "number": + while True: + value = input(f"{label} : ") + if value.replace('.', '', 1).isdigit(): + value = float(value) if '.' in value else int(value) + break + print("Veuillez entrer un nombre valide.") + + else: + value = input(f"{label} : ") + + self.data[name] = value + + def get_data(self) -> dict: + """ + Retourne les données collectées sous forme de dictionnaire. + """ + return self.data \ No newline at end of file diff --git a/core/generator.py b/core/generator.py new file mode 100644 index 0000000..6793a72 --- /dev/null +++ b/core/generator.py @@ -0,0 +1,254 @@ +import json +from fpdf import FPDF +import os + +class Generator: + def __init__(self, form_data, template_path="Templates/proposition_commercial.json"): + self.form_data = form_data + self.template_path = template_path + + def load_template(self): + with open(self.template_path, 'r', encoding='utf-8') as file: + template = json.load(file) + return template + + def replace_variables(self, content, visited=None): + if visited is None: + visited = set() + + # Avoid infinite recursion by tracking visited objects + if id(content) in visited: + return content + visited.add(id(content)) + + if isinstance(content, str): + # Replace variables in a string + for key, value in self.form_data.items(): + if key == "client" and isinstance(value, dict): + # Handle client dictionary directly + for sub_key, sub_value in value.items(): + placeholder = f"{{{sub_key}}}" + content = content.replace(placeholder, str(sub_value)) + elif isinstance(value, dict): + # Flatten nested dictionaries like 'client' + for sub_key, sub_value in value.items(): + placeholder = f"{{{key}_{sub_key}}}" + content = content.replace(placeholder, str(sub_value)) + elif isinstance(value, list): + # Handle lists (e.g., features or services) + if key == "features": + formatted_features = "\n".join(f"- {item['description']}" for item in value if isinstance(item, dict) and 'description' in item) + content = content.replace(f"{{{key}}}", formatted_features) + elif key == "services": + formatted_services = "\n".join( + f"- {item['description']} : {item['prix_unitaire']} euros" + for item in value if isinstance(item, dict) and 'description' in item and 'prix_unitaire' in item + ) + content = content.replace(f"{{{key}}}", formatted_services) + else: + content = content.replace(f"{{{key}}}", str(value)) + return content + elif isinstance(content, list): + # Replace variables in each item of a list + return [self.replace_variables(item, visited) for item in content] + elif isinstance(content, dict): + # Replace variables in a dictionary + return {k: self.replace_variables(v, visited) for k, v in content.items()} + return content + + def generate_content(self): + template = self.load_template() + return self.replace_variables(template) + + def generate_pdf(self, output_path=None): + content = self.generate_content() + + # Format the output file name based on the client name + client_name = self.form_data.get("client_name", "client").replace(" ", "_").lower() + if output_path is None: + output_path = f"output/{client_name}_proposition_commercial.pdf" + else: + output_path = f"output/{output_path}/{client_name}_proposition_commercial.pdf" + + pdf = FPDF() + pdf.add_page() + + # Ajouter un fond (image de fond) + background_path = "Data/BG.png" # Chemin de l'image de fond + if os.path.exists(background_path): + pdf.image(background_path, x=0, y=0, w=210, h=297) # Dimensions pour une page A4 + + # Title + title = content.get("title", "Proposition Commerciale") + pdf.set_font("Helvetica", "B", 16) + pdf.cell(0, 10, title, ln=True, align="C") + pdf.ln(10) + + # Main content + pdf.set_font("Helvetica", "", 12) + for section in content.get("sections", []): + # Section header + pdf.set_font("Helvetica", "B", 14) + pdf.cell(0, 10, section.get("header", ""), ln=True) + pdf.ln(5) + + # Section content + pdf.set_font("Helvetica", "", 12) + section_content = section.get("content", []) + if isinstance(section_content, list): + for item in section_content: + pdf.multi_cell(0, 10, str(item)) + pdf.ln(2) + elif isinstance(section_content, str): + pdf.multi_cell(0, 10, section_content) + pdf.ln(5) + + # Additional fields outside sections + for key, value in content.items(): + if key not in ["sections", "title"]: # Exclure "sections" et "title" + pdf.set_font("Helvetica", "B", 12) + pdf.cell(0, 10, f"{key} :", ln=True) + pdf.set_font("Helvetica", "", 12) + + # Format the client data specially + if key == "client" and isinstance(value, dict): + client_str = "\n".join(f"{k.capitalize()}: {v}" for k, v in value.items()) + pdf.multi_cell(0, 10, client_str) + else: + pdf.multi_cell(0, 10, str(value)) + + pdf.ln(5) + + # Ensure output folder exists + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + pdf.output(output_path) + print(f"PDF saved to {output_path}") + + def generate_facture(self, output_path=None): + content = self.generate_content() + + # Format the output file name based on the client name + # On récupère le nom du client dans le dictionnaire 'client' + client = content.get("client", {}) + if isinstance(client, dict): + client_name = client.get("nom", "client").replace(" ", "_").lower() + else: + # Fallback to a default name if 'client' is not a dictionary + client_name = "client" + print(f"Client name: {client_name}") + + if output_path is None: + output_path = f"output/{client_name}_devis.pdf" + else: + output_path = f"output/{output_path}/{client_name}_devis.pdf" + + # Create PDF with invoice-like styling + pdf = FPDF() + pdf.add_page() + + # Add background if exists + background_path = "Data/BG.png" + if os.path.exists(background_path): + pdf.image(background_path, x=0, y=0, w=210, h=297) + + # Document Title + pdf.set_font("Helvetica", "B", 18) + pdf.cell(0, 15, content.get("title", "Devis de service"), ln=True, align="C") + + # Add a horizontal line + pdf.line(10, pdf.get_y(), 200, pdf.get_y()) + pdf.ln(10) + + # Header Info (Devis number and date) + pdf.set_font("Helvetica", "B", 10) + pdf.cell(95, 10, f"DEVIS N° : {content.get('numero', '')}", 0, 0) + pdf.cell(95, 10, f"DATE : {content.get('date', '')}", 0, 1, 'R') + pdf.ln(5) + + # Client Information Box + pdf.set_fill_color(240, 240, 240) # Light gray background + pdf.rect(10, pdf.get_y(), 190, 30, style='F') + + pdf.set_font("Helvetica", "B", 12) + pdf.cell(0, 10, "INFORMATIONS CLIENT", 0, 1, 'L') + + pdf.set_font("Helvetica", "", 10) + client_data = content.get("client", {}) + if isinstance(client_data, dict): + pdf.cell(40, 5, f"Nom:", 0, 0) + pdf.cell(150, 5, f"{client_data.get('nom', '')}", 0, 1) + + pdf.cell(40, 5, f"Email:", 0, 0) + pdf.cell(150, 5, f"{client_data.get('email', '')}", 0, 1) + + pdf.cell(40, 5, f"Téléphone:", 0, 0) + pdf.cell(150, 5, f"{client_data.get('telephone', '')}", 0, 1) + + pdf.cell(40, 5, f"Adresse:", 0, 0) + pdf.cell(150, 5, f"{client_data.get('adresse', '')}", 0, 1) + + pdf.ln(10) + + # Services Table Header + pdf.set_fill_color(60, 60, 60) # Dark gray for header + pdf.set_text_color(255, 255, 255) # White text + pdf.set_font("Helvetica", "B", 10) + + # Table header + pdf.cell(120, 10, "DESCRIPTION", 1, 0, 'C', 1) + pdf.cell(35, 10, "PRIX UNITAIRE", 1, 0, 'C', 1) + pdf.cell(35, 10, "MONTANT", 1, 1, 'C', 1) + + # Reset color for table content + pdf.set_text_color(0, 0, 0) + pdf.set_font("Helvetica", "", 10) + + # Services Table Content + services = self.form_data.get("services", []) + total = 0 + + if isinstance(services, list): + for service in services: + if isinstance(service, dict): + description = service.get("description", "") + prix = service.get("prix_unitaire", 0) + total += float(prix) + + pdf.cell(120, 8, description, 1, 0) + pdf.cell(35, 8, f"{prix} E", 1, 0, 'R') + pdf.cell(35, 8, f"{prix} E", 1, 1, 'R') + + # Total Amount + pdf.set_font("Helvetica", "B", 10) + pdf.cell(155, 10, "TOTAL", 1, 0, 'R') + pdf.cell(35, 10, f"{total} E", 1, 1, 'R') + + # Payment Terms and Notes + pdf.ln(10) + pdf.set_font("Helvetica", "B", 10) + pdf.cell(0, 10, "CONDITIONS DE PAIEMENT", 0, 1) + + pdf.set_font("Helvetica", "", 10) + payment_terms = self.form_data.get("payment_terms", "50% à la signature, 50% à la livraison") + pdf.multi_cell(0, 5, payment_terms) + + pdf.ln(5) + pdf.set_font("Helvetica", "B", 10) + pdf.cell(0, 10, "NOTES", 0, 1) + + pdf.set_font("Helvetica", "", 10) + pdf.multi_cell(0, 5, "Ce devis est valable 30 jours à compter de la date d'émission.") + + # Footer + pdf.ln(10) + pdf.set_font("Helvetica", "I", 8) + pdf.cell(0, 10, "Merci de votre confiance.", 0, 1, 'C') + pdf.cell(0, 10, "Pour accepter ce devis, veuillez le signer et me le retourner par email.", 0, 1, 'C') + + # Ensure output folder exists + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # Save the PDF + pdf.output(output_path) + print(f"PDF saved to {output_path}") \ No newline at end of file diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/create_logo.py b/create_logo.py new file mode 100644 index 0000000..9d218b0 --- /dev/null +++ b/create_logo.py @@ -0,0 +1,81 @@ +# Logo Creation Script +from PIL import Image, ImageDraw, ImageFont +import os + +def create_logo(filename, bg_color, text_color): + """Create a logo for SuiteConsultance""" + # Create a blank image with a white background + width, height = 400, 400 + image = Image.new('RGBA', (width, height), bg_color) + + # Create a drawing context + draw = ImageDraw.Draw(image) + + # Draw a circle background + circle_center = (width // 2, height // 2) + circle_radius = 150 + draw.ellipse( + ( + circle_center[0] - circle_radius, + circle_center[1] - circle_radius, + circle_center[0] + circle_radius, + circle_center[1] + circle_radius + ), + fill=bg_color, outline=text_color, width=8 + ) + + # Try to load a font, fall back to default if not available + try: + font_large = ImageFont.truetype("Arial.ttf", 120) + font_small = ImageFont.truetype("Arial.ttf", 30) + except IOError: + font_large = ImageFont.load_default() + font_small = ImageFont.load_default() + + # Draw the "SC" text + text = "SC" + text_bbox = draw.textbbox((0, 0), text, font=font_large) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + text_position = ( + (width - text_width) // 2, + (height - text_height) // 2 - 20 + ) + draw.text(text_position, text, font=font_large, fill=text_color) + + # Draw "Suite Consultance" text below + subtext = "Suite Consultance" + subtext_bbox = draw.textbbox((0, 0), subtext, font=font_small) + subtext_width = subtext_bbox[2] - subtext_bbox[0] + subtext_height = subtext_bbox[3] - subtext_bbox[1] + subtext_position = ( + (width - subtext_width) // 2, + (height + text_height) // 2 + 30 + ) + draw.text(subtext_position, subtext, font=font_small, fill=text_color) + + # Save the image + image.save(filename) + print(f"Logo saved to {filename}") + +if __name__ == "__main__": + # Create Data directory if it doesn't exist + data_dir = "Data" + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + # Create light mode logo (dark text on light background) + create_logo( + os.path.join(data_dir, "logo_light.png"), + bg_color=(255, 255, 255, 255), # White + text_color=(41, 128, 185, 255) # Blue + ) + + # Create dark mode logo (light text on dark background) + create_logo( + os.path.join(data_dir, "logo_dark.png"), + bg_color=(52, 73, 94, 255), # Dark Blue + text_color=(236, 240, 241, 255) # White/Light Grey + ) + + print("Logo generation complete!") diff --git a/documentation/api.html b/documentation/api.html new file mode 100644 index 0000000..8e9b673 --- /dev/null +++ b/documentation/api.html @@ -0,0 +1,52 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + +
+

Endpoints disponibles

+ +

Recherche prospects

+
GET /api/crm/prospects/search?q=...&name=...&email=...&company=...&status=...&city=...&tags=a,b&date_from=YYYY-MM-DD&date_to=YYYY-MM-DD
+

Retourne une liste filtrée par texte ou filtres spécifiques. Les tags sont tous requis s’ils sont fournis.

+ +

Templates email (lecture)

+
GET /api/email_template/<template_id>
+ +

Créer des prospects depuis un scraping

+
POST /api/scraping/create_prospects
+Content-Type: application/json
+[{"name":"...", "email":"...", "company":"..."}]
+ +

Tester la configuration email

+
POST /api/test_email_config
+
+ + + + \ No newline at end of file diff --git a/documentation/assets/style.css b/documentation/assets/style.css new file mode 100644 index 0000000..f4b3bf1 --- /dev/null +++ b/documentation/assets/style.css @@ -0,0 +1,141 @@ +/* Wiki layout & design for documentation */ +:root{ + --wiki-bg:#f7f7f9; + --wiki-surface:#ffffff; + --wiki-border:#e5e7eb; + --wiki-text:#1f2937; + --wiki-muted:#6b7280; + --wiki-accent:#2563eb; + --wiki-accent-strong:#1d4ed8; + --wiki-link:#1f6feb; + --shadow-sm:0 1px 2px rgba(0,0,0,.04); + --shadow-md:0 4px 16px rgba(0,0,0,.06); +} + +/* Base */ +html,body{height:100%} +*{box-sizing:border-box} +body{ + margin:0; + color:var(--wiki-text); + background:var(--wiki-bg); + font:400 16px/1.55 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"; + text-rendering:optimizeLegibility; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; +} +a{color:var(--wiki-link);text-decoration:none} +a:hover{text-decoration:underline} +code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace} + +/* Sidebar (transforme la navbar en barre latérale wiki) */ +.navbar{ + position:fixed;left:0;top:0;bottom:0;width:280px; + background:var(--wiki-surface); + border-right:1px solid var(--wiki-border); + box-shadow:var(--shadow-sm); + overflow:auto; + z-index:100; +} +.navbar .inner{display:flex;flex-direction:column;gap:12px;padding:16px} +.brand{ + font-weight:700;font-size:18px;letter-spacing:.2px; +} +.badge{ + align-self:flex-start; + background:rgba(37,99,235,.1); + color:var(--wiki-accent-strong); + border:1px solid rgba(37,99,235,.25); + padding:2px 8px;border-radius:999px; + font-size:12px;font-weight:600; +} +.search{margin-top:4px} +.search input[type="search"]{ + width:100%;padding:10px 12px;border:1px solid var(--wiki-border); + border-radius:8px;background:#fff;color:var(--wiki-text); + outline:none;box-shadow:none; +} +.search input[type="search"]::placeholder{color:var(--wiki-muted)} +.search input[type="search"]:focus{border-color:var(--wiki-accent);box-shadow:0 0 0 3px rgba(37,99,235,.12)} + +.navlinks{ + display:flex;flex-direction:column;gap:4px;margin-top:4px +} +.navlinks a{ + display:block;padding:8px 10px;border-radius:8px; + color:var(--wiki-text);text-decoration:none; + border:1px solid transparent; +} +.navlinks a:hover{ + background:#f3f4f6;border-color:#e5e7eb;text-decoration:none +} +/* Neutraliser l'ancien surlignage inline et définir l'état actif */ +.navlinks a[style]{background:unset !important} +.navlinks a[style*="#141727"]{ + background:var(--wiki-accent);color:#fff !important; + border-color:var(--wiki-accent); + font-weight:600; +} + +/* Mise en page du contenu */ +body{margin-left:280px} +.container{max-width:1040px;margin:0 auto;padding:0 24px} +.hero{ + background:transparent;margin:0;border-bottom:1px solid var(--wiki-border) +} +.hero .container{padding:28px 24px} +.hero h1{margin:0 0 4px 0;font-size:28px;line-height:1.2} +.hero p{margin:6px 0 0 0;color:var(--wiki-muted)} + +/* Cartes en style wiki (simples blocs) */ +.grid{ + display:grid;grid-template-columns:repeat(3,minmax(0,1fr)); + gap:14px +} +.card{ + background:var(--wiki-surface); + border:1px solid var(--wiki-border); + border-radius:10px; + padding:14px 14px; + box-shadow:var(--shadow-sm); +} +.card h3{margin:0 0 6px 0;font-size:16px} +.card p{margin:0;color:#374151} + +/* Tables et code */ +.table-wrap{overflow:auto;border:1px solid var(--wiki-border);border-radius:8px;background:#fff} +table{border-collapse:collapse;width:100%} +th,td{padding:10px 12px;border-bottom:1px solid var(--wiki-border);text-align:left} +th{background:#f8fafc;font-weight:700} +pre{background:#0b1020;color:#e7e7e7;padding:14px;border-radius:10px;overflow:auto} +code{background:#f3f4f6;border:1px solid #e5e7eb;color:#111827;padding:2px 6px;border-radius:6px} + +/* Footer */ +.footer.small{ + color:var(--wiki-muted); + border-top:1px solid var(--wiki-border); + padding:16px 24px; + margin-top:24px; + background:linear-gradient(#fff,#fff) +} + +/* Responsive */ +@media (max-width: 1200px){ + .grid{grid-template-columns:repeat(2,minmax(0,1fr))} +} +@media (max-width: 1024px){ + body{margin-left:0} + .navbar{ + position:sticky;top:0;width:100%;height:auto;max-height:unset; + border-right:none;border-bottom:1px solid var(--wiki-border) + } + .navbar .inner{gap:10px} + .navlinks{flex-direction:row;flex-wrap:wrap;gap:6px} + .navlinks a{padding:6px 10px} + .container{padding:0 16px} + .hero .container{padding:16px} +} +@media (max-width: 640px){ + .grid{grid-template-columns:1fr} + .search input[type="search"]{padding:8px 10px} +} diff --git a/documentation/assets/wiki.css b/documentation/assets/wiki.css new file mode 100644 index 0000000..f4b3bf1 --- /dev/null +++ b/documentation/assets/wiki.css @@ -0,0 +1,141 @@ +/* Wiki layout & design for documentation */ +:root{ + --wiki-bg:#f7f7f9; + --wiki-surface:#ffffff; + --wiki-border:#e5e7eb; + --wiki-text:#1f2937; + --wiki-muted:#6b7280; + --wiki-accent:#2563eb; + --wiki-accent-strong:#1d4ed8; + --wiki-link:#1f6feb; + --shadow-sm:0 1px 2px rgba(0,0,0,.04); + --shadow-md:0 4px 16px rgba(0,0,0,.06); +} + +/* Base */ +html,body{height:100%} +*{box-sizing:border-box} +body{ + margin:0; + color:var(--wiki-text); + background:var(--wiki-bg); + font:400 16px/1.55 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"; + text-rendering:optimizeLegibility; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; +} +a{color:var(--wiki-link);text-decoration:none} +a:hover{text-decoration:underline} +code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace} + +/* Sidebar (transforme la navbar en barre latérale wiki) */ +.navbar{ + position:fixed;left:0;top:0;bottom:0;width:280px; + background:var(--wiki-surface); + border-right:1px solid var(--wiki-border); + box-shadow:var(--shadow-sm); + overflow:auto; + z-index:100; +} +.navbar .inner{display:flex;flex-direction:column;gap:12px;padding:16px} +.brand{ + font-weight:700;font-size:18px;letter-spacing:.2px; +} +.badge{ + align-self:flex-start; + background:rgba(37,99,235,.1); + color:var(--wiki-accent-strong); + border:1px solid rgba(37,99,235,.25); + padding:2px 8px;border-radius:999px; + font-size:12px;font-weight:600; +} +.search{margin-top:4px} +.search input[type="search"]{ + width:100%;padding:10px 12px;border:1px solid var(--wiki-border); + border-radius:8px;background:#fff;color:var(--wiki-text); + outline:none;box-shadow:none; +} +.search input[type="search"]::placeholder{color:var(--wiki-muted)} +.search input[type="search"]:focus{border-color:var(--wiki-accent);box-shadow:0 0 0 3px rgba(37,99,235,.12)} + +.navlinks{ + display:flex;flex-direction:column;gap:4px;margin-top:4px +} +.navlinks a{ + display:block;padding:8px 10px;border-radius:8px; + color:var(--wiki-text);text-decoration:none; + border:1px solid transparent; +} +.navlinks a:hover{ + background:#f3f4f6;border-color:#e5e7eb;text-decoration:none +} +/* Neutraliser l'ancien surlignage inline et définir l'état actif */ +.navlinks a[style]{background:unset !important} +.navlinks a[style*="#141727"]{ + background:var(--wiki-accent);color:#fff !important; + border-color:var(--wiki-accent); + font-weight:600; +} + +/* Mise en page du contenu */ +body{margin-left:280px} +.container{max-width:1040px;margin:0 auto;padding:0 24px} +.hero{ + background:transparent;margin:0;border-bottom:1px solid var(--wiki-border) +} +.hero .container{padding:28px 24px} +.hero h1{margin:0 0 4px 0;font-size:28px;line-height:1.2} +.hero p{margin:6px 0 0 0;color:var(--wiki-muted)} + +/* Cartes en style wiki (simples blocs) */ +.grid{ + display:grid;grid-template-columns:repeat(3,minmax(0,1fr)); + gap:14px +} +.card{ + background:var(--wiki-surface); + border:1px solid var(--wiki-border); + border-radius:10px; + padding:14px 14px; + box-shadow:var(--shadow-sm); +} +.card h3{margin:0 0 6px 0;font-size:16px} +.card p{margin:0;color:#374151} + +/* Tables et code */ +.table-wrap{overflow:auto;border:1px solid var(--wiki-border);border-radius:8px;background:#fff} +table{border-collapse:collapse;width:100%} +th,td{padding:10px 12px;border-bottom:1px solid var(--wiki-border);text-align:left} +th{background:#f8fafc;font-weight:700} +pre{background:#0b1020;color:#e7e7e7;padding:14px;border-radius:10px;overflow:auto} +code{background:#f3f4f6;border:1px solid #e5e7eb;color:#111827;padding:2px 6px;border-radius:6px} + +/* Footer */ +.footer.small{ + color:var(--wiki-muted); + border-top:1px solid var(--wiki-border); + padding:16px 24px; + margin-top:24px; + background:linear-gradient(#fff,#fff) +} + +/* Responsive */ +@media (max-width: 1200px){ + .grid{grid-template-columns:repeat(2,minmax(0,1fr))} +} +@media (max-width: 1024px){ + body{margin-left:0} + .navbar{ + position:sticky;top:0;width:100%;height:auto;max-height:unset; + border-right:none;border-bottom:1px solid var(--wiki-border) + } + .navbar .inner{gap:10px} + .navlinks{flex-direction:row;flex-wrap:wrap;gap:6px} + .navlinks a{padding:6px 10px} + .container{padding:0 16px} + .hero .container{padding:16px} +} +@media (max-width: 640px){ + .grid{grid-template-columns:1fr} + .search input[type="search"]{padding:8px 10px} +} diff --git a/documentation/build.html b/documentation/build.html new file mode 100644 index 0000000..96d68e1 --- /dev/null +++ b/documentation/build.html @@ -0,0 +1,67 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Construire l’application desktop (macOS)

+
    +
  1. Préparer l’icône à partir de icon.png :
    +
    # Générer le .iconset (macOS)
    +mkdir -p icon.iconset
    +sips -z 16 16     icon.png --out icon.iconset/icon_16x16.png
    +sips -z 32 32     icon.png --out icon.iconset/icon_16x16@2x.png
    +sips -z 32 32     icon.png --out icon.iconset/icon_32x32.png
    +sips -z 64 64     icon.png --out icon.iconset/icon_32x32@2x.png
    +sips -z 128 128   icon.png --out icon.iconset/icon_128x128.png
    +sips -z 256 256   icon.png --out icon.iconset/icon_128x128@2x.png
    +sips -z 256 256   icon.png --out icon.iconset/icon_256x256.png
    +sips -z 512 512   icon.png --out icon.iconset/icon_256x256@2x.png
    +sips -z 512 512   icon.png --out icon.iconset/icon_512x512.png
    +sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png
    +iconutil -c icns icon.iconset -o icon.icns
    +
  2. +
  3. Lancer le build :
    +
    pyinstaller SuiteConsultance.spec
    +# ou
    +pyinstaller "Suivi Consultance.spec"
    +
  4. +
  5. Tester l’app signée : double‑cliquez l’app. Si rien ne s’ouvre, testez en CLI :
    +
    python run.py --webview
    +
  6. +
+ +

Windows / Linux

+

Adaptez la commande PyInstaller (icônes au format .ico/.png). Vérifiez la présence de pywebview et des dépendances WebKit/Edge selon la plateforme.

+
+ + + + \ No newline at end of file diff --git a/documentation/crm.html b/documentation/crm.html new file mode 100644 index 0000000..a6f9d05 --- /dev/null +++ b/documentation/crm.html @@ -0,0 +1,59 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Vue CRM

+

La page CRM regroupe deux onglets : Prospects et Clients. Les fiches sont stockées en JSON dans Data/prospects et Data/clients.

+ +

Prospects

+
    +
  • Statut : Nouveau, Contacté, Qualifié, Proposition, Non intéressé.
  • +
  • Scoring : score numérique → catégories Froid / Tiède / Chaud.
  • +
  • Champs clés : nom, société, email, téléphone, source, notes, tags, dernière prise de contact, prochaine action.
  • +
  • Recherche & filtres (API /api/crm/prospects/search) : filtrez par nom/email/société/ville/tags/période.
  • +
+ +

Actions fréquentes

+
    +
  • Ajouter : bouton “Ajouter un prospect”.
  • +
  • Modifier : via la fiche.
  • +
  • Convertir en client : bouton “Convertir”.
  • +
  • Envoyer un email : depuis la fiche (historique accessible).
  • +
+ +

Clients

+

Les clients héritent d’un modèle proche (coordonnées, tags, notes, documents liés) et peuvent recevoir des projets liés.

+
+ + + + \ No newline at end of file diff --git a/documentation/demarrage.html b/documentation/demarrage.html new file mode 100644 index 0000000..1cca910 --- /dev/null +++ b/documentation/demarrage.html @@ -0,0 +1,53 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Objectif : être opérationnel en 5 minutes

+
    +
  1. Ajouter un prospect : menu CRM → Prospects → Ajouter. Renseignez nom, société, email, statut (Nouveau/Contacté/Qualifié/Proposition/Non intéressé) et les tags.
  2. +
  3. Planifier l’action suivante : dans la fiche prospect, définissez Prochaine action (ex. relance jeudi 14h).
  4. +
  5. Envoyer un email : configurez le SMTP (menu Email → Configuration), créez un template puis utilisez Email → Envoi ou depuis la fiche prospect.
  6. +
  7. Convertir en client : depuis un prospect qualifié, utilisez Convertir en client.
  8. +
  9. Créer une proposition ou un devis : rubrique Propositions ou Devis, remplissez le formulaire et générez le PDF.
  10. +
+ +

Conseils rapides

+
    +
  • Utilisez des tags pour vos segments (ex. “restauration”, “TPE”, “chaud”).
  • +
  • Le score de prospect évolue via les interactions (ouverture/clic/réponse). Catégories : Froid, Tiède, Chaud.
  • +
  • Le job quotidien peut générer des brouillons d’emails du jour pour vos relances (voir Tâches & Rappels).
  • +
+
+ + + + \ No newline at end of file diff --git a/documentation/depannage.html b/documentation/depannage.html new file mode 100644 index 0000000..a2951db --- /dev/null +++ b/documentation/depannage.html @@ -0,0 +1,65 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + +
+

Problèmes fréquents

+ +

Pillow / lxml : erreurs de compilation

+
+

Mettez à jour les outils de build et réinstallez :

+
python -m pip install --upgrade pip setuptools wheel
+pip install --no-cache-dir --force-reinstall pillow lxml
+
+ +

NumPy: dtype size changed

+
+

Incompatibilité binaire. Supprimez le cache et forcez la réinstallation :

+
pip uninstall -y numpy
+pip install --no-cache-dir numpy
+
+ +

PyInstaller : l’app ne se lance pas

+
    +
  • Testez d’abord : python run.py --webview.
  • +
  • Vérifiez les spécifications SuiteConsultance.spec et les icônes.
  • +
  • Regardez les logs en lançant l’exécutable depuis un terminal.
  • +
+ +

“Le répertoire n’existe pas”

+

Exécutez python install.py pour créer Data/ et output/.

+ +

Dépendances manquantes

+

Le lanceur vérifie fpdf, dateutil, pillow, packaging, pywebview. Installez ce qui manque :

+
pip install -r requirements.txt
+
+ + + + \ No newline at end of file diff --git a/documentation/devis.html b/documentation/devis.html new file mode 100644 index 0000000..672c004 --- /dev/null +++ b/documentation/devis.html @@ -0,0 +1,47 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Devis

+

Allez dans Devis → Nouveau devis. Saisissez les lignes (description, quantité, prix) et générez le PDF (stocké dans output/devis/).

+ +

Conseils

+
    +
  • Numérotez vos devis (SC‑2025‑001).
  • +
  • Indiquez les conditions de paiement et la durée de validité.
  • +
  • Reliez le devis à un client pour le retrouver facilement.
  • +
+
+ + + + \ No newline at end of file diff --git a/documentation/donnees.html b/documentation/donnees.html new file mode 100644 index 0000000..597d30a --- /dev/null +++ b/documentation/donnees.html @@ -0,0 +1,54 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Arborescence des données

+
Data/
+  clients/            # JSON clients
+  prospects/          # JSON prospects
+  email_templates/    # modèles d’emails
+  email_history/      # historique des envois
+  scraping/           # résultats scraping
+output/
+  devis/              # PDF devis
+  propositions/       # PDF propositions
+ +

Export / Sauvegarde

+
    +
  • Sauvegardez Data/ et output/ régulièrement.
  • +
  • Pour migrer : copiez ces dossiers vers la nouvelle machine et réinstallez les dépendances.
  • +
+
+ + + + \ No newline at end of file diff --git a/documentation/email.html b/documentation/email.html new file mode 100644 index 0000000..b7aa42d --- /dev/null +++ b/documentation/email.html @@ -0,0 +1,59 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Configuration SMTP

+

Allez dans Email → Configuration et renseignez :

+
    +
  • Serveur (host) et port
  • +
  • Adresse expéditrice et mot de passe (ou mot de passe application)
  • +
  • Option TLS/SSL
  • +
+
Un bouton de test permet de valider vos paramètres.
+ +

Templates

+

Créez/modifiez des modèles réutilisables (stockés dans Data/email_templates). Variables supportées : {{name}}, {{company}}, etc.

+ +

Envoi

+
    +
  • Depuis une fiche prospect : envoi individuel + historique.
  • +
  • Envoi groupé (bulk) : sélectionnez une liste filtrée de prospects et un template.
  • +
+ +

Scraping d’emails

+

Dans Email → Scraper, indiquez une URL d’entreprise. Le scraper parcourt les pages (pagination incluse) et récupère emails / numéros de téléphone.

+
Respectez le RGPD et les règles anti‑spam. Limitez la fréquence, privilégiez le opt‑in et conservez une base de preuve si nécessaire.
+
+ + + + \ No newline at end of file diff --git a/documentation/faq.html b/documentation/faq.html new file mode 100644 index 0000000..25644d1 --- /dev/null +++ b/documentation/faq.html @@ -0,0 +1,51 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + +
+

FAQ

+

Où sont les PDF générés ?

+

Dans output/propositions et output/devis.

+ +

Puis-je personnaliser les templates PDF ?

+

Oui, ajustez les fichiers dans Templates/propositions et Templates/devis. Faites une copie avant modification.

+ +

Comment filtrer rapidement mes prospects “chauds” ?

+

Utilisez la recherche avec un filtre de score élevé ou un tag dédié.

+ +

Le scraping est-il légal ?

+

Il dépend du contexte et de la juridiction. Respectez le RGPD, vérifiez les mentions légales et privilégiez l’opt‑in.

+ +

Comment sauvegarder / migrer ?

+

Copiez simplement Data/ et output/ vers la nouvelle machine.

+
+ + + + \ No newline at end of file diff --git a/documentation/index.html b/documentation/index.html new file mode 100644 index 0000000..7a21d9c --- /dev/null +++ b/documentation/index.html @@ -0,0 +1,79 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+
+

Documentation utilisateur

+

Un guide clair et complet pour bien démarrer, maîtriser les modules, et gagner du temps au quotidien.

+ +
+
+
+
+
+

CRM

+

Gérez vos prospects (pipeline, scoring Froid/Tiède/Chaud), convertissez en clients, archivez les interactions, planifiez la prochaine action.

+
+
+

Emails & modèles

+

Configurez l’envoi SMTP, créez des templates, envoyez des campagnes one‑to‑one ou en bulk, suivez l’historique.

+
+
+

Scraping

+

Récupérez automatiquement emails/téléphones depuis des sites d’entreprises et générez des fiches prospects (RGPD : utilisez avec parcimonie).

+
+
+

Propositions & Devis

+

Générez des PDF propres depuis des formulaires guidés. Les documents sont stockés dans output/.

+
+
+

Projets

+

Reliez des projets à chaque client, suivez l’avancement, les échéances et le contexte.

+
+
+

Tâches & Rappels

+

Créez des todo liées à des prospects/clients et générez des brouillons d’emails du jour via un job quotidien.

+
+
+

Scoring & Tracking

+

Comprenez et paramétrez le scoring Froid/Tiède/Chaud, suivez les événements (emails, web, appels) et pilotez vos priorités.

+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/installation.html b/documentation/installation.html new file mode 100644 index 0000000..5bed873 --- /dev/null +++ b/documentation/installation.html @@ -0,0 +1,75 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Prérequis

+
    +
  • Python 3.8+ (recommandé : 3.10/3.11)
  • +
  • macOS, Linux ou Windows
  • +
  • Accès internet (pour les dépendances pip)
  • +
+ +

Étapes d’installation

+
    +
  1. Ouvrir un terminal dans le dossier du projet SuiteConsultance.
  2. +
  3. Créer un environnement virtuel :
    +
    python -m venv .venv
    +source .venv/bin/activate  # macOS/Linux
    +.venv\Scripts\activate   # Windows
    +
  4. +
  5. Installer les dépendances :
    +
    pip install -r requirements.txt
    +
    Si une erreur Pillow ou lxml survient, consultez Dépannage.
    +
  6. +
  7. Initialiser l’arborescence des dossiers :
    +
    python install.py
    +
  8. +
+ +

Lancer l’application

+

Deux modes sont possibles :

+
    +
  • Mode application desktop (fenêtre WebView) :
    +
    python run.py --webview
    +
  • +
  • Mode navigateur (serveur Flask) :
    +
    python run.py --browser
    +# puis ouvrir http://127.0.0.1:8080
    +
  • +
+ +
Par défaut, le serveur écoute sur le port 8080. Si le port est occupé, libérez‑le ou modifiez la commande de lancement.
+
+ + + + \ No newline at end of file diff --git a/documentation/projets.html b/documentation/projets.html new file mode 100644 index 0000000..f41f134 --- /dev/null +++ b/documentation/projets.html @@ -0,0 +1,49 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Projets liés aux clients

+

Depuis une fiche client, ouvrez l’onglet Projets. Vous pouvez créer, éditer, clôturer ou supprimer des projets.

+ + + + + + + + +
ChampDescription
Nom du projetIdentifiant clair (ex. “Refonte site vitrine”)
TypeCatégorie (web, logiciel, audit…)
ÉchéanceDate cible
DescriptionContexte, objectifs, contraintes
BudgetBudget estimé/contrat
ModalitésPaiement, livrables, propriété
+
+ + + + \ No newline at end of file diff --git a/documentation/propositions.html b/documentation/propositions.html new file mode 100644 index 0000000..96ad9a9 --- /dev/null +++ b/documentation/propositions.html @@ -0,0 +1,48 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Propositions commerciales

+

Créez une proposition via Propositions → Nouvelle proposition. Renseignez les champs métiers (titre, contexte, livrables, prix, conditions). Le PDF est généré dans output/propositions/.

+

Les champs par défaut sont définis dans modules/proposition/fields.py et le rendu HTML dans Templates/propositions/.

+ +

Bonnes pratiques

+
    +
  • Pré‑remplissez les features du client (ex. “landing page + CRM léger”).
  • +
  • Incluez des options modulaires (Option A/B/C) et des délais réalistes.
  • +
  • Gardez un historique des versions dans le nom de fichier (ex. ClientX_proposition_v2.pdf).
  • +
+
+ + + + \ No newline at end of file diff --git a/documentation/scoring.html b/documentation/scoring.html new file mode 100644 index 0000000..6647421 --- /dev/null +++ b/documentation/scoring.html @@ -0,0 +1,155 @@ + + + + + + SuiteConsultance — Scoring & Tracking + + + + + + +
+
+

Scoring & Tracking

+

Mesurez l’intérêt réel de vos prospects, suivez leurs interactions (emails, web, appels) et priorisez vos actions commerciales grâce à un scoring simple et actionnable.

+
+
+ +
+

1. Vue d’ensemble

+

Le scoring consolide toutes les interactions d’un contact en un score unique et lisible : Froid, Tiède ou Chaud. Il s’appuie sur des événements suivis automatiquement (ouverture/clic d’email, visites web) et des activités saisies par l’équipe (appels, réunions, tâches). Le score évolue dans le temps avec une décroissance pour refléter la fraîcheur de l’intérêt.

+ +

2. Échelle et statuts

+
    +
  • Échelle de 0 à 100 (arrondie à l’entier).
  • +
  • Seuils par défaut : +
      +
    • Froid : 0–29
    • +
    • Tiède : 30–69
    • +
    • Chaud : 70–100
    • +
    +
  • +
+

Les seuils sont des recommandations ; adaptez-les à votre cycle de vente et à votre volume d’interactions.

+ +

3. Événements suivis et pondérations

+

Chaque événement ajoute ou retire des points. Voici une grille de référence équilibrée, pensée pour refléter l’intention réelle :

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ÉvénementConditionScoreNotes
Ouverture d’emailUnique par campagne+3Ignorer les doublons
Clic dans un emailPar lien unique+8Max +16 par email
Réponse à un emailFil de discussion+25Fort signal d’intention
Bounce (NPAI)Hard bounce-20Nettoyer la base
DésabonnementOpt-out-50Stopper les relances
Visite siteSession identifiée+5Basé sur cookie/ID
Visites multiples2+ pages vues+10Intérêt confirmé
TéléchargementPDF/ressource+12Contenu avancé
Demande de démoFormulaire+30Lead chaud
Appel passéLog manuel+5Contact établi
Appel qualifiéOk décisionnaire+12Qualification
Réunion tenueRDV confirmé+20Étape clé
Tâche complétéeFollow-up+2Rappels utiles
InactivitéVoir décroissance-Automatique
+
+ +

4. Décroissance dans le temps

+

Pour éviter que d’anciens signaux gonflent artificiellement le score, une décote s’applique automatiquement :

+
    +
  • -0,5 point par jour sans nouvelle interaction (jusqu’à un minimum de 0).
  • +
  • Réinitialisation de la décote à chaque nouvel événement significatif (clic, réponse, visite).
  • +
+

Exemple : un prospect à 72 retombe à 61 après ~22 jours sans activité, repasse “Tiède” et sort des priorités quotidiennes.

+ +

5. Priorisation et règles d’usage

+
    +
  • Le tableau “Aujourd’hui” met en avant les “Chauds” récents (score ≥ 70, activité ≤ 7 jours).
  • +
  • Un passage “Chaud” déclenche une suggestion de conversion en opportunité/affaire.
  • +
  • Le statut redevient “Tiède” si le score repasse sous 70 ou en cas d’inactivité prolongée.
  • +
+ +

6. Sources de tracking

+
    +
  • Emails : ouvertures, clics, réponses, désinscriptions, bounces.
  • +
  • Web : pages vues, événements (téléchargements, formulaires), sessions.
  • +
  • Activités CRM : appels, réunions, tâches, notes.
  • +
  • Import/Tags : qualification manuelle (ex. “Hot lead”).
  • +
+ +

7. Configuration rapide

+

7.1. Tracking emails

+
    +
  1. Configurer l’envoi SMTP et activer le suivi d’ouvertures/clics.
  2. +
  3. Vérifier le domaine d’envoi (SPF/DKIM/DMARC) pour une délivrabilité optimale.
  4. +
  5. Respecter l’opt-in et fournir un lien de désabonnement fonctionnel.
  6. +
+ +

7.2. Tracking web

+

Ajoutez le script de suivi sur votre site (dans le head) :

+
<script src="https://votre-domaine.example/tracker.js" data-site="VOTRE_SITE_ID" defer></script>
+

Associez un visiteur à un contact connu après un clic d’email ou une soumission de formulaire.

+ +

7.3. Pondérations et seuils

+
    +
  • Ajustez les points par événement selon votre cycle de vente.
  • +
  • Modifiez les seuils Froid/Tiède/Chaud si nécessaire (ex. marché longues décisions).
  • +
+ +

8. Rapports & KPI

+
    +
  • Distribution des scores (Froid/Tiède/Chaud).
  • +
  • Taux de chauffe (leads devenus “Chaud” par période).
  • +
  • Temps moyen pour passer de Froid à Chaud.
  • +
  • Top événements contribuant aux conversions.
  • +
+ +

9. RGPD & Conformité

+
    +
  • Collecter uniquement les données nécessaires (minimisation).
  • +
  • Informer des finalités de traitement (transparence) et permettre l’opposition.
  • +
  • Honorer immédiatement le droit d’opposition : désabonnement = arrêt des campagnes.
  • +
  • Documenter la base légale (consentement/intérêt légitime) et la durée de conservation.
  • +
+ +

10. Exemples pratiques

+

Exemple A : Ouverture (+3) + 2 clics (+16) + visite (+5) = 24 → reste “Froid”, planifier un appel de découverte.

+

Exemple B : Clic (+8) + réponse email (+25) + réunion (+20) = 53 → “Tiède”, passer en opportunité si qualification ok.

+

Exemple C : Demande de démo (+30) + appels (+5) + téléchargement (+12) = 47 → avec visites répétées (+10) passe “Tiède/haut”.

+
+ + + + diff --git a/documentation/taches.html b/documentation/taches.html new file mode 100644 index 0000000..688cf01 --- /dev/null +++ b/documentation/taches.html @@ -0,0 +1,48 @@ + + + + + + SuiteConsultance — Documentation Utilisateur + + + + + + +
+

Tâches & Rappels

+

La vue Tâches permet de centraliser vos actions : relances, appels, RDV… Chaque tâche peut être liée à un prospect ou un client.

+ +

Brouillons d’emails du jour

+

Le job jobs/daily_reminder_job.py crée des brouillons pour les tâches du jour, fondés sur un template choisi.

+
0 8 * * * /usr/bin/python3 /chemin/vers/le/projet/jobs/daily_reminder_job.py >> /var/log/reminder_job.log 2>&1
+
Les brouillons sont listés dans Tâches → Brouillons. Relisez et envoyez‑les manuellement.
+ +

Macros

+

Créez des macros pour générer rapidement des tâches récurrentes (ex. “J+2 : relancer”, “J+7 : second rappel”).

+
+ + + + \ No newline at end of file diff --git a/icon.icns b/icon.icns new file mode 100644 index 0000000..c5e8ea4 Binary files /dev/null and b/icon.icns differ diff --git a/icon.iconset/icon_128x128.png b/icon.iconset/icon_128x128.png new file mode 100644 index 0000000..b5b4280 Binary files /dev/null and b/icon.iconset/icon_128x128.png differ diff --git a/icon.iconset/icon_128x128@2x.png b/icon.iconset/icon_128x128@2x.png new file mode 100644 index 0000000..623c209 Binary files /dev/null and b/icon.iconset/icon_128x128@2x.png differ diff --git a/icon.iconset/icon_16x16.png b/icon.iconset/icon_16x16.png new file mode 100644 index 0000000..5310d30 Binary files /dev/null and b/icon.iconset/icon_16x16.png differ diff --git a/icon.iconset/icon_16x16@2x.png b/icon.iconset/icon_16x16@2x.png new file mode 100644 index 0000000..2252670 Binary files /dev/null and b/icon.iconset/icon_16x16@2x.png differ diff --git a/icon.iconset/icon_256x256.png b/icon.iconset/icon_256x256.png new file mode 100644 index 0000000..623c209 Binary files /dev/null and b/icon.iconset/icon_256x256.png differ diff --git a/icon.iconset/icon_256x256@2x.png b/icon.iconset/icon_256x256@2x.png new file mode 100644 index 0000000..648f446 Binary files /dev/null and b/icon.iconset/icon_256x256@2x.png differ diff --git a/icon.iconset/icon_32x32.png b/icon.iconset/icon_32x32.png new file mode 100644 index 0000000..2252670 Binary files /dev/null and b/icon.iconset/icon_32x32.png differ diff --git a/icon.iconset/icon_32x32@2x.png b/icon.iconset/icon_32x32@2x.png new file mode 100644 index 0000000..6ec42f4 Binary files /dev/null and b/icon.iconset/icon_32x32@2x.png differ diff --git a/icon.iconset/icon_512x512.png b/icon.iconset/icon_512x512.png new file mode 100644 index 0000000..648f446 Binary files /dev/null and b/icon.iconset/icon_512x512.png differ diff --git a/icon.iconset/icon_512x512@2x.png b/icon.iconset/icon_512x512@2x.png new file mode 100644 index 0000000..d9e00f8 Binary files /dev/null and b/icon.iconset/icon_512x512@2x.png differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..d9e00f8 Binary files /dev/null and b/icon.png differ diff --git a/install.py b/install.py new file mode 100644 index 0000000..23b0cb2 --- /dev/null +++ b/install.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import platform + +def check_python_version(): + """Vérifie que la version de Python est suffisante.""" + major, minor = sys.version_info[:2] + if major < 3 or (major == 3 and minor < 7): + print(f"Votre version de Python ({major}.{minor}) est trop ancienne.") + print("SuiteConsultance nécessite Python 3.7 ou supérieur.") + return False + return True + +def check_tk_version(): + """Vérifie la version de Tk.""" + try: + import tkinter as tk + root = tk.Tk() + tk_version = root.tk.call("info", "patchlevel") + root.destroy() + + major, minor = map(int, tk_version.split('.')[:2]) + print(f"Version de Tk détectée: {tk_version}") + + if major < 8 or (major == 8 and minor < 6): + print("SuiteConsultance fonctionne mieux avec Tk 8.6 ou supérieur.") + print("Certaines fonctionnalités d'interface utilisateur peuvent être limitées.") + return False + return True + except: + print("Impossible de déterminer la version de Tk.") + return False + +def update_pip(): + """Met à jour pip vers la dernière version.""" + try: + print("Mise à jour de pip...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + return True + except subprocess.CalledProcessError: + print("Erreur lors de la mise à jour de pip.") + return False + +def install_dependencies(): + """Installe les dépendances à partir du fichier requirements.txt.""" + requirements_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt") + + if not os.path.exists(requirements_path): + print(f"Erreur: Le fichier {requirements_path} n'existe pas.") + return False + + try: + print("Installation des dépendances...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_path]) + return True + except subprocess.CalledProcessError: + print("Erreur lors de l'installation des dépendances.") + return False + +def create_directories(): + """Crée les répertoires nécessaires s'ils n'existent pas.""" + directories = [ + "Data", + "Data/clients", + "Data/prospects", + "output", + "output/devis", + "output/propositions", + "output/factures" + ] + + for directory in directories: + dir_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory) + if not os.path.exists(dir_path): + print(f"Création du répertoire {directory}...") + os.makedirs(dir_path) + +def main(): + """Fonction principale.""" + print("=== Installation de SuiteConsultance ===") + + if not check_python_version(): + return + + has_tk = check_tk_version() + + update_pip() + + if install_dependencies(): + create_directories() + print("\n=== Installation terminée avec succès ===") + print("\nVous pouvez maintenant lancer SuiteConsultance avec la commande:") + print("python main.py") + + if not has_tk: + print("\nAttention: Votre version de Tk est obsolète.") + print("L'interface utilisateur peut avoir un aspect différent de celui prévu.") + else: + print("\n=== L'installation a échoué ===") + print("Veuillez vérifier les erreurs ci-dessus et réessayer.") + +if __name__ == "__main__": + main() diff --git a/jobs/daily_reminder_job.py b/jobs/daily_reminder_job.py new file mode 100644 index 0000000..e8c872c --- /dev/null +++ b/jobs/daily_reminder_job.py @@ -0,0 +1,100 @@ +""" +Job quotidien: détecte les tâches 'du jour' liées aux prospects +et génère des brouillons d'emails basés sur un template. +À exécuter via crontab, par exemple: + 0 8 * * * /usr/bin/python3 /chemin/vers/le/projet/jobs/daily_reminder_job.py >> /var/log/reminder_job.log 2>&1 +""" + +from datetime import date +import sys +import os + +# S'assurer que le projet est dans le PYTHONPATH quand lancé via cron +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from modules.tasks.task_handler import TaskHandler +from modules.crm.prospect_handler import ProspectHandler +from modules.email.email_manager import EmailTemplate +from modules.email.draft import EmailDraft +from modules.email.draft_handler import DraftHandler + + +DEFAULT_TEMPLATE_ID = "prospect_followup" + + +def _render_with_fallback(template_mgr: EmailTemplate, template_id: str, context: dict) -> dict: + """ + Rend un template via EmailTemplate. Si introuvable, fabrique un contenu par défaut. + Retourne dict {subject, content} + """ + rendered = template_mgr.render_template(template_id, context=context) + if rendered: + return rendered + # Fallback basique + subject = f"Relance {context.get('name') or ''}".strip() + content = ( + f"

Bonjour {context.get('name','')},

" + f"

Je me permets de revenir vers vous au sujet de {context.get('company','votre société')}." + f"

Restant à votre disposition,

" + ) + return {"subject": subject, "content": content} + + +def main(): + today = date.today().isoformat() + tasks = TaskHandler().list_today_tasks(status="todo") + # Filtrer uniquement les tâches liées aux prospects (interprétées comme 'rappel') + prospect_tasks = [t for t in tasks if t.entity_type == "prospect"] + + if not prospect_tasks: + return 0 + + prospect_handler = ProspectHandler() + template_mgr = EmailTemplate() + draft_handler = DraftHandler() + + created = 0 + for t in prospect_tasks: + # Évite doublons: un draft existe déjà pour cette tâche ? + if draft_handler.find_existing_for_task(t.id): + continue + + # Récupère prospect pour variables + prospect = prospect_handler.get_prospect_by_id(t.entity_id) if t.entity_id else None + if not prospect: + continue + + to_email = prospect.email or "" + if not to_email: + # pas de destinataire, aucun draft créé + continue + + context = { + "name": prospect.name, + "company": prospect.company or "", + } + rendered = _render_with_fallback(template_mgr, DEFAULT_TEMPLATE_ID, context) + + draft = EmailDraft( + prospect_id=prospect.id, + to_email=to_email, + subject=rendered["subject"], + content=rendered["content"], + status="draft", + template_id=DEFAULT_TEMPLATE_ID, + task_id=t.id, + metadata={ + "generated_for_date": today, + "task_title": t.title, + }, + ) + draft_handler.add_draft(draft) + created += 1 + + return created + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..b25570c --- /dev/null +++ b/main.py @@ -0,0 +1,90 @@ +from core.form import Form +from core.generator import Generator +from modules.proposition.app import main as generate_proposal +from modules.devis.app import main as generate_quote +from modules.crm.cli import main as manage_crm + +def fake_data(): + """ + Fonction pour générer des données factices. + """ + print("Génération de données factices...") + + # Données factices pour un client + fake_client_data = { + "client_name": "Fake Client", + "email": "fakeclient@example.com", + "telephone": "123456789", + "adresse": "123 Rue Imaginaire", + "project_name": "Projet de Test", + "project_type": "Développement Web", + "deadline": "06/09/2025", + "project_description": "Développement d'une application web pour la gestion de projet.", + "features": [ + {"description": "Gestion des utilisateurs"}, + {"description": "Tableau de bord"}, + {"description": "Rapports personnalisés"} + ], + "budget": "5000", + "payment_terms": "50% à la signature, 50% à la livraison", + "contact_info": "Contact principal : John Doe", + "additional_info": "Aucune information complémentaire.", + } + + # Sauvegarder les données factices dans un fichier JSON + from core.data import Data + data_manager = Data("Data/clients/fake_client.json") + data_manager.save_data(fake_client_data) + generator = Generator(fake_client_data) + content = generator.generate_pdf("propositions") + + print("\n✅ Proposition générée avec succès.") + + print("Données factices générées avec succès.") + +def display_title(): + """Affiche le titre de l'application""" + title = """ + ╔═══════════════════════════════════════════════════╗ + ║ SUITE CONSULTANCE ║ + ║ Gestion de cabinet de conseil ╚═══════════════════════════════════════════════════╝ + """ + print(title) + +def main(): + # Boucle principale pour le menu + while True: + # Effacer l'écran pour une meilleure lisibilité + import os + os.system('cls' if os.name == 'nt' else 'clear') + + display_title() + print("=== Menu Principal ===") + print("1. CRM - Gestion des clients") + print("2. Générer une proposition commerciale") + print("3. Générer un devis") + print("4. Générer des données de test") + print("5. Quitter") + + choice = input("\nSélectionnez une option (1-5): ") + + if choice == "1": + manage_crm() + elif choice == "2": + generate_proposal() + elif choice == "3": + generate_quote() + elif choice == "4": + fake_data() + elif choice == "5": + print("\nMerci d'avoir utilisé Suite Consultance. Au revoir!") + break + else: + print("\nChoix invalide, veuillez réessayer.") + + # Pause avant de revenir au menu principal (sauf après certaines actions) + if choice not in ["1"]: # Le CRM a sa propre gestion de retour au menu + input("\nAppuyez sur Entrée pour revenir au menu principal...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modules/crm/__init__.py/init.py b/modules/crm/__init__.py/init.py new file mode 100644 index 0000000..52f18dd --- /dev/null +++ b/modules/crm/__init__.py/init.py @@ -0,0 +1,5 @@ +from .client import Client +from .handler import ClientHandler +from .prospect import Prospect +from .prospect_handler import ProspectHandler +from .cli import main diff --git a/modules/crm/cli.py b/modules/crm/cli.py new file mode 100644 index 0000000..0efc6de --- /dev/null +++ b/modules/crm/cli.py @@ -0,0 +1,849 @@ +import os +from .client import Client +from .handler import ClientHandler +from .prospect import Prospect +from .prospect_handler import ProspectHandler +from datetime import date + +def clear_screen(): + """Efface l'écran du terminal""" + os.system('cls' if os.name == 'nt' else 'clear') + +def main(): + """Point d'entrée pour l'application CRM""" + client_handler = ClientHandler() + prospect_handler = ProspectHandler() + + while True: + clear_screen() + print("=== CRM - Gestion des Relations Clients ===\n") + + print("1. Gestion des Clients") + print("2. Gestion des Prospects") + print("3. Tableau de bord") + print("4. Retour au menu principal") + + choice = input("\nSélectionnez une option (1-4): ") + + if choice == "1": + # Gestion des clients + manage_clients(client_handler, prospect_handler) + elif choice == "2": + # Gestion des prospects + manage_prospects(client_handler, prospect_handler) + elif choice == "3": + # Tableau de bord + show_dashboard(client_handler, prospect_handler) + elif choice == "4": + # Retour au menu principal + print("\nRetour au menu principal...") + break + else: + print("\nOption invalide. Veuillez réessayer.") + + input("\nAppuyez sur Entrée pour continuer...") + +def display_all_clients(client_handler): + """Affiche tous les clients""" + clear_screen() + print("=== Liste des Clients ===\n") + + clients = client_handler.get_all_clients() + + if not clients: + print("Aucun client trouvé.") + return + + for i, client in enumerate(clients, 1): + print(f"{i}. {client.name} ({client.company})") + print(f" Email: {client.email} | Téléphone: {client.phone}") + print(f" Dernière interaction: {client.last_contact}") + print(f" Documents: Devis ({len(client.linked_docs['devis'])}) | " + f"Propositions ({len(client.linked_docs['propositions'])}) | " + f"Factures ({len(client.linked_docs['factures'])})") + print("") + +def search_clients(client_handler): + """Recherche des clients par nom""" + clear_screen() + print("=== Recherche de Clients ===\n") + + search_term = input("Entrez un terme de recherche (nom, email, entreprise): ") + + if not search_term: + print("Recherche annulée.") + return + + results = [] + clients = client_handler.get_all_clients() + + for client in clients: + if (search_term.lower() in client.name.lower() or + search_term.lower() in client.email.lower() or + search_term.lower() in client.company.lower()): + results.append(client) + + if not results: + print(f"Aucun client trouvé pour '{search_term}'.") + return + + print(f"\n{len(results)} client(s) trouvé(s):\n") + + for i, client in enumerate(results, 1): + print(f"{i}. {client.name} ({client.company})") + print(f" Email: {client.email} | Téléphone: {client.phone}") + print("") + + # Offrir la possibilité de voir les détails d'un client + if results: + choice = input("\nEntrez le numéro du client pour voir les détails (ou Entrée pour annuler): ") + if choice.isdigit() and 1 <= int(choice) <= len(results): + client = results[int(choice) - 1] + display_client_details(client) + +def add_new_client(client_handler): + """Ajoute un nouveau client""" + clear_screen() + print("=== Ajout d'un Nouveau Client ===\n") + + # Recueillir les informations du client + name = input("Nom du client: ") + if not name: + print("Le nom est obligatoire. Opération annulée.") + return + + company = input("Entreprise: ") + email = input("Email: ") + phone = input("Téléphone: ") + notes = input("Notes: ") + + # Créer le nouveau client + new_client = Client( + name=name, + company=company, + email=email, + phone=phone, + notes=notes + ) + + # Ajouter le client + client_handler.add_client(new_client) + print(f"\nClient '{name}' ajouté avec succès!") + +def edit_client(client_handler): + """Modifie un client existant""" + clear_screen() + print("=== Modification d'un Client ===\n") + + # Afficher tous les clients pour sélection + clients = client_handler.get_all_clients() + + if not clients: + print("Aucun client trouvé.") + return + + for i, client in enumerate(clients, 1): + print(f"{i}. {client.name} ({client.company})") + + # Sélectionner un client à modifier + choice = input("\nEntrez le numéro du client à modifier (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients): + print("Opération annulée ou sélection invalide.") + return + + client = clients[int(choice) - 1] + + # Afficher les détails actuels + print(f"\nModification de '{client.name}':") + print(f"1. Nom: {client.name}") + print(f"2. Entreprise: {client.company}") + print(f"3. Email: {client.email}") + print(f"4. Téléphone: {client.phone}") + print(f"5. Notes: {client.notes}") + print("6. Tout modifier") + print("7. Annuler") + + # Choisir ce qu'il faut modifier + edit_choice = input("\nQue souhaitez-vous modifier (1-7): ") + + if edit_choice == "7": + print("Modification annulée.") + return + + if edit_choice == "1" or edit_choice == "6": + client.name = input(f"Nouveau nom [{client.name}]: ") or client.name + + if edit_choice == "2" or edit_choice == "6": + client.company = input(f"Nouvelle entreprise [{client.company}]: ") or client.company + + if edit_choice == "3" or edit_choice == "6": + client.email = input(f"Nouvel email [{client.email}]: ") or client.email + + if edit_choice == "4" or edit_choice == "6": + client.phone = input(f"Nouveau téléphone [{client.phone}]: ") or client.phone + + if edit_choice == "5" or edit_choice == "6": + client.notes = input(f"Nouvelles notes [{client.notes}]: ") or client.notes + + # Mettre à jour le client + client_handler.update_client(client) + print(f"\nClient '{client.name}' mis à jour avec succès!") + +def delete_client(client_handler): + """Supprime un client""" + clear_screen() + print("=== Suppression d'un Client ===\n") + + # Afficher tous les clients pour sélection + clients = client_handler.get_all_clients() + + if not clients: + print("Aucun client trouvé.") + return + + for i, client in enumerate(clients, 1): + print(f"{i}. {client.name} ({client.company})") + + # Sélectionner un client à supprimer + choice = input("\nEntrez le numéro du client à supprimer (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients): + print("Opération annulée ou sélection invalide.") + return + + client = clients[int(choice) - 1] + + # Confirmer la suppression + confirm = input(f"Êtes-vous sûr de vouloir supprimer '{client.name}'? (o/n): ") + + if confirm.lower() != 'o': + print("Suppression annulée.") + return + + # Supprimer le client + client_handler.delete_client(client.id) + print(f"\nClient '{client.name}' supprimé avec succès!") + +def view_client_documents(client_handler): + """Affiche les documents associés à un client""" + clear_screen() + print("=== Documents d'un Client ===\n") + + # Afficher tous les clients pour sélection + clients = client_handler.get_all_clients() + + if not clients: + print("Aucun client trouvé.") + return + + for i, client in enumerate(clients, 1): + print(f"{i}. {client.name} ({client.company})") + + # Sélectionner un client + choice = input("\nEntrez le numéro du client pour voir ses documents (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients): + print("Opération annulée ou sélection invalide.") + return + + client = clients[int(choice) - 1] + display_client_documents(client) + +def display_client_details(client): + """Affiche les détails d'un client""" + clear_screen() + print(f"=== Détails du Client: {client.name} ===\n") + + print(f"ID: {client.id}") + print(f"Nom: {client.name}") + print(f"Entreprise: {client.company}") + print(f"Email: {client.email}") + print(f"Téléphone: {client.phone}") + print(f"Notes: {client.notes}") + print(f"Dernière interaction: {client.last_contact}") + print(f"Prochaine action: {client.next_action}") + + if client.tags: + print(f"Tags: {', '.join(client.tags)}") + + # Afficher les documents + display_client_documents(client) + +def display_client_documents(client): + """Affiche les documents associés à un client""" + print(f"\n=== Documents de {client.name} ===\n") + + devis = client.linked_docs.get('devis', []) + propositions = client.linked_docs.get('propositions', []) + factures = client.linked_docs.get('factures', []) + + if not devis and not propositions and not factures: + print("Aucun document trouvé pour ce client.") + return + + if devis: + print("Devis:") + for i, doc in enumerate(devis, 1): + print(f" {i}. {os.path.basename(doc)}") + + if propositions: + print("\nPropositions commerciales:") + for i, doc in enumerate(propositions, 1): + print(f" {i}. {os.path.basename(doc)}") + + if factures: + print("\nFactures:") + for i, doc in enumerate(factures, 1): + print(f" {i}. {os.path.basename(doc)}") + + # Option pour ouvrir un document + print("\n1. Ouvrir un document") + print("2. Retour") + + choice = input("\nQue souhaitez-vous faire (1-2): ") + + if choice == "1": + # Choisir le type de document + print("\nType de document:") + print("1. Devis") + print("2. Proposition commerciale") + print("3. Facture") + + doc_type_choice = input("\nSélectionnez le type de document (1-3): ") + + doc_list = [] + if doc_type_choice == "1" and devis: + doc_list = devis + doc_type_name = "devis" + elif doc_type_choice == "2" and propositions: + doc_list = propositions + doc_type_name = "proposition commerciale" + elif doc_type_choice == "3" and factures: + doc_list = factures + doc_type_name = "facture" + else: + print("Choix invalide ou aucun document de ce type.") + return + + # Afficher les documents du type sélectionné + print(f"\n{doc_type_name.capitalize()}s disponibles:") + for i, doc in enumerate(doc_list, 1): + print(f" {i}. {os.path.basename(doc)}") + + # Sélectionner un document à ouvrir + doc_choice = input(f"\nSélectionnez un {doc_type_name} à ouvrir (1-{len(doc_list)}): ") + + if doc_choice.isdigit() and 1 <= int(doc_choice) <= len(doc_list): + selected_doc = doc_list[int(doc_choice) - 1] + open_document(selected_doc) + else: + print("Sélection invalide.") + +def open_document(document_path): + """Ouvre un document PDF""" + try: + if os.path.exists(document_path): + if os.name == 'nt': # Windows + os.startfile(document_path) + elif os.name == 'posix': # macOS et Linux + if 'darwin' in os.uname().sysname.lower(): # macOS + os.system(f'open "{document_path}"') + else: # Linux + os.system(f'xdg-open "{document_path}"') + print(f"Document ouvert: {os.path.basename(document_path)}") + else: + print(f"Erreur: Le document {document_path} n'existe pas.") + except Exception as e: + print(f"Erreur lors de l'ouverture du document: {e}") + +def manage_clients(client_handler, prospect_handler): + """Gestion des clients""" + while True: + clear_screen() + print("=== Gestion des Clients ===\n") + + print("1. Afficher tous les clients") + print("2. Rechercher un client") + print("3. Ajouter un nouveau client") + print("4. Modifier un client") + print("5. Supprimer un client") + print("6. Voir les documents d'un client") + print("7. Retour") + + choice = input("\nSélectionnez une option (1-7): ") + + if choice == "1": + # Afficher tous les clients + display_all_clients(client_handler) + elif choice == "2": + # Rechercher un client + search_clients(client_handler) + elif choice == "3": + # Ajouter un nouveau client + add_new_client(client_handler) + elif choice == "4": + # Modifier un client + edit_client(client_handler) + elif choice == "5": + # Supprimer un client + delete_client(client_handler) + elif choice == "6": + # Voir les documents d'un client + view_client_documents(client_handler) + elif choice == "7": + # Retour + break + else: + print("\nOption invalide. Veuillez réessayer.") + + input("\nAppuyez sur Entrée pour continuer...") + +def manage_prospects(client_handler, prospect_handler): + """Gestion des prospects""" + while True: + clear_screen() + print("=== Gestion des Prospects ===\n") + + print("1. Afficher tous les prospects") + print("2. Rechercher un prospect") + print("3. Ajouter un nouveau prospect") + print("4. Modifier un prospect") + print("5. Supprimer un prospect") + print("6. Convertir un prospect en client") + print("7. Afficher les prospects par statut") + print("8. Retour") + + choice = input("\nSélectionnez une option (1-8): ") + + if choice == "1": + # Afficher tous les prospects + display_all_prospects(prospect_handler) + elif choice == "2": + # Rechercher un prospect + search_prospects(prospect_handler) + elif choice == "3": + # Ajouter un nouveau prospect + add_new_prospect(prospect_handler) + elif choice == "4": + # Modifier un prospect + edit_prospect(prospect_handler) + elif choice == "5": + # Supprimer un prospect + delete_prospect(prospect_handler) + elif choice == "6": + # Convertir un prospect en client + convert_prospect_to_client(prospect_handler, client_handler) + elif choice == "7": + # Afficher les prospects par statut + display_prospects_by_status(prospect_handler) + elif choice == "8": + # Retour + break + else: + print("\nOption invalide. Veuillez réessayer.") + + input("\nAppuyez sur Entrée pour continuer...") + +def show_dashboard(client_handler, prospect_handler): + """Affiche un tableau de bord avec des statistiques""" + clear_screen() + print("=== Tableau de Bord CRM ===\n") + + # Récupérer les données + clients = client_handler.get_all_clients() + prospects = prospect_handler.get_all_prospects() + + # Statistiques de base + print(f"Nombre total de clients: {len(clients)}") + print(f"Nombre total de prospects: {len(prospects)}") + + # Statistiques des prospects par statut + statuses = {} + for prospect in prospects: + status = prospect.status + if status in statuses: + statuses[status] += 1 + else: + statuses[status] = 1 + + if statuses: + print("\nProspects par statut:") + for status, count in statuses.items(): + print(f" {status}: {count}") + + # Derniers prospects ajoutés + recent_prospects = prospect_handler.get_recent_prospects(days=30) + if recent_prospects: + print(f"\nNouveaux prospects (30 derniers jours): {len(recent_prospects)}") + + # Activité récente + print("\nRécapitulatif des activités récentes:") + print(" Propositions commerciales en attente...") + print(" Devis en cours...") + + # Suggestions d'actions + print("\nSuggestions d'actions:") + if len(prospects) > 0: + print(" • Suivez vos prospects pour augmenter votre taux de conversion") + if len(clients) > 0: + print(" • Contactez vos clients existants pour des opportunités de vente additionnelle") + if len(prospects) == 0: + print(" • Ajoutez des prospects pour développer votre portefeuille client") + +def display_all_prospects(prospect_handler): + """Affiche tous les prospects""" + clear_screen() + print("=== Liste des Prospects ===\n") + + prospects = prospect_handler.get_all_prospects() + + if not prospects: + print("Aucun prospect trouvé.") + return + + for i, prospect in enumerate(prospects, 1): + print(f"{i}. {prospect.name} ({prospect.company})") + print(f" Email: {prospect.email} | Téléphone: {prospect.phone}") + print(f" Statut: {prospect.status} | Source: {prospect.source}") + print(f" Dernière interaction: {prospect.last_contact}") + print("") + +def search_prospects(prospect_handler): + """Recherche des prospects par nom""" + clear_screen() + print("=== Recherche de Prospects ===\n") + + search_term = input("Entrez un terme de recherche (nom, email, entreprise): ") + + if not search_term: + print("Recherche annulée.") + return + + results = [] + prospects = prospect_handler.get_all_prospects() + + for prospect in prospects: + if (search_term.lower() in prospect.name.lower() or + search_term.lower() in prospect.email.lower() or + search_term.lower() in prospect.company.lower()): + results.append(prospect) + + if not results: + print(f"Aucun prospect trouvé pour '{search_term}'.") + return + + print(f"\n{len(results)} prospect(s) trouvé(s):\n") + + for i, prospect in enumerate(results, 1): + print(f"{i}. {prospect.name} ({prospect.company})") + print(f" Email: {prospect.email} | Téléphone: {prospect.phone}") + print(f" Statut: {prospect.status}") + print("") + + # Offrir la possibilité de voir les détails d'un prospect + if results: + choice = input("\nEntrez le numéro du prospect pour voir les détails (ou Entrée pour annuler): ") + if choice.isdigit() and 1 <= int(choice) <= len(results): + prospect = results[int(choice) - 1] + display_prospect_details(prospect) + +def add_new_prospect(prospect_handler): + """Ajoute un nouveau prospect""" + clear_screen() + print("=== Ajout d'un Nouveau Prospect ===\n") + + # Recueillir les informations du prospect + name = input("Nom du prospect: ") + if not name: + print("Le nom est obligatoire. Opération annulée.") + return + + company = input("Entreprise: ") + email = input("Email: ") + phone = input("Téléphone: ") + source = input("Source (site web, référence, salon, etc.): ") + notes = input("Notes: ") + + # Statut du prospect + print("\nStatut du prospect:") + print("1. Nouveau") + print("2. Contacté") + print("3. Qualifié") + print("4. Proposition envoyée") + print("5. Non intéressé") + + status_choice = input("\nSélectionnez le statut (1-5): ") + status = "Nouveau" # Défaut + + if status_choice == "1": + status = "Nouveau" + elif status_choice == "2": + status = "Contacté" + elif status_choice == "3": + status = "Qualifié" + elif status_choice == "4": + status = "Proposition envoyée" + elif status_choice == "5": + status = "Non intéressé" + + # Créer le nouveau prospect + new_prospect = Prospect( + name=name, + company=company, + email=email, + phone=phone, + source=source, + notes=notes, + status=status + ) + + # Ajouter le prospect + prospect_handler.add_prospect(new_prospect) + print(f"\nProspect '{name}' ajouté avec succès!") + +def edit_prospect(prospect_handler): + """Modifie un prospect existant""" + clear_screen() + print("=== Modification d'un Prospect ===\n") + + # Afficher tous les prospects pour sélection + prospects = prospect_handler.get_all_prospects() + + if not prospects: + print("Aucun prospect trouvé.") + return + + for i, prospect in enumerate(prospects, 1): + print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}") + + # Sélectionner un prospect à modifier + choice = input("\nEntrez le numéro du prospect à modifier (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects): + print("Opération annulée ou sélection invalide.") + return + + prospect = prospects[int(choice) - 1] + + # Afficher les détails actuels + print(f"\nModification de '{prospect.name}':") + print(f"1. Nom: {prospect.name}") + print(f"2. Entreprise: {prospect.company}") + print(f"3. Email: {prospect.email}") + print(f"4. Téléphone: {prospect.phone}") + print(f"5. Source: {prospect.source}") + print(f"6. Statut: {prospect.status}") + print(f"7. Notes: {prospect.notes}") + print("8. Tout modifier") + print("9. Annuler") + + # Choisir ce qu'il faut modifier + edit_choice = input("\nQue souhaitez-vous modifier (1-9): ") + + if edit_choice == "9": + print("Modification annulée.") + return + + if edit_choice == "1" or edit_choice == "8": + prospect.name = input(f"Nouveau nom [{prospect.name}]: ") or prospect.name + + if edit_choice == "2" or edit_choice == "8": + prospect.company = input(f"Nouvelle entreprise [{prospect.company}]: ") or prospect.company + + if edit_choice == "3" or edit_choice == "8": + prospect.email = input(f"Nouvel email [{prospect.email}]: ") or prospect.email + + if edit_choice == "4" or edit_choice == "8": + prospect.phone = input(f"Nouveau téléphone [{prospect.phone}]: ") or prospect.phone + + if edit_choice == "5" or edit_choice == "8": + prospect.source = input(f"Nouvelle source [{prospect.source}]: ") or prospect.source + + if edit_choice == "6" or edit_choice == "8": + print("\nStatut du prospect:") + print("1. Nouveau") + print("2. Contacté") + print("3. Qualifié") + print("4. Proposition envoyée") + print("5. Non intéressé") + + status_choice = input(f"\nSélectionnez le statut (1-5) [Actuel: {prospect.status}]: ") + + if status_choice == "1": + prospect.status = "Nouveau" + elif status_choice == "2": + prospect.status = "Contacté" + elif status_choice == "3": + prospect.status = "Qualifié" + elif status_choice == "4": + prospect.status = "Proposition envoyée" + elif status_choice == "5": + prospect.status = "Non intéressé" + + if edit_choice == "7" or edit_choice == "8": + prospect.notes = input(f"Nouvelles notes [{prospect.notes}]: ") or prospect.notes + + # Mettre à jour le prospect + prospect_handler.update_prospect(prospect) + print(f"\nProspect '{prospect.name}' mis à jour avec succès!") + +def delete_prospect(prospect_handler): + """Supprime un prospect""" + clear_screen() + print("=== Suppression d'un Prospect ===\n") + + # Afficher tous les prospects pour sélection + prospects = prospect_handler.get_all_prospects() + + if not prospects: + print("Aucun prospect trouvé.") + return + + for i, prospect in enumerate(prospects, 1): + print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}") + + # Sélectionner un prospect à supprimer + choice = input("\nEntrez le numéro du prospect à supprimer (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects): + print("Opération annulée ou sélection invalide.") + return + + prospect = prospects[int(choice) - 1] + + # Confirmer la suppression + confirm = input(f"Êtes-vous sûr de vouloir supprimer '{prospect.name}'? (o/n): ") + + if confirm.lower() != 'o': + print("Suppression annulée.") + return + + # Supprimer le prospect + prospect_handler.delete_prospect(prospect.id) + print(f"\nProspect '{prospect.name}' supprimé avec succès!") + +def convert_prospect_to_client(prospect_handler, client_handler): + """Convertit un prospect en client""" + clear_screen() + print("=== Conversion d'un Prospect en Client ===\n") + + # Afficher tous les prospects pour sélection + prospects = prospect_handler.get_all_prospects() + + if not prospects: + print("Aucun prospect trouvé.") + return + + for i, prospect in enumerate(prospects, 1): + print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}") + + # Sélectionner un prospect à convertir + choice = input("\nEntrez le numéro du prospect à convertir en client (ou Entrée pour annuler): ") + if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects): + print("Opération annulée ou sélection invalide.") + return + + prospect = prospects[int(choice) - 1] + + # Confirmer la conversion + confirm = input(f"Êtes-vous sûr de vouloir convertir '{prospect.name}' en client? (o/n): ") + + if confirm.lower() != 'o': + print("Conversion annulée.") + return + + # Convertir le prospect en client + client = prospect_handler.convert_to_client(prospect.id, client_handler) + if client: + print(f"\nProspect '{prospect.name}' converti en client avec succès!") + else: + print("\nErreur lors de la conversion du prospect en client.") + +def display_prospect_details(prospect): + """Affiche les détails d'un prospect""" + clear_screen() + print(f"=== Détails du Prospect: {prospect.name} ===\n") + + print(f"ID: {prospect.id}") + print(f"Nom: {prospect.name}") + print(f"Entreprise: {prospect.company}") + print(f"Email: {prospect.email}") + print(f"Téléphone: {prospect.phone}") + print(f"Source: {prospect.source}") + print(f"Statut: {prospect.status}") + print(f"Notes: {prospect.notes}") + print(f"Dernière interaction: {prospect.last_contact}") + print(f"Prochaine action: {prospect.next_action}") + + if prospect.tags: + print(f"Tags: {', '.join(prospect.tags)}") + +def display_prospects_by_status(prospect_handler): + """Affiche les prospects regroupés par statut""" + clear_screen() + print("=== Prospects par Statut ===\n") + + # Récupérer tous les prospects + prospects = prospect_handler.get_all_prospects() + + if not prospects: + print("Aucun prospect trouvé.") + return + + # Regrouper les prospects par statut + status_groups = {} + for prospect in prospects: + status = prospect.status + if status not in status_groups: + status_groups[status] = [] + status_groups[status].append(prospect) + + # Afficher les prospects par groupe de statut + for status, prospects_list in status_groups.items(): + print(f"\n=== {status} ({len(prospects_list)}) ===") + for i, prospect in enumerate(prospects_list, 1): + print(f"{i}. {prospect.name} ({prospect.company})") + print(f" Email: {prospect.email} | Téléphone: {prospect.phone}") + + print("\n") + + # Option pour filtrer par un statut spécifique + print("Voulez-vous voir les détails d'un statut spécifique?") + print("1. Nouveau") + print("2. Contacté") + print("3. Qualifié") + print("4. Proposition envoyée") + print("5. Non intéressé") + print("6. Retour") + + choice = input("\nSélectionnez une option (1-6): ") + + status_mapping = { + "1": "Nouveau", + "2": "Contacté", + "3": "Qualifié", + "4": "Proposition envoyée", + "5": "Non intéressé" + } + + if choice in status_mapping: + selected_status = status_mapping[choice] + filtered_prospects = prospect_handler.get_prospects_by_status(selected_status) + + if filtered_prospects: + clear_screen() + print(f"=== Prospects avec statut: {selected_status} ===\n") + + for i, prospect in enumerate(filtered_prospects, 1): + print(f"{i}. {prospect.name} ({prospect.company})") + print(f" Email: {prospect.email} | Téléphone: {prospect.phone}") + print(f" Dernière interaction: {prospect.last_contact}") + print(f" Notes: {prospect.notes[:50]}..." if len(prospect.notes) > 50 else f" Notes: {prospect.notes}") + print("") + + # Option pour voir les détails d'un prospect spécifique + detail_choice = input("\nEntrez le numéro du prospect pour voir les détails (ou Entrée pour annuler): ") + if detail_choice.isdigit() and 1 <= int(detail_choice) <= len(filtered_prospects): + prospect = filtered_prospects[int(detail_choice) - 1] + display_prospect_details(prospect) + else: + print(f"\nAucun prospect avec le statut '{selected_status}' trouvé.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modules/crm/client.py b/modules/crm/client.py new file mode 100644 index 0000000..068742c --- /dev/null +++ b/modules/crm/client.py @@ -0,0 +1,54 @@ +from datetime import date +import uuid + +from datetime import date +import uuid + +class Client: + def __init__( + self, + name: str, + company: str = "", + email: str = "", + phone: str = "", + notes: str = "", + tags: list = None, + last_contact: str = None, + next_action: str = "", + linked_docs: dict = None, + id: str = None, + created_at: str = None + ): + self.id = id or f"cli_{uuid.uuid4().hex[:8]}" + self.name = name + self.company = company + self.email = email + self.phone = phone + self.notes = notes + self.tags = tags or [] + self.last_contact = last_contact or str(date.today()) + self.next_action = next_action + self.linked_docs = linked_docs or {"devis": [], "propositions": [], "factures": []} + self.created_at = created_at or str(date.today()) + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "company": self.company, + "email": self.email, + "phone": self.phone, + "notes": self.notes, + "tags": self.tags, + "last_contact": self.last_contact, + "next_action": self.next_action, + "linked_docs": self.linked_docs, + "created_at": self.created_at + } + + @staticmethod + def from_dict(data: dict): + return Client(**data) + + def __str__(self): + return f"Client({self.name}, {self.company}, {self.email}, {self.phone})" \ No newline at end of file diff --git a/modules/crm/handler.py b/modules/crm/handler.py new file mode 100644 index 0000000..68b712c --- /dev/null +++ b/modules/crm/handler.py @@ -0,0 +1,172 @@ +from datetime import date +import os +import json +from .client import Client +from core.data import Data + +class ClientHandler: + def __init__(self, clients_folder="Data/clients"): + self.clients_folder = clients_folder + self.clients = [] + self.load_clients() + + def load_clients(self): + """Charge tous les clients depuis les fichiers existants""" + self.clients = [] + # Charger les clients depuis les fichiers JSON existants + if os.path.exists(self.clients_folder): + for filename in os.listdir(self.clients_folder): + if filename.endswith('.json'): + client_path = os.path.join(self.clients_folder, filename) + try: + # Utiliser le gestionnaire de données pour charger le fichier JSON + data_manager = Data(client_path) + client_data = data_manager.load_data() + + # Si le fichier a la structure d'un client, créer un objet Client + if "client_name" in client_data: + # Conversion du format existant vers le format Client + client_obj = { + "name": client_data.get("client_name", ""), + "email": client_data.get("client_email", ""), + "phone": client_data.get("client_phone", ""), + "notes": client_data.get("additional_info", ""), + "company": client_data.get("project_name", ""), + "tags": [], # Pas de tags dans les données existantes + "next_action": "", # Pas d'action suivante dans les données existantes + "linked_docs": self._find_linked_docs(client_data.get("client_name", "")) + } + client = Client(**client_obj) + self.clients.append(client) + except Exception as e: + print(f"Erreur lors du chargement du client {filename}: {e}") + + return self.clients + + def _find_linked_docs(self, client_name): + """Trouve les documents associés à un client""" + linked_docs = {"devis": [], "propositions": [], "factures": []} + client_name_normalized = client_name.replace(" ", "_").lower() + + # Rechercher les devis + devis_folder = "output/devis" + if os.path.exists(devis_folder): + for filename in os.listdir(devis_folder): + if filename.startswith(client_name_normalized) and filename.endswith(".pdf"): + linked_docs["devis"].append(os.path.join(devis_folder, filename)) + + # Rechercher les propositions + propositions_folder = "output/propositions" + if os.path.exists(propositions_folder): + for filename in os.listdir(propositions_folder): + if filename.startswith(client_name_normalized) and filename.endswith(".pdf"): + linked_docs["propositions"].append(os.path.join(propositions_folder, filename)) + + # Rechercher les factures (si elles existent) + factures_folder = "output/factures" + if os.path.exists(factures_folder): + for filename in os.listdir(factures_folder): + if filename.startswith(client_name_normalized) and filename.endswith(".pdf"): + linked_docs["factures"].append(os.path.join(factures_folder, filename)) + + return linked_docs + + def get_all_clients(self): + """Retourne tous les clients""" + return self.clients + + def get_client_by_id(self, client_id): + """Retourne un client par son ID""" + for client in self.clients: + if client.id == client_id: + return client + return None + + def get_client_by_name(self, client_name): + """Retourne un client par son nom""" + for client in self.clients: + if client.name.lower() == client_name.lower(): + return client + return None + + def add_client(self, client): + """Ajoute un nouveau client""" + self.clients.append(client) + # Sauvegarder le client dans un fichier JSON + self._save_client(client) + return client + + def update_client(self, client): + """Met à jour un client existant""" + for i, existing_client in enumerate(self.clients): + if existing_client.id == client.id: + self.clients[i] = client + # Sauvegarder le client mis à jour + self._save_client(client) + return client + return None + + def delete_client(self, client_id): + """Supprime un client""" + for i, client in enumerate(self.clients): + if client.id == client_id: + deleted_client = self.clients.pop(i) + # Supprimer le fichier JSON du client + client_file = os.path.join(self.clients_folder, f"{deleted_client.name.replace(' ', '_').lower()}.json") + if os.path.exists(client_file): + os.remove(client_file) + return deleted_client + return None + + def _save_client(self, client): + """Sauvegarde un client dans un fichier JSON""" + client_data = { + "client_name": client.name, + "client_email": client.email, + "client_phone": client.phone, + "additional_info": client.notes, + "project_name": client.company, + # Autres champs pour maintenir la compatibilité avec le format existant + "project_type": "", + "project_description": "", + "features": [], + "budget": "", + "payment_terms": "", + "contact_info": f"{client.name}, {client.phone}", + "deadline": "" + } + + client_file = os.path.join(self.clients_folder, f"{client.name.replace(' ', '_').lower()}.json") + data_manager = Data(client_file) + data_manager.save_data(client_data) + + return client_file + + def add_task_for_client(self, client_id: str, title: str, due_date: str, description: str = "", priority: str = "normale", metadata: dict = None): + """ + Crée une tâche liée à un client. + :param client_id: ID du client + :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 ou None si client introuvable + """ + client = self.get_client_by_id(client_id) + if not client: + return None + # Import local pour éviter dépendances circulaires au chargement + 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="client", + entity_id=client_id, + priority=priority, + metadata=metadata or {}, + ) + return TaskHandler().add_task(task) \ No newline at end of file diff --git a/modules/crm/prospect.py b/modules/crm/prospect.py new file mode 100644 index 0000000..63d6da4 --- /dev/null +++ b/modules/crm/prospect.py @@ -0,0 +1,95 @@ +from datetime import date +import uuid +from typing import Union + +from .status import ProspectStatus + + +class Prospect: + def __init__( + self, + name: str, + company: str = "", + email: str = "", + phone: str = "", + source: str = "", # D'où vient ce prospect (site web, référence, etc.) + notes: str = "", + status: Union[ProspectStatus, str] = ProspectStatus.NOUVEAU, # Nouveau, Contacté, Qualifié, Proposition, Non intéressé + tags: list = None, + last_contact: str = None, + next_action: str = "", + linked_docs: dict = None, + id: str = None, + created_at: str = None, + score: int = 0, + last_interaction: str = None + ): + self.id = id or f"pros_{uuid.uuid4().hex[:8]}" + self.name = name + self.company = company + self.email = email + self.phone = phone + self.source = source + self.notes = notes + # Stockage interne en Enum + self._status: ProspectStatus = ProspectStatus.from_value(status) + self.tags = tags or [] + self.last_contact = last_contact or str(date.today()) + self.next_action = next_action + self.linked_docs = linked_docs or {"propositions": []} + self.created_at = created_at or str(date.today()) + self.score = score or 0 + self.last_interaction = last_interaction + + @property + def status(self) -> str: + """Exposition publique du statut en chaîne pour compatibilité (templates, JSON).""" + return self._status.value + + @status.setter + def status(self, value: Union[ProspectStatus, str]) -> None: + """Permet d'assigner soit une chaîne, soit l'enum directement.""" + self._status = ProspectStatus.from_value(value) + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "company": self.company, + "email": self.email, + "phone": self.phone, + "source": self.source, + "notes": self.notes, + "status": self.status, # sérialise en chaîne + "tags": self.tags, + "last_contact": self.last_contact, + "next_action": self.next_action, + "linked_docs": self.linked_docs, + "created_at": self.created_at, + "score": self.score, + "last_interaction": self.last_interaction + } + + @staticmethod + def from_dict(data: dict): + # __init__ gère la conversion status -> Enum + return Prospect(**data) + + def convert_to_client(self): + """Convertit ce prospect en client""" + from .client import Client + + return Client( + name=self.name, + company=self.company, + email=self.email, + phone=self.phone, + notes=self.notes, + tags=self.tags, + last_contact=self.last_contact, + next_action=self.next_action, + linked_docs=self.linked_docs + ) + + def __str__(self): + return f"Prospect({self.name}, {self.company}, {self.status}, {self.email})" diff --git a/modules/crm/prospect_handler.py b/modules/crm/prospect_handler.py new file mode 100644 index 0000000..e1463e2 --- /dev/null +++ b/modules/crm/prospect_handler.py @@ -0,0 +1,157 @@ +from datetime import date +import os +import json +from .prospect import Prospect +from core.data import Data + +class ProspectHandler: + def __init__(self, prospects_folder="Data/prospects"): + self.prospects_folder = prospects_folder + self.prospects = [] + + # Créer le dossier des prospects s'il n'existe pas + if not os.path.exists(self.prospects_folder): + os.makedirs(self.prospects_folder) + + self.load_prospects() + + def load_prospects(self): + """Charge tous les prospects depuis les fichiers existants""" + self.prospects = [] + # Charger les prospects depuis les fichiers JSON existants + if os.path.exists(self.prospects_folder): + for filename in os.listdir(self.prospects_folder): + if filename.endswith('.json'): + prospect_path = os.path.join(self.prospects_folder, filename) + try: + # Utiliser le gestionnaire de données pour charger le fichier JSON + data_manager = Data(prospect_path) + prospect_data = data_manager.load_data() + + # Si le fichier a la structure d'un prospect, créer un objet Prospect + if "name" in prospect_data: + prospect = Prospect.from_dict(prospect_data) + self.prospects.append(prospect) + except Exception as e: + print(f"Erreur lors du chargement du prospect {filename}: {e}") + + return self.prospects + + def get_all_prospects(self): + """Retourne tous les prospects""" + return self.prospects + + def get_prospect_by_id(self, prospect_id): + """Retourne un prospect par son ID""" + for prospect in self.prospects: + if prospect.id == prospect_id: + return prospect + return None + + def get_prospect_by_name(self, prospect_name): + """Retourne un prospect par son nom""" + for prospect in self.prospects: + if prospect.name.lower() == prospect_name.lower(): + return prospect + return None + + def add_prospect(self, prospect): + """Ajoute un nouveau prospect""" + self.prospects.append(prospect) + # Sauvegarder le prospect dans un fichier JSON + self._save_prospect(prospect) + return prospect + + def update_prospect(self, prospect): + """Met à jour un prospect existant""" + for i, existing_prospect in enumerate(self.prospects): + if existing_prospect.id == prospect.id: + self.prospects[i] = prospect + # Sauvegarder le prospect mis à jour + self._save_prospect(prospect) + return prospect + return None + + def delete_prospect(self, prospect_id): + """Supprime un prospect""" + for i, prospect in enumerate(self.prospects): + if prospect.id == prospect_id: + deleted_prospect = self.prospects.pop(i) + # Supprimer le fichier JSON du prospect + prospect_file = os.path.join(self.prospects_folder, f"{deleted_prospect.id}.json") + if os.path.exists(prospect_file): + os.remove(prospect_file) + return deleted_prospect + return None + + def convert_to_client(self, prospect_id, client_handler): + """Convertit un prospect en client et le supprime de la liste des prospects""" + prospect = self.get_prospect_by_id(prospect_id) + if prospect: + # Créer un client à partir du prospect + client = prospect.convert_to_client() + # Ajouter le client + client_handler.add_client(client) + # Supprimer le prospect + self.delete_prospect(prospect_id) + return client + return None + + def _save_prospect(self, prospect): + """Sauvegarde un prospect dans un fichier JSON""" + prospect_data = prospect.to_dict() + + prospect_file = os.path.join(self.prospects_folder, f"{prospect.id}.json") + data_manager = Data(prospect_file) + data_manager.save_data(prospect_data) + + return prospect_file + + def get_prospects_by_status(self, status): + """Retourne les prospects par statut""" + return [p for p in self.prospects if p.status.lower() == status.lower()] + + def get_recent_prospects(self, days=30): + """Retourne les prospects créés dans les X derniers jours""" + today = date.today() + recent_prospects = [] + + for prospect in self.prospects: + try: + created_date = date.fromisoformat(prospect.created_at) + delta = today - created_date + if delta.days <= days: + recent_prospects.append(prospect) + except ValueError: + # Si la date n'est pas au format ISO, ignorer ce prospect + pass + + return recent_prospects + + def add_task_for_prospect(self, prospect_id: str, title: str, due_date: str, description: str = "", priority: str = "normale", metadata: dict = None): + """ + Crée une tâche liée à un prospect. + :param prospect_id: ID du prospect + :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 ou None si prospect introuvable + """ + prospect = self.get_prospect_by_id(prospect_id) + if not prospect: + return None + 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="prospect", + entity_id=prospect_id, + priority=priority, + metadata=metadata or {}, + ) + return TaskHandler().add_task(task) diff --git a/modules/crm/scoring.py b/modules/crm/scoring.py new file mode 100644 index 0000000..401b859 --- /dev/null +++ b/modules/crm/scoring.py @@ -0,0 +1,52 @@ +from datetime import date +from typing import Literal + +from modules.crm.prospect_handler import ProspectHandler + + +def categorize(score: int) -> Literal["Froid", "Tiède", "Chaud"]: + if score is None: + score = 0 + if score >= 50: + return "Chaud" + if score >= 20: + return "Tiède" + return "Froid" + + +def adjust_score(prospect_id: str, delta: int) -> int: + """ + Ajuste le score d'un prospect et met à jour la dernière interaction. + Retourne le score à jour. + """ + handler = ProspectHandler() + p = handler.get_prospect_by_id(prospect_id) + if not p: + return 0 + try: + current = int(getattr(p, "score", 0) or 0) + except Exception: + current = 0 + new_score = max(0, current + int(delta)) + setattr(p, "score", new_score) + setattr(p, "last_interaction", date.today().isoformat()) + # On ne change pas le statut ici, sauf règle spécifique (reply) + handler.update_prospect(p) + return new_score + + +def adjust_score_for_event(prospect_id: str, event: Literal["open", "click", "reply"]) -> int: + if event == "open": + return adjust_score(prospect_id, 10) + if event == "click": + return adjust_score(prospect_id, 20) + if event == "reply": + score = adjust_score(prospect_id, 30) + # Règle: un reply passe le prospect en statut "Chaud" + handler = ProspectHandler() + p = handler.get_prospect_by_id(prospect_id) + if p: + p.status = "Chaud" + handler.update_prospect(p) + return score + return adjust_score(prospect_id, 0) diff --git a/modules/crm/search.py b/modules/crm/search.py new file mode 100644 index 0000000..961d80e --- /dev/null +++ b/modules/crm/search.py @@ -0,0 +1,181 @@ +import json +import os +from datetime import datetime +from typing import List, Dict, Any, Optional + + +def _project_root() -> str: + # modules/crm/search.py -> remonter à la racine projet + return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def _prospects_dir() -> str: + return os.path.join(_project_root(), "Data", "prospects") + + +def _read_json(path: str) -> Optional[Dict[str, Any]]: + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return None + + +def _norm_date(date_str: Optional[str]) -> Optional[str]: + if not date_str: + return None + # Tente de normaliser vers YYYY-MM-DD + s = str(date_str) + # Si déjà au format ISO ou contient "T" + if len(s) >= 10: + return s[:10] + return None + + +def _extract_fields(data: Dict[str, Any], fallback_id: str) -> Dict[str, Any]: + # Essaye plusieurs variantes de clés + name = data.get("name") or data.get("nom") or data.get("full_name") or data.get("contact_name") or "" + email = data.get("email") or data.get("mail") or "" + company = data.get("company") or data.get("societe") or data.get("entreprise") or data.get("organization") or "" + status = data.get("status") or data.get("statut") or "" + city = data.get("city") or data.get("ville") or "" + tags = data.get("tags") or [] + if isinstance(tags, str): + # transformer "a, b, c" -> ["a","b","c"] + tags = [t.strip() for t in tags.split(",") if t.strip()] + created_at = data.get("created_at") or data.get("created") or data.get("date_created") or "" + created_at = _norm_date(created_at) or "" + pid = data.get("id") or fallback_id + + # Liaisons potentielles + client_id = data.get("client_id") or data.get("clientId") or data.get("client") + project_ids = data.get("project_ids") or data.get("projects") or data.get("projectId") or [] + if isinstance(project_ids, (str, int)): + project_ids = [str(project_ids)] + elif isinstance(project_ids, list): + project_ids = [str(x) for x in project_ids] + + return { + "id": pid, + "entity_type": "prospect", + "name": name, + "email": email, + "company": company, + "status": status, + "city": city, + "tags": tags, + "created_at": created_at, + "client_id": client_id, + "project_ids": project_ids, + } + + +def load_all_prospects() -> List[Dict[str, Any]]: + base_dir = _prospects_dir() + os.makedirs(base_dir, exist_ok=True) + results: List[Dict[str, Any]] = [] + for fn in os.listdir(base_dir): + if not fn.endswith(".json"): + continue + path = os.path.join(base_dir, fn) + data = _read_json(path) + if not data: + continue + pid = os.path.splitext(fn)[0] + results.append(_extract_fields(data, pid)) + return results + + +def _contains(hay: str, needle: str) -> bool: + return needle in hay + + +def _safe_lower(s: Optional[str]) -> str: + return (s or "").lower() + + +def filter_prospects( + *, + q: Optional[str] = None, + name: Optional[str] = None, + email: Optional[str] = None, + company: Optional[str] = None, + status: Optional[str] = None, + city: Optional[str] = None, + tags: Optional[List[str]] = None, + date_from: Optional[str] = None, # YYYY-MM-DD + date_to: Optional[str] = None, # YYYY-MM-DD +) -> List[Dict[str, Any]]: + items = load_all_prospects() + + q = _safe_lower(q) + name = _safe_lower(name) + email = _safe_lower(email) + company = _safe_lower(company) + status = _safe_lower(status) + city = _safe_lower(city) + tags = [t.strip().lower() for t in (tags or []) if t.strip()] + + df = None + dt = None + try: + if date_from: + df = datetime.strptime(date_from, "%Y-%m-%d").date() + except Exception: + df = None + try: + if date_to: + dt = datetime.strptime(date_to, "%Y-%m-%d").date() + except Exception: + dt = None + + out: List[Dict[str, Any]] = [] + for it in items: + iname = _safe_lower(it.get("name")) + iemail = _safe_lower(it.get("email")) + icompany = _safe_lower(it.get("company")) + istatus = _safe_lower(it.get("status")) + icity = _safe_lower(it.get("city")) + itags = [str(t).lower() for t in (it.get("tags") or [])] + icreated = it.get("created_at") or "" + icreated_d = None + try: + if icreated: + icreated_d = datetime.strptime(icreated[:10], "%Y-%m-%d").date() + except Exception: + icreated_d = None + + # Filtre global q + if q and not ( + _contains(iname, q) or _contains(iemail, q) or _contains(icompany, q) or _contains(icity, q) + ): + continue + + # Filtres spécifiques + if name and not _contains(iname, name): + continue + if email and not _contains(iemail, email): + continue + if company and not _contains(icompany, company): + continue + if status and istatus != status: + continue + if city and not _contains(icity, city): + continue + + if tags: + # exiger que tous les tags demandés soient présents + itags_set = set(itags) + if not all(tag in itags_set for tag in tags): + continue + + if df and (not icreated_d or icreated_d < df): + continue + if dt and (not icreated_d or icreated_d > dt): + continue + + out.append(it) + + # On peut trier par date de création desc, puis nom + out.sort(key=lambda d: (d.get("created_at") or "", d.get("name") or ""), reverse=True) + return out diff --git a/modules/crm/status.py b/modules/crm/status.py new file mode 100644 index 0000000..00f6cdf --- /dev/null +++ b/modules/crm/status.py @@ -0,0 +1,51 @@ +import unicodedata +from enum import Enum +from typing import Union, Optional + + +def _normalize_no_accents(s: str) -> str: + """Normalise une chaîne en supprimant les accents et en la passant en minuscule.""" + return "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c)).lower() + + +class ProspectStatus(str, Enum): + NOUVEAU = "Nouveau" + CONTACTE = "Contacté" + RELANCE = "Relancé" + QUALIFIE = "Qualifié" + PROPOSITION = "Proposition" + NON_INTERESSE = "Non intéressé" + + @classmethod + def from_value(cls, value: Optional[Union["ProspectStatus", str]]) -> "ProspectStatus": + """Convertit une valeur (enum ou chaîne) en ProspectStatus, avec tolérance sur la casse/accents.""" + if isinstance(value, cls): + return value + if value is None: + return cls.NOUVEAU + s = str(value).strip() + if not s: + return cls.NOUVEAU + + n = _normalize_no_accents(s) + + if n in {"nouveau", "new"}: + return cls.NOUVEAU + if n in {"contacte", "contacted"}: + return cls.CONTACTE + if n in {"relance", "relaunched"}: + return cls.RELANCE + if n in {"qualifie", "qualified"}: + return cls.QUALIFIE + if n in {"proposition", "proposal"}: + return cls.PROPOSITION + if n in {"non interesse", "noninteresse", "not interested", "refuse", "refus", "abandon"}: + return cls.NON_INTERESSE + + # Tentative de correspondance exacte insensible aux accents/casse avec les valeurs de l'enum + for member in cls: + if _normalize_no_accents(member.value) == n: + return member + + # Valeur inconnue -> par défaut + return cls.NOUVEAU diff --git a/modules/crm/web.py b/modules/crm/web.py new file mode 100644 index 0000000..84c10a9 --- /dev/null +++ b/modules/crm/web.py @@ -0,0 +1,78 @@ +from flask import Blueprint, request, jsonify, url_for +from typing import List +from modules.crm.search import filter_prospects + +crm_api_bp = Blueprint("crm_api", __name__, url_prefix="/api/crm") + + +@crm_api_bp.get("/prospects/search") +def prospects_search(): + """ + API de recherche/filtre prospects. + Paramètres (query string): + - q: recherche globale (nom, email, société, ville) + - name, email, company, status, city: filtres spécifiques (contient, sauf status: égal insensible à la casse) + - tags: liste séparée par des virgules (tous doivent être présents) + - date_from, date_to: période sur created_at (YYYY-MM-DD) + """ + q = request.args.get("q") + name = request.args.get("name") + email = request.args.get("email") + company = request.args.get("company") + status = request.args.get("status") + city = request.args.get("city") + tags_raw = request.args.get("tags") or "" + tags: List[str] = [t.strip() for t in tags_raw.split(",")] if tags_raw else [] + date_from = request.args.get("date_from") or None + date_to = request.args.get("date_to") or None + + results = filter_prospects( + q=q, + name=name, + email=email, + company=company, + status=status, + city=city, + tags=tags, + date_from=date_from, + date_to=date_to, + ) + + # Enrichit avec des URLs navigables + enriched = [] + for it in results: + new_it = dict(it) + # URL principale (prospect) + try: + new_it["url"] = url_for("prospect_details", prospect_id=it.get("id")) + except Exception: + pass + + related = [] + cid = it.get("client_id") + if cid: + try: + related.append({"type": "client", "url": url_for("client_details", client_id=cid)}) + except Exception: + pass + + pids = it.get("project_ids") or [] + for pid in pids: + try: + related.append({"type": "project", "url": url_for("project_details", project_id=pid)}) + except Exception: + pass + + if related: + new_it["related"] = related + + # S'assure que le type est présent + new_it["entity_type"] = new_it.get("entity_type") or "prospect" + enriched.append(new_it) + + return jsonify( + { + "count": len(enriched), + "results": enriched, + } + ) diff --git a/modules/devis/app.py b/modules/devis/app.py new file mode 100644 index 0000000..746f7ed --- /dev/null +++ b/modules/devis/app.py @@ -0,0 +1,88 @@ +def main(): + print("=== Générateur de devis ===\n") + + # Importation des modules nécessaires + from core.form import Form + from core.generator import Generator + from core.data import Data + import os + + # On demande d'abord si le client existe déjà + client_name = input("Entrez le nom du client : ") + client_name = client_name.replace(" ", "_").lower() # Normaliser le nom du client + client_data_path = f"Data/clients/{client_name}.json" + if os.path.exists(client_data_path): + print(f"Le client {client_name} existe déjà. Chargement des données...") + data_manager = Data(client_data_path) + client_data = data_manager.load_data() + else: + print(f"Le client {client_name} n'existe pas. Création d'un nouveau client...") + #On charge le module proposition pour créer un nouveau client + from modules.proposition.app import main as generate_proposal + generate_proposal() + data_manager = Data(client_data_path) + client_data = data_manager.load_data() + print(f"Le client {client_name} a été créé avec succès.") + + # On génère le devis via les données du client en servant du template "devis" du dossier Templates + if client_data: + print("\n=== Création du devis ===") + + # Charger le modèle de devis + from core.generator import Generator + template_path = "Templates/devis.json" + + # Préparer les champs pour le formulaire + fields = [ + {"name": "numero", "label": "Numéro du devis", "type": "text"}, + ] + + # Collecter les données via le formulaire + form = Form(fields) + form.ask() + form_data = form.get_data() + + # Préparer les données du client pour le modèle + client_data_formatted = { + "client_name": client_data.get("client_name", ""), + "client_email": client_data.get("email", ""), + "client_phone": client_data.get("telephone", ""), + "client_adress": client_data.get("adresse", "") + } + + # Récupérer les fonctionnalités du client + features = client_data.get("features", []) + if not isinstance(features, list): + print("Erreur : Les fonctionnalités du client ne sont pas correctement formatées. Elles seront ignorées.") + features = [] + + # Ajouter les services avec tarifs pour chaque fonctionnalité + services = [] + for feature in features: + if isinstance(feature, dict): # Vérifier que 'feature' est un dictionnaire + description = feature.get("description", "") + prix_unitaire = float(input(f"Entrez le tarif pour la fonctionnalité '{description}' : ")) + services.append({"description": description, "prix_unitaire": prix_unitaire}) + else: + print(f"Erreur : La fonctionnalité '{feature}' n'est pas valide et sera ignorée.") + + # Ajouter les données spécifiques au devis + devis_content = { + "numero": form_data["numero"], + "date": "11/05/2025", + "client": client_data_formatted, # Transmettre les données formatées du client + "services": services, + "total": sum(service["prix_unitaire"] for service in services) + } + + # Générer le devis + output_path = f"devis" + try: + generator = Generator(devis_content, template_path) + generator.generate_facture(output_path) + print("\n✅ Devis généré avec succès.") + print(f"Le devis a été enregistré dans le dossier '{output_path}'.") + except Exception as e: + print(f"\n❌ Une erreur est survenue lors de la génération du devis : {e}") + else: + print("\n❌ Impossible de générer le devis car les données du client n'ont pas été chargées.") \ No newline at end of file diff --git a/modules/devis/fields.py b/modules/devis/fields.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/email/draft.py b/modules/email/draft.py new file mode 100644 index 0000000..5eb8e59 --- /dev/null +++ b/modules/email/draft.py @@ -0,0 +1,70 @@ +from typing import Optional, Dict, Any +from datetime import datetime +import uuid + + +class EmailDraft: + """ + Représente un brouillon d'email généré automatiquement. + Stockage fichier: Data/email_drafts/.json + """ + def __init__( + self, + prospect_id: str, + to_email: str, + subject: str, + content: str, + status: str = "draft", # draft | sent | failed + template_id: Optional[str] = None, + task_id: Optional[str] = None, + id: Optional[str] = None, + created_at: Optional[str] = None, + sent_at: Optional[str] = None, + error_message: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ): + self.id = id or f"ed_{uuid.uuid4().hex[:10]}" + self.prospect_id = prospect_id + self.to_email = to_email + self.subject = subject + self.content = content + self.status = status + self.template_id = template_id + self.task_id = task_id + self.created_at = created_at or datetime.utcnow().isoformat() + self.sent_at = sent_at + self.error_message = error_message + self.metadata = metadata or {} + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "prospect_id": self.prospect_id, + "to_email": self.to_email, + "subject": self.subject, + "content": self.content, + "status": self.status, + "template_id": self.template_id, + "task_id": self.task_id, + "created_at": self.created_at, + "sent_at": self.sent_at, + "error_message": self.error_message, + "metadata": self.metadata, + } + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "EmailDraft": + return EmailDraft( + id=data.get("id"), + prospect_id=data.get("prospect_id", ""), + to_email=data.get("to_email", ""), + subject=data.get("subject", ""), + content=data.get("content", ""), + status=data.get("status", "draft"), + template_id=data.get("template_id"), + task_id=data.get("task_id"), + created_at=data.get("created_at"), + sent_at=data.get("sent_at"), + error_message=data.get("error_message"), + metadata=data.get("metadata") or {}, + ) diff --git a/modules/email/draft_handler.py b/modules/email/draft_handler.py new file mode 100644 index 0000000..59214a6 --- /dev/null +++ b/modules/email/draft_handler.py @@ -0,0 +1,89 @@ +import os +import json +from typing import List, Optional, Dict, Any +from datetime import datetime + +from modules.email.draft import EmailDraft + + +class DraftHandler: + """ + Gestionnaire de brouillons (fichiers JSON). + Répertoire: Data/email_drafts + """ + def __init__(self, base_dir: Optional[str] = None): + base_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.base_dir = base_dir or os.path.join(base_root, "Data", "email_drafts") + os.makedirs(self.base_dir, exist_ok=True) + + def _draft_path(self, draft_id: str) -> str: + return os.path.join(self.base_dir, f"{draft_id}.json") + + def add_draft(self, draft: EmailDraft) -> str: + path = self._draft_path(draft.id) + with open(path, "w", encoding="utf-8") as f: + json.dump(draft.to_dict(), f, ensure_ascii=False, indent=2) + return draft.id + + def get_draft(self, draft_id: str) -> Optional[EmailDraft]: + path = self._draft_path(draft_id) + if not os.path.exists(path): + return None + with open(path, "r", encoding="utf-8") as f: + return EmailDraft.from_dict(json.load(f)) + + def update_draft(self, draft: EmailDraft) -> bool: + path = self._draft_path(draft.id) + if not os.path.exists(path): + return False + with open(path, "w", encoding="utf-8") as f: + json.dump(draft.to_dict(), f, ensure_ascii=False, indent=2) + return True + + def delete_draft(self, draft_id: str) -> bool: + path = self._draft_path(draft_id) + if os.path.exists(path): + try: + os.remove(path) + return True + except Exception: + return False + return False + + def list_drafts(self, status: Optional[str] = None) -> List[EmailDraft]: + drafts: List[EmailDraft] = [] + for filename in os.listdir(self.base_dir): + if filename.endswith(".json"): + try: + with open(os.path.join(self.base_dir, filename), "r", encoding="utf-8") as f: + data = json.load(f) + d = EmailDraft.from_dict(data) + if status is None or d.status == status: + drafts.append(d) + except Exception: + continue + # Tri: plus récents d'abord + drafts.sort(key=lambda d: d.created_at or "", reverse=True) + return drafts + + def list_pending(self) -> List[EmailDraft]: + return self.list_drafts(status="draft") + + def mark_sent(self, draft_id: str, success: bool, error_message: Optional[str] = None) -> bool: + d = self.get_draft(draft_id) + if not d: + return False + d.status = "sent" if success else "failed" + d.sent_at = datetime.utcnow().isoformat() + d.error_message = None if success else (error_message or "Unknown error") + return self.update_draft(d) + + def find_existing_for_task(self, task_id: str) -> Optional[EmailDraft]: + """ + Évite les doublons: si un draft 'draft' existe déjà pour cette tâche, le renvoie. + Les brouillons 'failed' ne bloquent pas la régénération. + """ + for d in self.list_drafts(): + if d.task_id == task_id and d.status == "draft": + return d + return None diff --git a/modules/email/drafts_web.py b/modules/email/drafts_web.py new file mode 100644 index 0000000..2d6ea9e --- /dev/null +++ b/modules/email/drafts_web.py @@ -0,0 +1,87 @@ +from flask import Blueprint, request, redirect, url_for, flash, Response +from typing import List +from html import escape + +from modules.email.draft_handler import DraftHandler +from modules.email.email_manager import EmailSender + +email_drafts_bp = Blueprint("email_drafts", __name__, url_prefix="/email/drafts") + + +@email_drafts_bp.get("/") +def list_drafts_page(): + """ + Page HTML minimaliste listant les brouillons avec bouton [Envoyer]. + Pas de template Jinja requis (HTML inline pour simplicité d'intégration). + """ + handler = DraftHandler() + drafts = handler.list_pending() + + def row_html(d): + # Contenu HTML tel quel (on suppose content déjà HTML sûr) + return f""" +
+
+

{escape(d.subject)}

+
+ + +
+
+
À: {escape(d.to_email)}
+
{d.content}
+
+ Prospect: {escape(d.prospect_id)} | Template: {escape(d.template_id or '-') } +
+
+ """ + + items_html = "\n".join(row_html(d) for d in drafts) or "

Aucun brouillon à envoyer.

" + + page = f""" + + + + + Brouillons d'emails + + + +

Brouillons d'emails à envoyer

+
{items_html}
+ + + """ + return Response(page, mimetype="text/html") + + +@email_drafts_bp.post("/send") +def send_draft(): + """ + Envoi d'un brouillon sélectionné, puis mise à jour du statut. + """ + draft_id = (request.form.get("draft_id") or "").strip() + if not draft_id: + flash("Brouillon invalide", "warning") + return redirect(url_for("email_drafts.list_drafts_page")) + + handler = DraftHandler() + draft = handler.get_draft(draft_id) + if not draft: + flash("Brouillon introuvable", "danger") + return redirect(url_for("email_drafts.list_drafts_page")) + + sender = EmailSender() + try: + res = sender.send_email(draft.to_email, draft.subject, draft.content) + if res.get("success"): + handler.mark_sent(draft.id, success=True) + flash("Email envoyé.", "success") + else: + handler.mark_sent(draft.id, success=False, error_message=res.get("error")) + flash("Échec de l'envoi de l'email.", "danger") + except Exception as e: + handler.mark_sent(draft.id, success=False, error_message=str(e)) + flash("Erreur lors de l'envoi de l'email.", "danger") + + return redirect(url_for("email_drafts.list_drafts_page")) diff --git a/modules/email/email_manager.py b/modules/email/email_manager.py new file mode 100644 index 0000000..1873e1b --- /dev/null +++ b/modules/email/email_manager.py @@ -0,0 +1,353 @@ +from typing import List, Dict, Any, Union +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime +import os +import json +import re +from urllib.parse import quote_plus +from uuid import uuid4 +from core.data import Data + +class EmailTemplate: + """Classe gérant les templates d'emails""" + + def __init__(self, template_folder="Data/email_templates"): + self.template_folder = template_folder + + # Créer le dossier de templates s'il n'existe pas + if not os.path.exists(self.template_folder): + os.makedirs(self.template_folder) + + def get_all_templates(self): + """Retourne tous les templates disponibles""" + templates = [] + if os.path.exists(self.template_folder): + for filename in os.listdir(self.template_folder): + if filename.endswith('.json'): + template_path = os.path.join(self.template_folder, filename) + try: + data_manager = Data(template_path) + template_data = data_manager.load_data() + templates.append(template_data) + except Exception as e: + print(f"Erreur lors du chargement du template {filename}: {e}") + return templates + + def get_template_by_id(self, template_id): + """Récupère un template par son ID""" + template_path = os.path.join(self.template_folder, f"{template_id}.json") + if os.path.exists(template_path): + data_manager = Data(template_path) + return data_manager.load_data() + return None + + def save_template(self, template_data): + """Sauvegarde un template d'email""" + template_id = template_data.get('id') + if not template_id: + # Générer un ID s'il n'existe pas + import uuid + template_id = f"tpl_{uuid.uuid4().hex[:8]}" + template_data['id'] = template_id + + template_path = os.path.join(self.template_folder, f"{template_id}.json") + data_manager = Data(template_path) + data_manager.save_data(template_data) + return template_data + + def delete_template(self, template_id): + """Supprime un template d'email""" + template_path = os.path.join(self.template_folder, f"{template_id}.json") + if os.path.exists(template_path): + os.remove(template_path) + return True + return False + + def render_template(self, template_id, context=None): + """Rend un template avec les variables spécifiées dans le contexte""" + template = self.get_template_by_id(template_id) + if not template: + return None + + subject = template.get('subject', '') + content = template.get('content', '') + + # Remplacer les variables dans le sujet et le contenu + if context: + for key, value in context.items(): + placeholder = f"{{{{{key}}}}}" + subject = subject.replace(placeholder, str(value)) + content = content.replace(placeholder, str(value)) + + return { + "subject": subject, + "content": content + } + + +class EmailSender: + """Classe gérant l'envoi d'emails""" + + def __init__(self, config_file="config/email_config.json"): + # Ensure config_file is an absolute path + if not os.path.isabs(config_file): + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.config_file = os.path.join(base_dir, config_file) + else: + self.config_file = config_file + self.config = self._load_config() + self.template_manager = EmailTemplate() + + def _load_config(self): + """Charge la configuration email depuis le fichier de configuration""" + config_dir = os.path.dirname(self.config_file) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + print(f"Loading email config from: {self.config_file}") + if os.path.exists(self.config_file): + try: + with open(self.config_file, 'r') as f: + config = json.load(f) + print(f"Loaded email config: {config}") + return config + except Exception as e: + print(f"Erreur lors du chargement de la configuration email: {e}") + + # Configuration par défaut + default_config = { + "smtp_server": "smtp.gmail.com", + "smtp_port": 587, + "username": "", + "password": "", + "sender_name": "Suite Consultance", + "sender_email": "" + } + print(f"Using default email config: {default_config}") + return default_config + + def save_config(self, config): + """Sauvegarde la configuration email""" + config_dir = os.path.dirname(self.config_file) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + with open(self.config_file, 'w') as f: + json.dump(config, f, indent=4) + + self.config = config + return True + + def send_email(self, to_email, subject, body, cc=None, bcc=None): + """Envoie un email à un destinataire""" + if not self.config.get('username') or not self.config.get('password'): + raise ValueError("La configuration email n'est pas complète") + + message = MIMEMultipart() + message["From"] = f"{self.config.get('sender_name')} <{self.config.get('sender_email')}>" + message["To"] = to_email + message["Subject"] = subject + + if cc: + message["Cc"] = ", ".join(cc) if isinstance(cc, list) else cc + if bcc: + message["Bcc"] = ", ".join(bcc) if isinstance(bcc, list) else bcc + + message.attach(MIMEText(body, "html")) + + try: + server = smtplib.SMTP(self.config.get('smtp_server'), self.config.get('smtp_port')) + server.starttls() + server.login(self.config.get('username'), self.config.get('password')) + + recipients = [to_email] + if cc: + recipients.extend(cc if isinstance(cc, list) else [cc]) + if bcc: + recipients.extend(bcc if isinstance(bcc, list) else [bcc]) + + server.sendmail(self.config.get('sender_email'), recipients, message.as_string()) + server.quit() + + return { + "success": True, + "timestamp": datetime.now().isoformat(), + "to": to_email, + "subject": subject + } + except Exception as e: + return { + "success": False, + "error": str(e), + "timestamp": datetime.now().isoformat() + } + + def send_templated_email(self, to_email, template_id, context=None, cc=None, bcc=None): + """Envoie un email basé sur un template à un destinataire""" + rendered = self.template_manager.render_template(template_id, context) + if not rendered: + return { + "success": False, + "error": "Template not found", + "timestamp": datetime.now().isoformat() + } + + return self.send_email(to_email, rendered['subject'], rendered['content'], cc, bcc) + + def send_bulk_email(self, emails, subject, body, cc=None, bcc=None): + """Envoie le même email à plusieurs destinataires""" + results = [] + for email in emails: + result = self.send_email(email, subject, body, cc, bcc) + results.append({ + "email": email, + **result + }) + return results + + def send_bulk_templated_email(self, recipients, template_id, cc=None, bcc=None): + """ + Envoie un email basé sur un template à plusieurs destinataires + recipients: liste de dictionnaires contenant l'email du destinataire et le contexte + [{ + "email": "example@example.com", + "context": {"name": "John Doe", "company": "ACME Inc."} + }] + """ + results = [] + for recipient in recipients: + email = recipient.get('email') + context = recipient.get('context', {}) + result = self.send_templated_email(email, template_id, context, cc, bcc) + results.append({ + "email": email, + **result + }) + return results + + # ---------- Tracking helpers ---------- + def _embed_tracking(self, html_body: str, tracking_id: str, prospect_id: str) -> str: + """ + Ajoute un pixel d'ouverture et réécrit les liens pour le click tracking. + Utilise APP_BASE_URL si définie, sinon génère des liens relatifs. + """ + base = (os.environ.get("APP_BASE_URL") or "").rstrip("/") + prefix = f"{base}/tasks/t" # routes de tracking montées sur le blueprint 'tasks' + # Pixel d'ouverture (1x1 PNG) + pixel = f'' + body = html_body or "" + # Injection du pixel avant la fermeture de body si possible + if "" in body.lower(): + # trouver la vraie balise en conservant la casse + idx = body.lower().rfind("") + body = body[:idx] + pixel + body[idx:] + else: + body = body + pixel + + # Réécriture des liens + def _rewrite(match): + url = match.group(1) + # ignore si déjà tracké + if "/tasks/t/c/" in url: + return f'href="{url}"' + tracked = f'{prefix}/c/{tracking_id}?u={quote_plus(url)}' + return f'href="{tracked}"' + + body = re.sub(r'href="([^"]+)"', _rewrite, body) + return body + + def send_tracked_email(self, to_email: str, subject: str, body: str, prospect_id: str, template_id: str = None, cc=None, bcc=None) -> Dict[str, Any]: + """ + Envoie un email avec tracking (open/click). + Crée un enregistrement de tracking et insère un pixel + réécriture des liens. + """ + tracking_id = f"trk_{uuid4().hex[:16]}" + # Créer l'enregistrement de tracking + try: + from modules.tracking.store import TrackingStore + store = TrackingStore() + store.create_record(tracking_id, { + "prospect_id": prospect_id, + "to": to_email, + "subject": subject, + "template_id": template_id, + "opens": 0, + "clicks": 0, + }) + except Exception: + # même si le tracking store échoue, on tente d'envoyer l'email + pass + + tracked_body = self._embed_tracking(body, tracking_id, prospect_id) + result = self.send_email(to_email, subject, tracked_body, cc, bcc) + result["tracking_id"] = tracking_id + return result + + +class EmailHistory: + """Classe gérant l'historique des emails envoyés""" + + def __init__(self, history_folder="Data/email_history"): + self.history_folder = history_folder + + # Créer le dossier d'historique s'il n'existe pas + if not os.path.exists(self.history_folder): + os.makedirs(self.history_folder) + + def add_email_record(self, prospect_id, email_data): + """Ajoute un email à l'historique d'un prospect""" + history_file = os.path.join(self.history_folder, f"{prospect_id}.json") + + # Charger l'historique existant + history = [] + if os.path.exists(history_file): + try: + with open(history_file, 'r') as f: + history = json.load(f) + except: + history = [] + + # Ajouter le nouvel email à l'historique + history.append({ + **email_data, + "timestamp": datetime.now().isoformat() + }) + + # Sauvegarder l'historique + with open(history_file, 'w') as f: + json.dump(history, f, indent=4) + + return True + + def get_prospect_email_history(self, prospect_id): + """Récupère l'historique des emails pour un prospect""" + history_file = os.path.join(self.history_folder, f"{prospect_id}.json") + + if os.path.exists(history_file): + try: + with open(history_file, 'r') as f: + return json.load(f) + except: + return [] + + return [] + + def get_all_email_history(self): + """Récupère l'historique de tous les emails envoyés""" + all_history = {} + + if os.path.exists(self.history_folder): + for filename in os.listdir(self.history_folder): + if filename.endswith('.json'): + prospect_id = filename.split('.')[0] + history_file = os.path.join(self.history_folder, filename) + + try: + with open(history_file, 'r') as f: + all_history[prospect_id] = json.load(f) + except: + all_history[prospect_id] = [] + + return all_history diff --git a/modules/email/email_scraper.py b/modules/email/email_scraper.py new file mode 100644 index 0000000..2ae26ac --- /dev/null +++ b/modules/email/email_scraper.py @@ -0,0 +1,968 @@ +import requests +from bs4 import BeautifulSoup +import re +from urllib.parse import urljoin, urlparse +import time +from typing import List, Set, Dict +import json +import os +from datetime import datetime + +class EmailScraper: + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }) + self.email_pattern = re.compile(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}') + self.phone_pattern = re.compile(r'(?:\+32|0)\s?[1-9](?:[\s\-\.\/]?\d){8}|\+32\s?[1-9](?:[\s\-\.\/]?\d){8}|(?:\+33|0)[1-9](?:[\s\-\.\/]?\d){8}') + self.visited_urls = set() + self.found_emails = set() + self.contact_info = {} + + def scrape_page(self, url: str, max_pages: int = 10) -> Dict: + """ + Scrape une page avec pagination pour extraire les données d'entreprises + """ + results = { + 'url': url, + 'contacts': [], # Liste des contacts avec email, nom, téléphone, etc. + 'pages_scraped': [], + 'errors': [], + 'start_time': datetime.now().isoformat(), + 'end_time': None, + 'domain_info': {} + } + + try: + self._scrape_with_pagination(url, results, max_pages) + self._extract_domain_info(url, results) + except Exception as e: + results['errors'].append(f"Erreur générale: {str(e)}") + + results['end_time'] = datetime.now().isoformat() + + return results + + def _scrape_with_pagination(self, base_url: str, results: Dict, max_pages: int): + """ + Scraper avec gestion de la pagination + """ + current_page = 1 + current_url = base_url + + while current_page <= max_pages: + if current_url in self.visited_urls: + break + + try: + # Normaliser l'URL + parsed_url = urlparse(current_url) + if not parsed_url.scheme: + current_url = 'https://' + current_url + + self.visited_urls.add(current_url) + + print(f"Scraping page {current_page}: {current_url}") + + # Faire la requête + response = self.session.get(current_url, timeout=15) + response.raise_for_status() + + # Parser le HTML + soup = BeautifulSoup(response.content, 'html.parser') + + # Extraire les entreprises/contacts de la page + page_contacts = self._extract_business_contacts(soup, response.text, current_url) + + # Ajouter les contacts à la liste principale + for contact in page_contacts: + # Vérifier si ce contact existe déjà (par email) + existing_contact = next((c for c in results['contacts'] if c['email'] == contact['email']), None) + if existing_contact: + # Fusionner les informations si le contact existe + self._merge_contact_info(existing_contact, contact) + else: + results['contacts'].append(contact) + + results['pages_scraped'].append({ + 'url': current_url, + 'page_number': current_page, + 'contacts_found': len(page_contacts), + 'contacts': page_contacts, + 'status': 'success', + 'timestamp': datetime.now().isoformat() + }) + + print(f" - Page {current_page}: Trouvé {len(page_contacts)} contact(s)") + + # Si aucun contact trouvé, peut-être qu'on a atteint la fin + if len(page_contacts) == 0: + print(f" - Aucun contact trouvé sur la page {current_page}, arrêt du scraping") + break + + # Chercher le lien vers la page suivante + next_url = self._find_next_page_url(soup, current_url, current_page) + + if not next_url: + print(f" - Pas de page suivante trouvée, arrêt du scraping") + break + + current_url = next_url + current_page += 1 + + # Délai entre les pages pour éviter la surcharge + time.sleep(2) + + except requests.exceptions.RequestException as e: + results['errors'].append(f"Erreur de requête pour la page {current_page} ({current_url}): {str(e)}") + results['pages_scraped'].append({ + 'url': current_url, + 'page_number': current_page, + 'contacts_found': 0, + 'contacts': [], + 'status': 'error', + 'error': str(e), + 'timestamp': datetime.now().isoformat() + }) + break + except Exception as e: + results['errors'].append(f"Erreur lors du parsing de la page {current_page}: {str(e)}") + break + + def _extract_business_contacts(self, soup: BeautifulSoup, text: str, page_url: str) -> List[Dict]: + """ + Extraire les informations d'entreprises d'une page (spécialisé pour les annuaires) + """ + contacts = [] + + # Chercher des conteneurs d'entreprises communs + business_containers = self._find_business_containers(soup) + + if business_containers: + # Si on trouve des conteneurs structurés, les traiter + for container in business_containers: + contact = self._extract_contact_from_container(container, page_url) + if contact and contact.get('email'): + contacts.append(contact) + else: + # Fallback: extraction générale comme avant + contacts = self._extract_contact_info(soup, text, page_url) + + return contacts + + def _find_business_containers(self, soup: BeautifulSoup) -> List: + """ + Trouver les conteneurs qui contiennent probablement des informations d'entreprises + """ + containers = [] + + # Patterns communs pour les annuaires d'entreprises + business_selectors = [ + # Classes/IDs communs + '[class*="business"]', + '[class*="company"]', + '[class*="enterprise"]', + '[class*="contact"]', + '[class*="listing"]', + '[class*="directory"]', + '[class*="card"]', + '[class*="item"]', + '[class*="entry"]', + '[class*="result"]', + # Balises sémantiques + 'article', + '[itemtype*="Organization"]', + '[itemtype*="LocalBusiness"]', + # Structures de liste + 'li[class*="business"]', + 'li[class*="company"]', + 'div[class*="row"]', + 'div[class*="col"]' + ] + + for selector in business_selectors: + try: + elements = soup.select(selector) + for element in elements: + # Vérifier si l'élément contient des informations utiles + if self._container_has_business_info(element): + containers.append(element) + except: + continue + + # Déduplication basée sur le contenu + unique_containers = [] + for container in containers: + if not any(self._containers_are_similar(container, existing) for existing in unique_containers): + unique_containers.append(container) + + return unique_containers[:50] # Limiter pour éviter la surcharge + + def _container_has_business_info(self, container) -> bool: + """ + Vérifier si un conteneur a des informations d'entreprise + """ + text = container.get_text(strip=True).lower() + + # Indicateurs d'informations d'entreprise + business_indicators = [ + '@', 'email', 'mail', 'contact', + 'tel', 'phone', 'telephone', 'gsm', + 'rue', 'avenue', 'boulevard', 'place', + 'www.', 'http', '.com', '.be', '.fr', + 'sarl', 'sprl', 'sa', 'nv', 'bvba' + ] + + score = sum(1 for indicator in business_indicators if indicator in text) + return score >= 2 and len(text) > 20 + + def _containers_are_similar(self, container1, container2) -> bool: + """ + Vérifier si deux conteneurs sont similaires (pour éviter les doublons) + """ + text1 = container1.get_text(strip=True) + text2 = container2.get_text(strip=True) + + # Si les textes sont identiques ou très similaires + if text1 == text2: + return True + + # Si un conteneur est inclus dans l'autre + if len(text1) > len(text2): + return text2 in text1 + else: + return text1 in text2 + + def _extract_contact_from_container(self, container, page_url: str) -> Dict: + """ + Extraire les informations de contact d'un conteneur spécifique + """ + contact = { + 'email': '', + 'name': '', + 'first_name': '', + 'last_name': '', + 'company': '', + 'phone': '', + 'location': '', + 'source_url': page_url, + 'notes': '' + } + + # Extraire l'email depuis les balises individuelles d'abord + email_found = False + + # Chercher dans les liens mailto + mailto_links = container.find_all('a', href=re.compile(r'^mailto:', re.I)) + if mailto_links: + href = mailto_links[0].get('href', '') + email_match = re.search(r'mailto:([^?&]+)', href, re.I) + if email_match and self._is_valid_email(email_match.group(1)): + contact['email'] = email_match.group(1).lower() + email_found = True + + # Si pas trouvé dans mailto, chercher dans les balises individuelles + if not email_found: + for element in container.find_all(['p', 'div', 'span', 'td', 'li']): + element_text = element.get_text(strip=True) + # Ajouter des espaces autour des balises pour éviter la concaténation + element_text = ' ' + element_text + ' ' + + email_matches = self.email_pattern.findall(element_text) + if email_matches: + for email in email_matches: + email = email.strip() + if re.match(r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$', email) and self._is_valid_email(email): + contact['email'] = email.lower() + email_found = True + break + if email_found: + break + + # Si toujours pas trouvé, chercher dans le texte global avec des patterns plus précis + if not email_found: + container_text = container.get_text(separator=' ', strip=True) # Utiliser un séparateur + + # Patterns avec contexte pour éviter la capture parasite + context_patterns = [ + r'(?:email|e-mail|mail|contact)\s*:?\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})', + r'([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})(?=\s|$|[^\w.-])', + ] + + for pattern in context_patterns: + matches = re.findall(pattern, container_text, re.IGNORECASE) + if matches: + email = matches[0] if isinstance(matches[0], str) else matches[0][0] if matches[0] else '' + if email and self._is_valid_email(email): + contact['email'] = email.lower() + email_found = True + break + + # Extraire le téléphone + container_text = container.get_text(separator=' ', strip=True) + phone_matches = self.phone_pattern.findall(container_text) + if phone_matches: + # Prendre le premier numéro et le nettoyer + phone = phone_matches[0] + # S'assurer qu'on n'a que des chiffres, espaces, tirets, points, slash et + + clean_phone = re.sub(r'[^0-9\s\-\.\/\+].*$', '', phone) + contact['phone'] = clean_phone.strip() + + # Extraire le nom de l'entreprise + contact['company'] = self._extract_company_name(container, container_text) + + # Extraire les noms de personnes + names = self._extract_person_names(container, container_text) + if names: + contact.update(names) + + # Extraire la localisation + contact['location'] = self._extract_location_from_container(container, container_text) + + # Enrichir avec des informations contextuelles + self._enhance_business_contact(contact, container, container_text) + + return contact if contact['email'] or contact['company'] else None + + def _extract_company_name(self, container, text: str) -> str: + """ + Extraire le nom de l'entreprise d'un conteneur + """ + # Chercher dans les balises title, h1-h6, strong, b + title_elements = container.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'b', '[class*="title"]', '[class*="name"]', '[class*="company"]']) + + for element in title_elements: + company_text = element.get_text(strip=True) + if len(company_text) > 2 and len(company_text) < 100: + # Éviter les textes trop génériques + if not any(generic in company_text.lower() for generic in ['accueil', 'contact', 'email', 'téléphone', 'adresse']): + return company_text + + # Fallback: prendre la première ligne non-vide qui semble être un nom + lines = text.split('\n') + for line in lines[:3]: # Les 3 premières lignes + line = line.strip() + if len(line) > 2 and len(line) < 100 and not '@' in line and not any(char.isdigit() for char in line[:3]): + return line + + return '' + + def _extract_person_names(self, container, text: str) -> Dict: + """ + Extraire les noms de personnes + """ + names = {'name': '', 'first_name': '', 'last_name': ''} + + # Patterns pour les noms de personnes + name_patterns = [ + r'\b([A-Z][a-zÀ-ÿ]+)\s+([A-Z][a-zÀ-ÿ]+)\b', # Prénom Nom + r'\b([A-Z][A-Z]+)\s+([A-Z][a-zÀ-ÿ]+)\b', # NOM Prénom + ] + + # Chercher dans les balises spécifiques + name_elements = container.find_all(['[class*="name"]', '[class*="contact"]', '[class*="person"]']) + + for element in name_elements: + element_text = element.get_text(strip=True) + for pattern in name_patterns: + match = re.search(pattern, element_text) + if match: + names['first_name'] = match.group(1) + names['last_name'] = match.group(2) + names['name'] = f"{names['first_name']} {names['last_name']}" + return names + + # Si pas trouvé dans les balises, chercher dans le texte + for pattern in name_patterns: + match = re.search(pattern, text) + if match: + names['first_name'] = match.group(1) + names['last_name'] = match.group(2) + names['name'] = f"{names['first_name']} {names['last_name']}" + break + + return names + + def _extract_location_from_container(self, container, text: str) -> str: + """ + Extraire la localisation d'un conteneur + """ + # Chercher dans les balises d'adresse + address_elements = container.find_all(['address', '[class*="address"]', '[class*="location"]', '[class*="ville"]', '[class*="city"]']) + + for element in address_elements: + location_text = element.get_text(strip=True) + if len(location_text) > 5: + return location_text + + # Patterns pour les adresses belges/françaises + location_patterns = [ + r'\b\d{4,5}\s+[A-Za-zÀ-ÿ\s\-]+\b', # Code postal + ville + r'\b[A-Za-zÀ-ÿ\s\-]+,\s*[A-Za-zÀ-ÿ\s\-]+\b', # Ville, Région/Pays + r'\b(?:rue|avenue|boulevard|place|chemin)\s+[A-Za-zÀ-ÿ\s\d\-,]+\b' # Adresse complète + ] + + for pattern in location_patterns: + match = re.search(pattern, text, re.IGNORECASE) + if match: + return match.group(0).strip() + + return '' + + def _enhance_business_contact(self, contact: Dict, container, text: str): + """ + Améliorer les informations de contact d'entreprise + """ + # Si pas de nom trouvé, essayer d'extraire depuis l'email + if not contact['name'] and contact['email']: + local_part = contact['email'].split('@')[0] + domain_part = contact['email'].split('@')[1] + + if '.' in local_part: + parts = local_part.split('.') + contact['first_name'] = parts[0].title() + contact['last_name'] = parts[1].title() if len(parts) > 1 else '' + contact['name'] = f"{contact['first_name']} {contact['last_name']}".strip() + + # Si pas d'entreprise, essayer de deviner depuis le domaine + if not contact['company']: + company_name = domain_part.split('.')[0] + contact['company'] = company_name.title() + + # Enrichir les notes avec des informations contextuelles + notes_parts = [] + + # Chercher des informations sur l'activité + activity_patterns = [ + r'(?i)\b(restaurant|café|boulangerie|pharmacie|garage|coiffeur|médecin|avocat|comptable|architecte|dentiste|vétérinaire|magasin|boutique|salon)\b', + r'(?i)\b(commerce|service|entreprise|société|bureau|cabinet|clinique|centre|institut)\b' + ] + + for pattern in activity_patterns: + matches = re.findall(pattern, text) + if matches: + notes_parts.append(f"Activité: {', '.join(set(matches))}") + break + + # Chercher des horaires + horaires_pattern = r'(?i)(?:ouvert|fermé|horaires?)[:\s]*([^.!?\n]{10,50})' + horaires_match = re.search(horaires_pattern, text) + if horaires_match: + notes_parts.append(f"Horaires: {horaires_match.group(1).strip()}") + + # Chercher un site web + website_pattern = r'\b(?:www\.)?[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.(?:com|be|fr|org|net)\b' + website_match = re.search(website_pattern, text) + if website_match: + notes_parts.append(f"Site web: {website_match.group(0)}") + + contact['notes'] = ' | '.join(notes_parts) + + def _find_next_page_url(self, soup: BeautifulSoup, current_url: str, current_page: int) -> str: + """ + Trouver l'URL de la page suivante + """ + base_url = '/'.join(current_url.split('/')[:-1]) if '/' in current_url else current_url + + # Patterns communs pour les liens de pagination + next_patterns = [ + # Liens avec texte + 'a[href]:contains("Suivant")', + 'a[href]:contains("Next")', + 'a[href]:contains(">")', + 'a[href]:contains("Page suivante")', + # Liens avec classes + 'a[class*="next"]', + 'a[class*="suivant"]', + 'a[class*="pagination"]', + # Numéros de page + f'a[href]:contains("{current_page + 1}")', + ] + + for pattern in next_patterns: + try: + links = soup.select(pattern) + for link in links: + href = link.get('href') + if href: + # Construire l'URL complète + if href.startswith('http'): + return href + elif href.startswith('/'): + parsed = urlparse(current_url) + return f"{parsed.scheme}://{parsed.netloc}{href}" + else: + return urljoin(current_url, href) + except: + continue + + # Essayer de construire l'URL de la page suivante par pattern + # Pattern 1: ?page=X + if 'page=' in current_url: + return re.sub(r'page=\d+', f'page={current_page + 1}', current_url) + + # Pattern 2: /pageX + if f'/page{current_page}' in current_url: + return current_url.replace(f'/page{current_page}', f'/page{current_page + 1}') + + # Pattern 3: Ajouter ?page=2 si c'est la première page + if current_page == 1: + separator = '&' if '?' in current_url else '?' + return f"{current_url}{separator}page={current_page + 1}" + + return None + + def _extract_contact_info(self, soup: BeautifulSoup, text: str, page_url: str) -> List[Dict]: + """ + Extraire les informations de contact complètes d'une page + """ + contacts = [] + + # Extraire tous les emails + emails = set() + emails.update(self._extract_emails_from_text(text)) + emails.update(self._extract_emails_from_links(soup)) + + # Extraire les numéros de téléphone + phones = self._extract_phone_numbers(text) + + # Extraire les noms et entreprises depuis les balises structurées + structured_contacts = self._extract_structured_contacts(soup) + + # Extraire l'adresse/localité + location = self._extract_location_info(soup, text) + + # Créer des contacts pour chaque email trouvé + for email in emails: + if not self._is_valid_email(email): + continue + + contact = { + 'email': email.lower(), + 'name': '', + 'first_name': '', + 'last_name': '', + 'company': '', + 'phone': '', + 'location': location, + 'source_url': page_url, + 'notes': '' + } + + # Essayer de trouver des informations complémentaires + self._enhance_contact_info(contact, soup, text, structured_contacts, phones) + + contacts.append(contact) + + return contacts + + def _extract_phone_numbers(self, text: str) -> List[str]: + """ + Extraire les numéros de téléphone + """ + phones = [] + matches = self.phone_pattern.findall(text) + + for phone in matches: + # Nettoyer le numéro + clean_phone = re.sub(r'[\s\-\.\/]', '', phone) + if len(clean_phone) >= 9: # Numéro valide + phones.append(phone) + + return phones + + def _extract_structured_contacts(self, soup: BeautifulSoup) -> List[Dict]: + """ + Extraire les contacts depuis les données structurées (microdata, JSON-LD, etc.) + """ + contacts = [] + + # Chercher les données JSON-LD + json_scripts = soup.find_all('script', type='application/ld+json') + for script in json_scripts: + try: + data = json.loads(script.string) + if isinstance(data, dict): + contact = self._parse_json_ld_contact(data) + if contact: + contacts.append(contact) + elif isinstance(data, list): + for item in data: + contact = self._parse_json_ld_contact(item) + if contact: + contacts.append(contact) + except: + continue + + # Chercher les microdata + contacts.extend(self._extract_microdata_contacts(soup)) + + return contacts + + def _parse_json_ld_contact(self, data: Dict) -> Dict: + """ + Parser un contact depuis les données JSON-LD + """ + contact = {} + + if data.get('@type') in ['Organization', 'LocalBusiness', 'Person']: + contact['name'] = data.get('name', '') + contact['company'] = data.get('name', '') if data.get('@type') != 'Person' else '' + + # Email + email = data.get('email') + if email: + contact['email'] = email + + # Téléphone + phone = data.get('telephone') + if phone: + contact['phone'] = phone + + # Adresse + address = data.get('address') + if address: + if isinstance(address, dict): + location_parts = [] + if address.get('addressLocality'): + location_parts.append(address['addressLocality']) + if address.get('addressRegion'): + location_parts.append(address['addressRegion']) + if address.get('addressCountry'): + location_parts.append(address['addressCountry']) + contact['location'] = ', '.join(location_parts) + elif isinstance(address, str): + contact['location'] = address + + return contact if contact.get('email') or contact.get('name') else None + + def _extract_microdata_contacts(self, soup: BeautifulSoup) -> List[Dict]: + """ + Extraire les contacts depuis les microdata + """ + contacts = [] + + # Chercher les éléments avec itemtype Person ou Organization + items = soup.find_all(attrs={'itemtype': re.compile(r'.*(Person|Organization|LocalBusiness).*')}) + + for item in items: + contact = {} + + # Nom + name_elem = item.find(attrs={'itemprop': 'name'}) + if name_elem: + contact['name'] = name_elem.get_text(strip=True) + + # Email + email_elem = item.find(attrs={'itemprop': 'email'}) + if email_elem: + contact['email'] = email_elem.get('href', '').replace('mailto:', '') or email_elem.get_text(strip=True) + + # Téléphone + phone_elem = item.find(attrs={'itemprop': 'telephone'}) + if phone_elem: + contact['phone'] = phone_elem.get_text(strip=True) + + if contact.get('email') or contact.get('name'): + contacts.append(contact) + + return contacts + + def _extract_location_info(self, soup: BeautifulSoup, text: str) -> str: + """ + Extraire les informations de localisation + """ + location_indicators = [ + r'\b\d{4,5}\s+[A-Za-zÀ-ÿ\s\-]+\b', # Code postal + ville + r'\b[A-Za-zÀ-ÿ\s\-]+,\s*[A-Za-zÀ-ÿ\s\-]+\b', # Ville, Pays + ] + + # Chercher dans les balises d'adresse + address_tags = soup.find_all(['address', 'div'], class_=re.compile(r'.*address.*|.*location.*|.*contact.*')) + for tag in address_tags: + address_text = tag.get_text(strip=True) + for pattern in location_indicators: + match = re.search(pattern, address_text, re.IGNORECASE) + if match: + return match.group(0) + + # Chercher dans le texte global + for pattern in location_indicators: + match = re.search(pattern, text, re.IGNORECASE) + if match: + return match.group(0) + + return '' + + def _enhance_contact_info(self, contact: Dict, soup: BeautifulSoup, text: str, structured_contacts: List[Dict], phones: List[str]): + """ + Améliorer les informations de contact en croisant les données + """ + email = contact['email'] + + # Chercher dans les contacts structurés + for struct_contact in structured_contacts: + if struct_contact.get('email') == email: + contact.update(struct_contact) + break + + # Si pas de nom trouvé, essayer d'extraire depuis l'email + if not contact['name']: + local_part = email.split('@')[0] + domain_part = email.split('@')[1] + + # Essayer de deviner le nom depuis la partie locale + if '.' in local_part: + parts = local_part.split('.') + contact['first_name'] = parts[0].title() + contact['last_name'] = parts[1].title() if len(parts) > 1 else '' + contact['name'] = f"{contact['first_name']} {contact['last_name']}".strip() + else: + contact['name'] = local_part.title() + + # Essayer de deviner l'entreprise depuis le domaine + if not contact['company']: + company_name = domain_part.split('.')[0] + contact['company'] = company_name.title() + + # Ajouter un numéro de téléphone si disponible + if not contact['phone'] and phones: + contact['phone'] = phones[0] # Prendre le premier numéro trouvé + + # Enrichir les notes avec des informations contextuelles + notes_parts = [] + if contact['location']: + notes_parts.append(f"Localisation: {contact['location']}") + + # Chercher des informations sur la fonction/titre + title_patterns = [ + r'(?i)(?:directeur|manager|responsable|chef|président|ceo|cto|cfo)\s+[a-zA-ZÀ-ÿ\s]+', + r'(?i)[a-zA-ZÀ-ÿ\s]+\s+(?:director|manager|head|chief|president)' + ] + + for pattern in title_patterns: + matches = re.findall(pattern, text) + if matches: + notes_parts.append(f"Fonction possible: {matches[0]}") + break + + contact['notes'] = ' | '.join(notes_parts) + + def _merge_contact_info(self, existing: Dict, new: Dict): + """ + Fusionner les informations de deux contacts + """ + for key, value in new.items(): + if value and not existing.get(key): + existing[key] = value + + # Fusionner les notes + if new.get('notes') and existing.get('notes'): + existing['notes'] = f"{existing['notes']} | {new['notes']}" + elif new.get('notes'): + existing['notes'] = new['notes'] + + def _extract_domain_info(self, url: str, results: Dict): + """ + Extraire les informations générales du domaine + """ + domain = urlparse(url).netloc + + results['domain_info'] = { + 'domain': domain, + 'company_guess': domain.split('.')[0].title(), + 'total_contacts': len(results['contacts']), + 'total_pages_scraped': len(results['pages_scraped']) + } + + def _extract_emails_from_links(self, soup: BeautifulSoup) -> Set[str]: + """ + Extraire les emails des liens mailto + """ + emails = set() + + # Chercher les liens mailto + mailto_links = soup.find_all('a', href=re.compile(r'^mailto:', re.I)) + for link in mailto_links: + href = link.get('href', '') + email_match = re.search(r'mailto:([^?&]+)', href, re.I) + if email_match: + email = email_match.group(1) + if self._is_valid_email(email): + emails.add(email.lower()) + + return emails + + def _extract_emails_from_text(self, text: str) -> Set[str]: + """ + Extraire les emails du texte de la page + """ + emails = set() + matches = self.email_pattern.findall(text) + + for email in matches: + # Filtrer les emails indésirables + if not self._is_valid_email(email): + continue + emails.add(email.lower()) + + return emails + + def _extract_internal_links(self, soup: BeautifulSoup, base_url: str) -> List[str]: + """ + Extraire les liens internes de la page + """ + links = [] + base_domain = urlparse(base_url).netloc + + for link in soup.find_all('a', href=True): + href = link['href'] + full_url = urljoin(base_url, href) + parsed_link = urlparse(full_url) + + # Vérifier que c'est un lien interne et pas déjà visité + if (parsed_link.netloc == base_domain and + full_url not in self.visited_urls and + not self._is_excluded_link(full_url)): + links.append(full_url) + + return links + + def _is_valid_email(self, email: str) -> bool: + """ + Vérifier si l'email est valide et non indésirable + """ + # Filtrer les extensions de fichiers communes + excluded_extensions = ['.jpg', '.png', '.gif', '.pdf', '.doc', '.css', '.js'] + + for ext in excluded_extensions: + if email.lower().endswith(ext): + return False + + # Filtrer les emails génériques indésirables + excluded_patterns = [ + 'example.com', + 'test.com', + 'placeholder', + 'your-email', + 'youremail', + 'email@', + 'noreply', + 'no-reply' + ] + + for pattern in excluded_patterns: + if pattern in email.lower(): + return False + + # Vérifier la longueur + if len(email) < 5 or len(email) > 254: + return False + + return True + + def _is_excluded_link(self, url: str) -> bool: + """ + Vérifier si le lien doit être exclu du scraping + """ + excluded_patterns = [ + '#', + 'javascript:', + 'tel:', + 'mailto:', + '.pdf', + '.doc', + '.zip', + '.jpg', + '.png', + '.gif' + ] + + url_lower = url.lower() + for pattern in excluded_patterns: + if pattern in url_lower: + return True + + return False + + def save_results(self, results: Dict, filename: str = None) -> str: + """ + Sauvegarder les résultats dans un fichier JSON + """ + if not filename: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + domain = urlparse(results['url']).netloc.replace('.', '_') + filename = f"scraping_{domain}_{timestamp}.json" + + # Créer le dossier s'il n'existe pas + scraping_folder = 'Data/email_scraping' + os.makedirs(scraping_folder, exist_ok=True) + + filepath = os.path.join(scraping_folder, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(results, f, ensure_ascii=False, indent=2) + + return filepath + +class EmailScrapingHistory: + def __init__(self): + self.history_folder = 'Data/email_scraping' + os.makedirs(self.history_folder, exist_ok=True) + + def get_all_scrapings(self) -> List[Dict]: + """ + Récupérer l'historique de tous les scrapings + """ + scrapings = [] + + for filename in os.listdir(self.history_folder): + if filename.endswith('.json'): + filepath = os.path.join(self.history_folder, filename) + try: + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + scrapings.append({ + 'filename': filename, + 'url': data.get('url', ''), + 'emails_count': len(data.get('contacts', data.get('emails', []))), # Support pour ancienne et nouvelle structure + 'pages_count': len(data.get('pages_scraped', [])), + 'start_time': data.get('start_time', ''), + 'errors_count': len(data.get('errors', [])) + }) + except Exception as e: + print(f"Erreur lors de la lecture de {filename}: {e}") + + # Trier par date (plus récent d'abord) + scrapings.sort(key=lambda x: x.get('start_time', ''), reverse=True) + + return scrapings + + def get_scraping_details(self, filename: str) -> Dict: + """ + Récupérer les détails d'un scraping spécifique + """ + filepath = os.path.join(self.history_folder, filename) + + if os.path.exists(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + return json.load(f) + + return None + + def delete_scraping(self, filename: str) -> bool: + """ + Supprimer un fichier de scraping + """ + filepath = os.path.join(self.history_folder, filename) + + if os.path.exists(filepath): + try: + os.remove(filepath) + return True + except Exception as e: + print(f"Erreur lors de la suppression: {e}") + return False + + return False diff --git a/modules/projects/project.py b/modules/projects/project.py new file mode 100644 index 0000000..dd85349 --- /dev/null +++ b/modules/projects/project.py @@ -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"), + ) diff --git a/modules/projects/project_handler.py b/modules/projects/project_handler.py new file mode 100644 index 0000000..fd9a3bb --- /dev/null +++ b/modules/projects/project_handler.py @@ -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) diff --git a/modules/projects/routes.py b/modules/projects/routes.py new file mode 100644 index 0000000..f4387a7 --- /dev/null +++ b/modules/projects/routes.py @@ -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/ 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() + ) diff --git a/modules/proposition/app.py b/modules/proposition/app.py new file mode 100644 index 0000000..fcca82c --- /dev/null +++ b/modules/proposition/app.py @@ -0,0 +1,27 @@ +# Importation des modules nécessaires +from modules.proposition.fields import fields +from core.form import Form +from core.generator import Generator +from core.data import Data + +def main(): + print("=== Générateur de proposition commerciale ===\n") + + form = Form(fields()) + form.ask() + data = form.get_data() + + # Transformer les fonctionnalités en une liste de dictionnaires + features = data.get("features", "").split(",") + data["features"] = [{"description": feature.strip()} for feature in features if feature.strip()] + + client_name = data.get("client_name", "").replace(" ", "_").lower() + + data_manager = Data(f"Data/clients/{client_name}.json") + client_data = data_manager.save_data(data) + print("\n✅ Données du client enregistrées avec succès.") + + generator = Generator(data) + content = generator.generate_pdf("propositions") + + print("\n✅ Proposition générée avec succès.") \ No newline at end of file diff --git a/modules/proposition/fields.py b/modules/proposition/fields.py new file mode 100644 index 0000000..58618f3 --- /dev/null +++ b/modules/proposition/fields.py @@ -0,0 +1,16 @@ +def fields() -> list[dict]: + return [ + {"name": "client_name", "label": "Nom du client", "type": "text", "required": True}, + {"name": "email", "label": "Email", "type": "email"}, + {"name": "telephone", "label": "Téléphone", "type": "tel"}, + {"name": "adresse", "label": "Adresse", "type": "text"}, + {"name": "project_name", "label": "Nom du projet", "type": "text", "required": True}, + {"name": "project_type", "label": "Type de projet", "type": "text"}, + {"name": "deadline", "label": "Délai de livraison", "type": "date"}, + {"name": "project_description", "label": "Description du projet", "type": "textarea"}, + {"name": "features", "label": "Fonctionnalités principales (séparées par des virgules)", "type": "textarea"}, + {"name": "budget", "label": "Budget estimé", "type": "text"}, + {"name": "payment_terms", "label": "Conditions de paiement", "type": "text"}, + {"name": "contact_info", "label": "Coordonnées", "type": "text"}, + {"name": "additional_info", "label": "Informations complémentaires", "type": "textarea"} + ] \ No newline at end of file diff --git a/modules/tasks/__init__.py b/modules/tasks/__init__.py new file mode 100644 index 0000000..2332bdb --- /dev/null +++ b/modules/tasks/__init__.py @@ -0,0 +1,4 @@ +from .task import Task +from .task_handler import TaskHandler + +__all__ = ["Task", "TaskHandler"] diff --git a/modules/tasks/task.py b/modules/tasks/task.py new file mode 100644 index 0000000..11b8bec --- /dev/null +++ b/modules/tasks/task.py @@ -0,0 +1,64 @@ +from datetime import datetime +from typing import Optional, Literal, Dict, Any +import uuid + + +class Task: + def __init__( + self, + title: str, + due_date: str, + description: str = "", + entity_type: Optional[Literal["client", "prospect", "project", "campaign"]] = None, + entity_id: Optional[str] = None, + priority: Literal["basse", "normale", "haute"] = "normale", + status: Literal["todo", "done", "canceled"] = "todo", + id: Optional[str] = None, + created_at: Optional[str] = None, + completed_at: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ): + self.id = id or f"tsk_{uuid.uuid4().hex[:8]}" + self.title = title + self.description = description + # due_date: format ISO "YYYY-MM-DD" + self.due_date = due_date + self.entity_type = entity_type + self.entity_id = entity_id + self.priority = priority + self.status = status + now_iso = datetime.utcnow().isoformat() + self.created_at = created_at or now_iso + self.completed_at = completed_at + self.metadata = metadata or {} + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "title": self.title, + "description": self.description, + "due_date": self.due_date, + "entity_type": self.entity_type, + "entity_id": self.entity_id, + "priority": self.priority, + "status": self.status, + "created_at": self.created_at, + "completed_at": self.completed_at, + "metadata": self.metadata, + } + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Task": + return Task( + id=data.get("id"), + title=data.get("title", ""), + description=data.get("description", ""), + due_date=data.get("due_date", ""), + entity_type=data.get("entity_type"), + entity_id=data.get("entity_id"), + priority=data.get("priority", "normale"), + status=data.get("status", "todo"), + created_at=data.get("created_at"), + completed_at=data.get("completed_at"), + metadata=data.get("metadata") or {}, + ) diff --git a/modules/tasks/task_handler.py b/modules/tasks/task_handler.py new file mode 100644 index 0000000..9e82e89 --- /dev/null +++ b/modules/tasks/task_handler.py @@ -0,0 +1,207 @@ +import json +import os +from datetime import date, datetime, timedelta +from typing import List, Optional, Literal + +from modules.tasks.task import Task + + +class TaskHandler: + def __init__(self, base_dir: Optional[str] = None): + # Par défaut: Data/tasks (un fichier JSON par tâche) + self.base_dir = base_dir or os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "Data", + "tasks", + ) + os.makedirs(self.base_dir, exist_ok=True) + + def _task_path(self, task_id: str) -> str: + return os.path.join(self.base_dir, f"{task_id}.json") + + def add_task(self, task: Task) -> str: + path = self._task_path(task.id) + with open(path, "w", encoding="utf-8") as f: + json.dump(task.to_dict(), f, ensure_ascii=False, indent=2) + return task.id + + def get_task(self, task_id: str) -> Optional[Task]: + path = self._task_path(task_id) + if not os.path.exists(path): + return None + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return Task.from_dict(data) + + def update_task(self, task: Task) -> bool: + path = self._task_path(task.id) + if not os.path.exists(path): + return False + # pas de gestion updated_at pour rester simple + with open(path, "w", encoding="utf-8") as f: + json.dump(task.to_dict(), f, ensure_ascii=False, indent=2) + return True + + def delete_task(self, task_id: str) -> bool: + path = self._task_path(task_id) + if os.path.exists(path): + try: + os.remove(path) + return True + except Exception: + return False + return False + + def list_tasks(self, status: Optional[Literal["todo", "done", "canceled"]] = None) -> List[Task]: + tasks: List[Task] = [] + for filename in os.listdir(self.base_dir): + if filename.endswith(".json"): + try: + with open(os.path.join(self.base_dir, filename), "r", encoding="utf-8") as f: + data = json.load(f) + t = Task.from_dict(data) + if status is None or t.status == status: + tasks.append(t) + except Exception: + continue + # Tri par (due_date, priorité, création) + def _prio_val(p: str) -> int: + return {"haute": 0, "normale": 1, "basse": 2}.get(p, 1) + + tasks.sort(key=lambda t: (t.due_date or "", _prio_val(t.priority), t.created_at or "")) + return tasks + + def list_tasks_for_date( + self, + day_iso: str, + status: Optional[Literal["todo", "done", "canceled"]] = None, + ) -> List[Task]: + return [t for t in self.list_tasks(status=status) if (t.due_date or "") == day_iso] + + def list_today_tasks(self, status: Optional[Literal["todo", "done", "canceled"]] = None) -> List[Task]: + return self.list_tasks_for_date(date.today().isoformat(), status=status) + + def list_tomorrow_tasks(self, status: Optional[Literal["todo", "done", "canceled"]] = None) -> List[Task]: + tomorrow = (date.today() + timedelta(days=1)).isoformat() + return self.list_tasks_for_date(tomorrow, status=status) + + def list_overdue_tasks(self) -> List[Task]: + """Retourne les tâches en retard (due_date < aujourd'hui) encore à faire.""" + today_iso = date.today().isoformat() + tasks = self.list_tasks(status="todo") + return [t for t in tasks if (t.due_date or "") < today_iso] + + def list_tasks_by_entity( + self, + entity_type: Literal["client", "prospect", "project", "campaign"], + entity_id: str, + status: Optional[Literal["todo", "done", "canceled"]] = None, + ) -> List[Task]: + return [ + t + for t in self.list_tasks(status=status) + if t.entity_type == entity_type and t.entity_id == entity_id + ] + + def complete_task(self, task_id: str) -> bool: + t = self.get_task(task_id) + if not t: + return False + t.status = "done" + t.completed_at = datetime.utcnow().isoformat() + return self.update_task(t) + + def cancel_task(self, task_id: str) -> bool: + t = self.get_task(task_id) + if not t: + return False + t.status = "canceled" + return self.update_task(t) + + def postpone_task(self, task_id: str, new_due_date: str) -> bool: + t = self.get_task(task_id) + if not t: + return False + t.due_date = new_due_date + return self.update_task(t) + + # Raccourcis pour créer des tâches liées + def add_task_for_client( + self, + client_id: str, + title: str, + due_date: str, + description: str = "", + priority: Literal["basse", "normale", "haute"] = "normale", + metadata: Optional[dict] = None, + ) -> str: + task = Task( + title=title, + description=description, + due_date=due_date, + entity_type="client", + entity_id=client_id, + priority=priority, + metadata=metadata or {}, + ) + return self.add_task(task) + + def add_task_for_prospect( + self, + prospect_id: str, + title: str, + due_date: str, + description: str = "", + priority: Literal["basse", "normale", "haute"] = "normale", + metadata: Optional[dict] = None, + ) -> str: + task = Task( + title=title, + description=description, + due_date=due_date, + entity_type="prospect", + entity_id=prospect_id, + priority=priority, + metadata=metadata or {}, + ) + return self.add_task(task) + + def add_task_for_project( + self, + project_id: str, + title: str, + due_date: str, + description: str = "", + priority: Literal["basse", "normale", "haute"] = "normale", + metadata: Optional[dict] = None, + ) -> str: + task = Task( + title=title, + description=description, + due_date=due_date, + entity_type="project", + entity_id=project_id, + priority=priority, + metadata=metadata or {}, + ) + return self.add_task(task) + + def add_task_for_campaign( + self, + campaign_id: str, + title: str, + due_date: str, + description: str = "", + priority: Literal["basse", "normale", "haute"] = "normale", + metadata: Optional[dict] = None, + ) -> str: + task = Task( + title=title, + description=description, + due_date=due_date, + entity_type="campaign", + entity_id=campaign_id, + priority=priority, + metadata=metadata or {}, + ) + return self.add_task(task) diff --git a/modules/tasks/web.py b/modules/tasks/web.py new file mode 100644 index 0000000..b97bb94 --- /dev/null +++ b/modules/tasks/web.py @@ -0,0 +1,275 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from datetime import date, datetime +from modules.tasks.task_handler import TaskHandler +from modules.tasks.task import Task +from modules.email.draft_handler import DraftHandler +from modules.email.email_manager import EmailSender +from jobs.daily_reminder_job import main as generate_email_drafts_today +from html import escape + +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") + + +@tasks_bp.get("/") +def tasks_index(): + """ + Page de gestion des tâches: liste + formulaire de création. + Filtres optionnels via query string: status, entity_type, entity_id + """ + handler = TaskHandler() + status = request.args.get("status") + entity_type = request.args.get("entity_type") + entity_id = request.args.get("entity_id") + + tasks = handler.list_tasks(status=status) if status else handler.list_tasks() + if entity_type and entity_id: + tasks = [t for t in tasks if t.entity_type == entity_type and t.entity_id == entity_id] + + return render_template( + "tasks/manage.html", + tasks=tasks, + status=status, + entity_type=entity_type, + entity_id=entity_id, + today=date.today().isoformat(), + ) + + +@tasks_bp.get("/today/fragment") +def today_fragment(): + """ + Renvoie le fragment HTML du bloc 'tâches du jour' + """ + handler = TaskHandler() + today_tasks = handler.list_today_tasks(status="todo") + return render_template("partials/tasks_today_block.html", today_tasks=today_tasks) + +@tasks_bp.get('/tomorrow/fragment') +def tomorrow_fragment(): + """ + Renvoie le fragment HTML du bloc 'tâches du lendemain' + """ + handler = TaskHandler() + tomorrow_tasks = handler.list_tomorrow_tasks(status="todo") + return render_template("partials/tasks_today_block.html", today_tasks=tomorrow_tasks) + +@tasks_bp.get("/today/count") +def today_count(): + """ + Renvoie le nombre de tâches du jour (todo) au format JSON, pour affichage dans un badge. + """ + handler = TaskHandler() + count = len(handler.list_today_tasks(status="todo")) + return jsonify({"count": count}) + + +@tasks_bp.post("/create") +def create_task(): + """ + Création d'une tâche depuis un formulaire. + Champs attendus: title, due_date (YYYY-MM-DD), description, priority, entity_type, entity_id + """ + title = (request.form.get("title") or "").strip() + due_date = (request.form.get("due_date") or "").strip() + description = (request.form.get("description") or "").strip() + priority = (request.form.get("priority") or "normale").strip() + entity_type = (request.form.get("entity_type") or "").strip() or None + entity_id = (request.form.get("entity_id") or "").strip() or None + + if not title or not due_date: + flash("Titre et échéance sont obligatoires.", "warning") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + handler = TaskHandler() + task = Task( + title=title, + due_date=due_date, + description=description, + priority=priority, + entity_type=entity_type, + entity_id=entity_id, + ) + handler.add_task(task) + flash("Tâche créée avec succès.", "success") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + +@tasks_bp.post("/status") +def set_status(): + """ + Met à jour le statut d'une tâche (todo|done|canceled). + Form data: task_id, status + """ + task_id = (request.form.get("task_id") or "").strip() + status = (request.form.get("status") or "todo").strip() + if not task_id or status not in ("todo", "done", "canceled"): + flash("Requête invalide pour le changement de statut.", "warning") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + handler = TaskHandler() + task = handler.get_task(task_id) + if not task: + flash("Tâche introuvable.", "danger") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + task.status = status + task.completed_at = datetime.utcnow().isoformat() if status == "done" else None + if handler.update_task(task): + flash("Statut de la tâche mis à jour.", "success") + else: + flash("Échec de la mise à jour du statut.", "danger") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + +@tasks_bp.post("/delete") +def delete_task(): + """ + Supprime une tâche. + Form data: task_id + """ + task_id = (request.form.get("task_id") or "").strip() + if not task_id: + flash("Requête invalide pour la suppression.", "warning") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + handler = TaskHandler() + if handler.delete_task(task_id): + flash("Tâche supprimée.", "success") + else: + flash("Échec de la suppression.", "danger") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + +@tasks_bp.post("/update") +def update_task(): + """ + Met à jour les champs d'une tâche. + Form data: task_id, title, due_date, description, priority, entity_type, entity_id + """ + task_id = (request.form.get("task_id") or "").strip() + if not task_id: + flash("Requête invalide pour l'édition.", "warning") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + handler = TaskHandler() + task = handler.get_task(task_id) + if not task: + flash("Tâche introuvable.", "danger") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + # Mise à jour des champs + task.title = (request.form.get("title") or task.title).strip() + task.due_date = (request.form.get("due_date") or task.due_date).strip() + task.description = (request.form.get("description") or task.description).strip() + task.priority = (request.form.get("priority") or task.priority).strip() + task.entity_type = (request.form.get("entity_type") or task.entity_type or "").strip() or None + task.entity_id = (request.form.get("entity_id") or task.entity_id or "").strip() or None + + if handler.update_task(task): + flash("Tâche modifiée.", "success") + else: + flash("Échec de la modification.", "danger") + return redirect(request.referrer or url_for("tasks.tasks_index")) + + +@tasks_bp.get("/email-drafts") +def email_drafts_list(): + """ + Liste des brouillons d'emails à envoyer + actions UI (via template). + """ + dh = DraftHandler() + drafts = dh.list_pending() + return render_template("tasks/email_drafts.html", drafts=drafts) + + +@tasks_bp.post("/email-drafts/generate") +def email_drafts_generate(): + """ + Déclenche la génération des brouillons d'aujourd'hui (équivalent au job quotidien). + """ + try: + count = generate_email_drafts_today() + if count: + flash(f"{count} brouillon(s) généré(s) pour aujourd'hui.", "success") + else: + flash("Aucun brouillon créé. Vérifiez: tâches 'todo' dues aujourd'hui, liées à des prospects avec un email.", "info") + except Exception as e: + flash(f"Erreur lors de la génération: {e}", "danger") + return redirect(url_for("tasks.email_drafts_list")) + + +@tasks_bp.post("/email-drafts/send") +def email_drafts_send(): + """ + Envoie un brouillon d'email sélectionné puis met à jour son statut. + """ + draft_id = (request.form.get("draft_id") or "").strip() + if not draft_id: + flash("Brouillon invalide", "warning") + return redirect(url_for("tasks.email_drafts_list")) + + dh = DraftHandler() + draft = dh.get_draft(draft_id) + if not draft: + flash("Brouillon introuvable", "danger") + return redirect(url_for("tasks.email_drafts_list")) + + sender = EmailSender() + try: + res = sender.send_tracked_email( + to_email=draft.to_email, + subject=draft.subject, + body=draft.content, + prospect_id=draft.prospect_id, + template_id=draft.template_id, + ) + if res.get("success"): + dh.mark_sent(draft.id, success=True) + flash("Email envoyé.", "success") + else: + dh.mark_sent(draft.id, success=False, error_message=res.get("error")) + flash("Échec de l'envoi de l'email.", "danger") + except Exception as e: + dh.mark_sent(draft.id, success=False, error_message=str(e)) + flash("Erreur lors de l'envoi de l'email.", "danger") + + return redirect(url_for("tasks.email_drafts_list")) + + +@tasks_bp.get("/quick-add") +def quick_add_page(): + """ + Page complète 'Quick Add Task' pour une entité donnée. + Paramètres query: entity_type (client|prospect|project), entity_id + """ + entity_type = (request.args.get("entity_type") or "").strip() + entity_id = (request.args.get("entity_id") or "").strip() + if not entity_type or not entity_id: + flash("Paramètres manquants pour lier la tâche à une entité.", "warning") + return redirect(url_for("tasks.tasks_index")) + + return render_template( + "tasks/quick_add.html", + entity_type=entity_type, + entity_id=entity_id, + today=date.today().isoformat(), + ) + + +@tasks_bp.get("/quick-add/fragment") +def quick_add_fragment(): + """ + Fragment HTML du formulaire 'Quick Add Task' intégré dans d'autres pages. + Paramètres query: entity_type, entity_id + """ + entity_type = (request.args.get("entity_type") or "").strip() + entity_id = (request.args.get("entity_id") or "").strip() + if not entity_type or not entity_id: + return "

Paramètres manquants pour le formulaire de tâche liée.

" + + return render_template( + "partials/task_quick_add_form.html", + entity_type=entity_type, + entity_id=entity_id, + today=date.today().isoformat(), + ) \ No newline at end of file diff --git a/modules/tracking/store.py b/modules/tracking/store.py new file mode 100644 index 0000000..f40eb51 --- /dev/null +++ b/modules/tracking/store.py @@ -0,0 +1,57 @@ +import os +import json +from datetime import datetime +from typing import Optional, Dict, Any, List + + +class TrackingStore: + """ + Stocke les enregistrements d'emails trackés et leurs événements. + Répertoire: Data/email_tracking + Fichier par tracking_id: Data/email_tracking/.json + """ + def __init__(self, base_dir: Optional[str] = None): + base_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.base_dir = base_dir or os.path.join(base_root, "Data", "email_tracking") + os.makedirs(self.base_dir, exist_ok=True) + + def _path(self, tracking_id: str) -> str: + return os.path.join(self.base_dir, f"{tracking_id}.json") + + def create_record(self, tracking_id: str, record: Dict[str, Any]) -> None: + record = dict(record) + record.setdefault("created_at", datetime.utcnow().isoformat()) + record.setdefault("events", []) # list of {"type": "...","ts": "...", ...} + with open(self._path(tracking_id), "w", encoding="utf-8") as f: + json.dump(record, f, ensure_ascii=False, indent=2) + + def get_record(self, tracking_id: str) -> Optional[Dict[str, Any]]: + p = self._path(tracking_id) + if not os.path.exists(p): + return None + with open(p, "r", encoding="utf-8") as f: + return json.load(f) + + def update_record(self, tracking_id: str, updater) -> Optional[Dict[str, Any]]: + rec = self.get_record(tracking_id) + if rec is None: + return None + new_rec = updater(dict(rec)) or rec + with open(self._path(tracking_id), "w", encoding="utf-8") as f: + json.dump(new_rec, f, ensure_ascii=False, indent=2) + return new_rec + + def add_event(self, tracking_id: str, event_type: str, meta: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]: + meta = meta or {} + def _upd(rec: Dict[str, Any]): + events: List[Dict[str, Any]] = rec.get("events") or [] + events.append({"type": event_type, "ts": datetime.utcnow().isoformat(), **meta}) + rec["events"] = events + # counters + if event_type == "open": + rec["opens"] = int(rec.get("opens") or 0) + 1 + if event_type == "click": + rec["clicks"] = int(rec.get("clicks") or 0) + 1 + return rec + + return self.update_record(tracking_id, _upd) diff --git a/propositions/agree_proposition_commercial.pdf b/propositions/agree_proposition_commercial.pdf new file mode 100644 index 0000000..1aab39c Binary files /dev/null and b/propositions/agree_proposition_commercial.pdf differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3528fa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +# Dépendances principales +fpdf==1.7.2 +# Interface utilisateur +# ttkthemes - commenté car nécessite Tk 8.6+ +# alternatives à ttkthemes: +customtkinter==5.2.1 +flask +pillow==11.3.0 +packaging +# Fenêtre webview (mode application desktop) +pywebview>=4.4 +# Utilitaires +python-dateutil==2.8.2 +# Formatage +pyyaml==6.0.1 +# Web scraping +requests>=2.25.1 +beautifulsoup4>=4.9.3 +lxml>=4.6.3 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100755 index 0000000..c87e28b --- /dev/null +++ b/run.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +import os +import sys +import importlib.util +import threading +import time + +def check_dependency(package_name): + """Vérifie si un package est installé.""" + if package_name.lower() == "pillow": + try: + import PIL + return True + except ImportError: + return False + return importlib.util.find_spec(package_name) is not None + +def check_dependencies(require_webview: bool = False): + """Vérifie que toutes les dépendances requises sont installées.""" + required_packages = ["fpdf", "dateutil", "pillow", "packaging"] + missing_packages = [] + + # Vérifie les dépendances de base + for package in required_packages: + if not check_dependency(package): + missing_packages.append(package) + + # Vérifier pour CustomTkinter si demandé explicitement + if "--ctk" in sys.argv: + if not check_dependency("customtkinter"): + print("Le package 'customtkinter' est requis pour le mode GUI CustomTkinter.") + print("Veuillez l'installer avec :") + print("pip install customtkinter") + return False + + # Vérifier pywebview si demandé + if require_webview and not check_dependency("webview"): + print("Le mode --webview nécessite le package 'pywebview'.") + print("Veuillez l'installer avec :") + print("pip install pywebview") + return False + + if missing_packages: + print("Certaines dépendances requises ne sont pas installées:") + for package in missing_packages: + print(f" - {package}") + print("\nVeuillez exécuter le script d'installation:") + print("python install.py") + return False + + return True + +def check_directories(): + """Vérifie que les répertoires nécessaires existent.""" + directories = [ + "Data", + "Data/clients", + "Data/prospects", + "output", + "output/devis", + "output/propositions" + ] + + for directory in directories: + dir_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory) + if not os.path.exists(dir_path): + print(f"Le répertoire {directory} n'existe pas.") + print("Veuillez exécuter le script d'installation:") + print("python install.py") + return False + + return True + +def _run_flask_server(host: str = "127.0.0.1", port: int = 8080, debug: bool = False): + """Démarre le serveur Flask (sans reloader)""" + from app import app + from modules.tasks.web import tasks_bp + from modules.crm.web import crm_api_bp + app.register_blueprint(tasks_bp) + app.register_blueprint(crm_api_bp) + # use_reloader=False pour éviter le double démarrage lorsque lancé en thread + app.run(debug=debug, host=host, port=port, use_reloader=False) + +def launch_application(on_browser: bool = False): + """Lance l'application principale, soit en mode webview, soit en mode navigateur.""" + if not on_browser: + # Lancer Flask en arrière-plan + server_thread = threading.Thread(target=_run_flask_server, kwargs={"host": "127.0.0.1", "port": 8080, "debug": False}, daemon=True) + server_thread.start() + + # Petite attente pour laisser le serveur démarrer + time.sleep(0.5) + + # Ouvrir la fenêtre desktop + import webview + webview.create_window("Suite Consultance", "http://127.0.0.1:8080", width=1280, height=800) + webview.start() + else: + # Mode interface web avec Flask classique + from app import app + from modules.tasks.web import tasks_bp + from modules.crm.web import crm_api_bp + app.register_blueprint(tasks_bp) + app.register_blueprint(crm_api_bp) + app.run(debug=True, host='0.0.0.0', port=8080) + +if __name__ == "__main__": + app_mode_flag = "--browser" in sys.argv + launch_application(on_browser=app_mode_flag) diff --git a/silent_run.py b/silent_run.py new file mode 100755 index 0000000..9e3e887 --- /dev/null +++ b/silent_run.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess + +# Supprimer l'avertissement de dépréciation de Tk +os.environ["TK_SILENCE_DEPRECATION"] = "1" + +# Construire la commande pour exécuter l'application +cmd = [sys.executable, "run.py"] +if len(sys.argv) > 1: + cmd.extend(sys.argv[1:]) + +# Exécuter l'application +subprocess.run(cmd) diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..587526d --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,1027 @@ +/* Suite Consultance - Style principal */ + +:root { + /* Couleurs de base */ + --primary-color: #007bff; + --primary-hover: #0069d9; + --secondary-color: #6c757d; + --success-color: #28a745; + --info-color: #17a2b8; + --warning-color: #ffc107; + --danger-color: #dc3545; + --light-color: #f8f9fa; + --dark-color: #343a40; + --body-bg: #f8f9fa; + --card-bg: #ffffff; + --text-color: #212529; + --border-color: #dee2e6; + + /* Couleurs spécifiques pour chaque module */ + --crm-color: #6a4c93; + --crm-hover: #593c80; + --crm-light: #d0bdf4; + + --propositions-color: #1a936f; + --propositions-hover: #127a5c; + --propositions-light: #88d498; + + --devis-color: #e76f51; + --devis-hover: #d65f41; + --devis-light: #f4a261; + + --project-color: #ce476d; + --project-hover: #b3375a; + --project-light: #f087a1; + + --task-color: #007bff; + --task-hover: #0062cc; + --task-light: #66afe9; + + --email-color: #a79228; + --email-hover: #8a7a1f; + --email-light: #f4d03f; + + /* Couleurs des boutons standard */ + --btn-text-color: #fff; + --btn-text-hover: #fff; +} + +/* Mode sombre */ +body.dark-mode { + --primary-color: #0d6efd; + --primary-hover: #0b5ed7; + --secondary-color: #6c757d; + --body-bg: #121212; + --card-bg: #1e1e1e; + --text-color: #f8f9fa; + --border-color: #495057; + --light-color: #2c2c2c; /* Redéfinition pour le mode sombre */ + --success-color: #198754; + --info-color: #0dcaf0; + --warning-color: #ffc107; + --danger-color: #dc3545; + + /* Couleurs des modules en mode sombre */ + --crm-color: #8e72b4; + --crm-hover: #7c5fa3; + --crm-light: #6a4c93; + + --propositions-color: #3cb991; + --propositions-hover: #2ca57d; + --propositions-light: #1a936f; + + --devis-color: #f4845f; + --devis-hover: #e76f51; + --devis-light: #d65f41; +} + +body { + background-color: var(--body-bg); + color: var(--text-color); + transition: background-color 0.5s ease, color 0.5s ease; +} + +/* Transition fluide pour tous les éléments */ +*, *::before, *::after { + transition: background-color 0.5s ease, border-color 0.5s ease, color 0.3s ease, box-shadow 0.3s ease; +} + +/* Mais pas pour les animations et transformations */ +.fade-in, .fade-out, +[class*="animation-"], +[class*="transition-"], +.modal.fade, +.collapsing, +.carousel-item { + transition: none; +} + +.dark-mode .navbar { + background-color: #343a40 !important; +} + +.dark-mode .card { + background-color: var(--card-bg); + border-color: var(--border-color); +} + +.dark-mode .table { + color: var(--text-color); +} + +.dark-mode .footer { + background-color: #343a40 !important; +} + +.dark-mode .text-muted { + color: #adb5bd !important; +} + +.dark-mode .form-control, +.dark-mode .form-select { + background-color: #2c2c2c; + border-color: var(--border-color); + color: var(--text-color); +} + +.dark-mode .input-group-text { + background-color: #2c2c2c; + border-color: var(--border-color); + color: var(--text-color); +} + +.dark-mode .btn-outline-secondary { + color: #adb5bd; + border-color: #adb5bd; +} + +.dark-mode .modal-content { + background-color: var(--card-bg); + color: var(--text-color); +} + +.dark-mode .list-group-item { + background-color: var(--card-bg); + color: var(--text-color); + border-color: var(--border-color); +} + +.dark-mode a { + color: #6ea8fe; +} + +.dark-mode a:hover { + color: #9ec5fe; +} + +.dark-mode .navbar-dark .navbar-nav .nav-link { + color: #ffffff !important; +} + +.dark-mode .navbar-dark .navbar-nav .nav-link:hover, +.dark-mode .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.9) !important; +} + +.dark-mode .navbar-dark .navbar-nav .nav-link.active { + color: #ffffff !important; +} + +/* Styles supplémentaires pour le mode sombre */ +.dark-mode .bg-light { + background-color: #2c2c2c !important; +} + +.dark-mode .border { + border-color: var(--border-color) !important; +} + +.dark-mode .alert { + background-color: #2c2c2c; + color: var(--text-color); + border-color: var(--border-color); +} + +.dark-mode .alert-success { + background-color: rgba(25, 135, 84, 0.2); + color: #75b798; + border-color: #198754; +} + +.dark-mode .alert-danger { + background-color: rgba(220, 53, 69, 0.2); + color: #ea868f; + border-color: #dc3545; +} + +.dark-mode .alert-warning { + background-color: rgba(255, 193, 7, 0.2); + color: #ffda6a; + border-color: #ffc107; +} + +.dark-mode .alert-info { + background-color: rgba(13, 202, 240, 0.2); + color: #6edff6; + border-color: #0dcaf0; +} + +.dark-mode .dropdown-menu { + background-color: #2c2c2c; + border-color: var(--border-color); +} + +.dark-mode .dropdown-item { + color: var(--text-color); +} + +.dark-mode .dropdown-item:hover { + background-color: #3e3e3e; + color: var(--text-color); +} + +/* Styles des dropdowns par module */ +body[data-module="crm"] .dropdown-item:hover { + background-color: rgba(106, 76, 147, 0.1); +} + +body[data-module="propositions"] .dropdown-item:hover { + background-color: rgba(26, 147, 111, 0.1); +} + +body[data-module="devis"] .dropdown-item:hover { + background-color: rgba(231, 111, 81, 0.1); +} + +.dark-mode[data-module="crm"] .dropdown-item:hover { + background-color: rgba(208, 189, 244, 0.1); +} + +.dark-mode[data-module="propositions"] .dropdown-item:hover { + background-color: rgba(136, 212, 152, 0.1); +} + +.dark-mode[data-module="devis"] .dropdown-item:hover { + background-color: rgba(244, 162, 97, 0.1); +} + +.dark-mode .jumbotron { + background-color: var(--card-bg); + color: var(--text-color); +} + +.dark-mode .breadcrumb { + background-color: #2c2c2c; +} + +.dark-mode .page-link { + background-color: #2c2c2c; + border-color: var(--border-color); + color: var(--text-color); +} + +.dark-mode .page-item.active .page-link { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.dark-mode .badge { + background-color: #3e3e3e; + color: var(--text-color); +} + +.dark-mode hr { + border-color: var(--border-color); +} + +.dark-mode .modal-header, +.dark-mode .modal-footer { + border-color: var(--border-color); +} + +.dark-mode .form-check-input { + background-color: #3e3e3e; + border-color: var(--border-color); +} + +/* Cartes */ +.card { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + transition: box-shadow 0.3s ease; + border-radius: 0.5rem; + overflow: hidden; +} + +.card:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +/* Styles de carte spécifiques aux modules */ +body[data-module="crm"] .card { + border-top: 3px solid var(--crm-color); +} + +body[data-module="propositions"] .card { + border-top: 3px solid var(--propositions-color); +} + +body[data-module="devis"] .card { + border-top: 3px solid var(--devis-color); +} + +.dark-mode[data-module="crm"] .card { + border-top: 3px solid var(--crm-light); +} + +.dark-mode[data-module="propositions"] .card { + border-top: 3px solid var(--propositions-light); +} + +.dark-mode[data-module="devis"] .card { + border-top: 3px solid var(--devis-light); +} + +/* Navigation */ +.navbar { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.navbar-brand { + font-weight: bold; +} + +/* Style pour les liens de la barre de navigation principale uniquement */ +.navbar .nav-link { + font-weight: 500; + color: #ffffff !important; +} + +.navbar .nav-link:hover { + color: rgba(255, 255, 255, 0.9) !important; +} + +.navbar .nav-link.active { + font-weight: 600; + color: #ffffff !important; +} + +/* Styles des onglets par module */ +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-link { + margin-bottom: -1px; + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + color: var(--secondary-color); + background-color: transparent; +} + +.nav-tabs .nav-link:hover { + border-color: #e9ecef #e9ecef #dee2e6; + color: var(--primary-color); + background-color: rgba(0, 0, 0, 0.02); +} + +.nav-tabs .nav-link.active { + color: var(--primary-color); + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; + border-bottom-width: 2px; +} + +/* CRM Tab Style */ +body[data-module="crm"] .nav-tabs { + border-bottom-color: var(--crm-color); +} + +body[data-module="crm"] .nav-tabs .nav-link.active { + border-bottom: 2px solid var(--crm-color); +} + +/* Propositions Tab Style */ +body[data-module="propositions"] .nav-tabs { + border-bottom-color: var(--propositions-color); +} + +body[data-module="propositions"] .nav-tabs .nav-link.active { + border-bottom: 2px solid var(--propositions-color); +} + +/* Devis Tab Style */ +body[data-module="devis"] .nav-tabs { + border-bottom-color: var(--devis-color); +} + +body[data-module="devis"] .nav-tabs .nav-link.active { + border-bottom: 2px solid var(--devis-color); +} + +/* Dark Mode Tabs */ +.dark-mode .nav-tabs .nav-link.active { + background-color: var(--card-bg); + border-color: var(--border-color) var(--border-color) var(--card-bg); + color: var(--primary-color); +} + +.dark-mode .nav-tabs .nav-link { + color: var(--text-color); +} + +.dark-mode .nav-tabs .nav-link:hover { + color: var(--primary-color); + background-color: rgba(255, 255, 255, 0.05); + border-color: var(--border-color); +} + +/* Spécificités mode sombre pour les onglets de modules */ +.dark-mode[data-module="crm"] .nav-tabs .nav-link.active { + color: var(--crm-light); + border-bottom: 2px solid var(--crm-light); +} + +.dark-mode[data-module="crm"] .nav-tabs .nav-link:hover { + color: var(--crm-light); +} + +.dark-mode[data-module="propositions"] .nav-tabs .nav-link.active { + color: var(--propositions-light); + border-bottom: 2px solid var(--propositions-light); +} + +.dark-mode[data-module="propositions"] .nav-tabs .nav-link:hover { + color: var(--propositions-light); +} + +.dark-mode[data-module="devis"] .nav-tabs .nav-link.active { + color: var(--devis-light); + border-bottom: 2px solid var(--devis-light); +} + +.dark-mode[data-module="devis"] .nav-tabs .nav-link:hover { + color: var(--devis-light); +} + +/* Couleurs spécifiques des modules */ +/* CRM */ +.navbar-module-crm { + background-color: var(--crm-color) !important; +} + +.navbar-module-crm .navbar-brand, +.navbar-module-crm .nav-link { + color: #ffffff !important; +} + +.btn-crm { + background-color: var(--crm-color); + border-color: var(--crm-color); + color: var(--btn-text-color); +} + +.btn-crm:hover, .btn-crm:focus { + background-color: var(--crm-hover); + border-color: var(--crm-hover); + color: var(--btn-text-color); +} + +.btn-outline-crm { + color: var(--crm-color); + border-color: var(--crm-color); +} + +.btn-outline-crm:hover, .btn-outline-crm:focus { + background-color: var(--crm-color); + color: var(--btn-text-color); +} + +.text-crm { + color: var(--crm-color) !important; +} + +.bg-crm { + background-color: var(--crm-color) !important; + color: var(--btn-text-color); +} + +.bg-crm-light { + background-color: var(--crm-light) !important; +} + +/* Propositions */ +.navbar-module-propositions { + background-color: var(--propositions-color) !important; +} + +.navbar-module-propositions .navbar-brand, +.navbar-module-propositions .nav-link { + color: #ffffff !important; +} + +.btn-propositions { + background-color: var(--propositions-color); + border-color: var(--propositions-color); + color: var(--btn-text-color); +} + +.btn-propositions:hover, .btn-propositions:focus { + background-color: var(--propositions-hover); + border-color: var(--propositions-hover); + color: var(--btn-text-color); +} + +.btn-outline-propositions { + color: var(--propositions-color); + border-color: var(--propositions-color); +} + +.btn-outline-propositions:hover, .btn-outline-propositions:focus { + background-color: var(--propositions-color); + color: var(--btn-text-color); +} + +.text-propositions { + color: var(--propositions-color) !important; +} + +.bg-propositions { + background-color: var(--propositions-color) !important; + color: var(--btn-text-color); +} + +.bg-propositions-light { + background-color: var(--propositions-light) !important; +} + +/* Devis */ +.navbar-module-devis { + background-color: var(--devis-color) !important; +} + +.navbar-module-devis .navbar-brand, +.navbar-module-devis .nav-link { + color: #ffffff !important; +} + +/* Projects */ +.navbar-module-project { + background-color: var(--project-color) !important; +} + +.navbar-module-project .navbar-brand, +.navbar-module-project .nav-link { + color: #ffffff !important; +} + +/* Emails */ +.navbar-module-email { + background-color: var(--email-color) !important; +} + +.navbar-module-email .navbar-brand, +.navbar-module-email .nav-link { + color: #ffffff !important; +} + +.btn-devis { + background-color: var(--devis-color); + border-color: var(--devis-color); + color: var(--btn-text-color); +} + +.btn-devis:hover, .btn-devis:focus { + background-color: var(--devis-hover); + border-color: var(--devis-hover); + color: var(--btn-text-color); +} + +.btn-outline-devis { + color: var(--devis-color); + border-color: var(--devis-color); +} + +.btn-outline-devis:hover, .btn-outline-devis:focus { + background-color: var(--devis-color); + color: var(--btn-text-color); +} + +.text-devis { + color: var(--devis-color) !important; +} + +.bg-devis { + background-color: var(--devis-color) !important; + color: var(--btn-text-color); +} + +.bg-devis-light { + background-color: var(--devis-light) !important; +} + +/* Forms */ +.form-control:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Module-specific form focus styles */ +body[data-module="crm"] .form-control:focus { + border-color: var(--crm-color); + box-shadow: 0 0 0 0.2rem rgba(106, 76, 147, 0.25); +} + +body[data-module="propositions"] .form-control:focus { + border-color: var(--propositions-color); + box-shadow: 0 0 0 0.2rem rgba(26, 147, 111, 0.25); +} + +body[data-module="devis"] .form-control:focus { + border-color: var(--devis-color); + box-shadow: 0 0 0 0.2rem rgba(231, 111, 81, 0.25); +} + +/* Dark mode form styles */ +.dark-mode .form-control:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); + background-color: #3e3e3e; +} + +.dark-mode[data-module="crm"] .form-control:focus { + border-color: var(--crm-color); + box-shadow: 0 0 0 0.2rem rgba(142, 114, 180, 0.25); + background-color: #3e3e3e; +} + +.dark-mode[data-module="propositions"] .form-control:focus { + border-color: var(--propositions-color); + box-shadow: 0 0 0 0.2rem rgba(60, 185, 145, 0.25); + background-color: #3e3e3e; +} + +.dark-mode[data-module="devis"] .form-control:focus { + border-color: var(--devis-color); + box-shadow: 0 0 0 0.2rem rgba(244, 132, 95, 0.25); + background-color: #3e3e3e; +} + +.dark-mode input::placeholder, +.dark-mode textarea::placeholder { + color: #adb5bd; + opacity: 0.7; +} + +.dark-mode .form-text { + color: #adb5bd; +} + +.dark-mode .form-label { + color: var(--text-color); +} + +/* Buttons */ +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: var(--btn-text-color); +} + +.btn-primary:hover { + background-color: var(--primary-hover); + border-color: var(--primary-hover); + color: var(--btn-text-color); +} + +.btn a, .btn-primary a, .btn-secondary a, .btn-success a, .btn-danger a, +.btn-warning a, .btn-info a, .btn-crm a, .btn-propositions a, .btn-devis a { + color: #ffffff !important; + text-decoration: none; +} + +.btn a:hover, .btn-primary a:hover, .btn-secondary a:hover, .btn-success a:hover, +.btn-danger a:hover, .btn-warning a:hover, .btn-info a:hover, +.btn-crm a:hover, .btn-propositions a:hover, .btn-devis a:hover { + color: #ffffff !important; + text-decoration: none; +} + +.dark-mode .btn-light { + background-color: #2c2c2c; + border-color: #2c2c2c; + color: var(--text-color); +} + +.dark-mode .btn-light:hover { + background-color: #3e3e3e; + border-color: #3e3e3e; + color: var(--text-color); +} + +.dark-mode .btn-outline-light { + color: #e9ecef; + border-color: #e9ecef; +} + +.dark-mode .btn-outline-light:hover { + background-color: #e9ecef; + color: #212529; +} + +.dark-mode .btn-link { + color: var(--primary-color); +} + +/* Sélecteur de mode sombre */ +#darkModeSwitch { + cursor: pointer; +} + +#darkModeLabel { + cursor: pointer; + transition: all 0.3s ease; + color: #ffffff; +} + +#darkModeSwitch:checked + #darkModeLabel .fa-sun { + color: #ffc107; +} + +.dark-mode #darkModeLabel .fa-moon { + color: #ffffff; +} + +/* Animations */ +.fade-in { + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Tableaux responsifs */ +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.dark-mode .table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +.dark-mode .table-hover tbody tr:hover { + background-color: rgba(255, 255, 255, 0.075); +} + +.dark-mode .table thead th { + border-bottom-color: var(--border-color); +} + +.dark-mode .table td, +.dark-mode .table th { + border-top-color: var(--border-color); +} + +/* Colors for links in tables and content */ +.table a, .card-body a:not(.btn) { + color: var(--primary-color); + text-decoration: none; +} + +.table a:hover, .card-body a:not(.btn):hover { + color: var(--primary-hover); + text-decoration: underline; +} + +/* Styles pour les liens selon le module actif */ +/* CRM */ +body[data-module="crm"] .table a, +body[data-module="crm"] .card-body a:not(.btn), +body[data-module="crm"] .nav-tabs .nav-link { + color: var(--crm-color); +} + +body[data-module="crm"] .table a:hover, +body[data-module="crm"] .card-body a:not(.btn):hover, +body[data-module="crm"] .nav-tabs .nav-link:hover { + color: var(--crm-hover); +} + +body[data-module="crm"] .nav-tabs .nav-link.active { + color: var(--crm-color); + border-color: #dee2e6 #dee2e6 #fff; + border-bottom-color: var(--crm-color); + font-weight: 600; + background-color: #fff; +} + +/* Propositions */ +body[data-module="propositions"] .table a, +body[data-module="propositions"] .card-body a:not(.btn), +body[data-module="propositions"] .nav-tabs .nav-link { + color: var(--propositions-color); +} + +body[data-module="propositions"] .table a:hover, +body[data-module="propositions"] .card-body a:not(.btn):hover, +body[data-module="propositions"] .nav-tabs .nav-link:hover { + color: var(--propositions-hover); +} + +body[data-module="propositions"] .nav-tabs .nav-link.active { + color: var(--propositions-color); + border-color: #dee2e6 #dee2e6 #fff; + border-bottom-color: var(--propositions-color); + font-weight: 600; + background-color: #fff; +} + +/* Devis */ +body[data-module="devis"] .table a, +body[data-module="devis"] .card-body a:not(.btn), +body[data-module="devis"] .nav-tabs .nav-link { + color: var(--devis-color); +} + +body[data-module="devis"] .table a:hover, +body[data-module="devis"] .card-body a:not(.btn):hover, +body[data-module="devis"] .nav-tabs .nav-link:hover { + color: var(--devis-hover); +} + +body[data-module="devis"] .nav-tabs .nav-link.active { + color: var(--devis-color); + border-color: #dee2e6 #dee2e6 #fff; + border-bottom-color: var(--devis-color); + font-weight: 600; + background-color: #fff; +} + +/* Mode sombre */ +.dark-mode .table a, .dark-mode .card-body a:not(.btn) { + color: #6ea8fe; +} + +.dark-mode .table a:hover, .dark-mode .card-body a:not(.btn):hover { + color: #9ec5fe; +} + +/* Adaptation pour le mode sombre par module */ +.dark-mode[data-module="crm"] .table a, +.dark-mode[data-module="crm"] .card-body a:not(.btn), +.dark-mode[data-module="crm"] .nav-tabs .nav-link { + color: var(--crm-light); +} + +.dark-mode[data-module="crm"] .nav-tabs .nav-link.active { + background-color: var(--card-bg); + border-color: var(--border-color) var(--border-color) var(--card-bg); +} + +.dark-mode[data-module="propositions"] .table a, +.dark-mode[data-module="propositions"] .card-body a:not(.btn), +.dark-mode[data-module="propositions"] .nav-tabs .nav-link { + color: var(--propositions-light); +} + +.dark-mode[data-module="propositions"] .nav-tabs .nav-link.active { + background-color: var(--card-bg); + border-color: var(--border-color) var(--border-color) var(--card-bg); +} + +.dark-mode[data-module="devis"] .table a, +.dark-mode[data-module="devis"] .card-body a:not(.btn), +.dark-mode[data-module="devis"] .nav-tabs .nav-link { + color: var(--devis-light); +} + +.dark-mode[data-module="devis"] .nav-tabs .nav-link.active { + background-color: var(--card-bg); + border-color: var(--border-color) var(--border-color) var(--card-bg); +} + +/* Footer */ +.footer { + margin-top: 2rem; + background-color: var(--light-color); + color: var(--secondary-color); + padding: 1.5rem 0; + transition: background-color 0.5s ease, color 0.5s ease; +} + +.footer a { + color: var(--primary-color); + text-decoration: none; +} + +.footer a:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +/* Module-specific Footer */ +body[data-module="crm"] .footer a { + color: var(--crm-color); +} + +body[data-module="crm"] .footer a:hover { + color: var(--crm-hover); +} + +body[data-module="propositions"] .footer a { + color: var(--propositions-color); +} + +body[data-module="propositions"] .footer a:hover { + color: var(--propositions-hover); +} + +body[data-module="devis"] .footer a { + color: var(--devis-color); +} + +body[data-module="devis"] .footer a:hover { + color: var(--devis-hover); +} + +.dark-mode .footer { + background-color: #343a40 !important; +} + +.dark-mode .footer .text-muted { + color: #adb5bd !important; +} + +.dark-mode .footer a { + color: #6ea8fe; +} + +.dark-mode .footer a:hover { + color: #9ec5fe; +} + +.dark-mode[data-module="crm"] .footer a { + color: var(--crm-light); +} + +.dark-mode[data-module="propositions"] .footer a { + color: var(--propositions-light); +} + +.dark-mode[data-module="devis"] .footer a { + color: var(--devis-light); +} + +/* Cartes d'accueil */ +.card .fas, .card .far { + color: var(--primary-color); +} + +/* Infobulle personnalisée */ +.custom-tooltip { + position: relative; + display: inline-block; +} + +.custom-tooltip .tooltip-text { + visibility: hidden; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -60px; + opacity: 0; + transition: opacity 0.3s; +} + +.custom-tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* Titres et en-têtes */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + margin-bottom: 1rem; +} + +/* Styles de titres spécifiques aux modules */ +body[data-module="crm"] h1, +body[data-module="crm"] .card-header { + border-left: 4px solid var(--crm-color); + padding-left: 10px; +} + +body[data-module="propositions"] h1, +body[data-module="propositions"] .card-header { + border-left: 4px solid var(--propositions-color); + padding-left: 10px; +} + +body[data-module="devis"] h1, +body[data-module="devis"] .card-header { + border-left: 4px solid var(--devis-color); + padding-left: 10px; +} + +/* Page d'accueil */ +.jumbotron { + background-color: var(--card-bg); + border-radius: 0.3rem; + padding: 2rem 1rem; + margin-bottom: 2rem; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} diff --git a/static/css/tasks.css b/static/css/tasks.css new file mode 100644 index 0000000..7aee041 --- /dev/null +++ b/static/css/tasks.css @@ -0,0 +1,18 @@ +/* Styles additionnels pour les blocs de tâches */ +.tasks-card { + border: 1px solid #e5e5e5; + border-radius: .5rem; + padding: 1rem; +} + +.tasks-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: .6rem; +} + +#tasksTodayContent .placeholder { + color: #888; + font-size: .95rem; +} diff --git a/static/img/logo_dark.png b/static/img/logo_dark.png new file mode 100644 index 0000000..e5d3572 Binary files /dev/null and b/static/img/logo_dark.png differ diff --git a/static/img/logo_light.png b/static/img/logo_light.png new file mode 100644 index 0000000..d9e00f8 Binary files /dev/null and b/static/img/logo_light.png differ diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..53e70f6 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,190 @@ +// Suite Consultance - Script principal + +document.addEventListener('DOMContentLoaded', function() { + // Gestion du mode sombre + const darkModeSwitch = document.getElementById('darkModeSwitch'); + if (darkModeSwitch) { + // Initialisation du mode sombre selon la préférence enregistrée + if (localStorage.getItem('darkMode') === 'enabled') { + document.body.classList.add('dark-mode'); + darkModeSwitch.checked = true; + updateDarkModeIcon(true); + } else { + updateDarkModeIcon(false); + } + + // Écouteur d'événement pour le changement de mode + darkModeSwitch.addEventListener('change', function() { + const isDarkMode = this.checked; + document.body.classList.toggle('dark-mode', isDarkMode); + localStorage.setItem('darkMode', isDarkMode ? 'enabled' : 'disabled'); + updateDarkModeIcon(isDarkMode); + + // Mettre à jour un attribut de données pour que d'autres scripts puissent détecter le mode + document.body.setAttribute('data-theme', isDarkMode ? 'dark' : 'light'); + + // Déclencher un événement personnalisé pour informer d'autres scripts du changement + const event = new CustomEvent('themeChanged', { detail: { darkMode: isDarkMode } }); + document.dispatchEvent(event); + }); + } + + // Gestion des couleurs de modules + setModuleColors(); + + // Mise à jour de l'icône du mode sombre + function updateDarkModeIcon(isDarkMode) { + const darkModeLabel = document.querySelector('label[for="darkModeSwitch"]'); + if (darkModeLabel) { + darkModeLabel.innerHTML = isDarkMode ? + '' : + ''; + } + } + + // Initialisation des tooltips Bootstrap + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + + // Initialisation des popovers Bootstrap + var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + + // Fermeture automatique des alertes après 5 secondes + setTimeout(function() { + var alerts = document.querySelectorAll('.alert'); + alerts.forEach(function(alert) { + var bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + }); + }, 5000); + + // Activation des onglets via URL hash + if (window.location.hash) { + const hash = window.location.hash.substring(1); + const tabEl = document.querySelector(`#${hash}-tab`); + if (tabEl) { + const tab = new bootstrap.Tab(tabEl); + tab.show(); + } + } + + // Navigation entre les onglets avec historique + document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tabEl => { + tabEl.addEventListener('shown.bs.tab', function(event) { + const id = event.target.getAttribute('data-bs-target').substring(1).replace('-tab-pane', ''); + history.replaceState(null, null, `#${id}`); + }); + }); + + // Gestion de la recherche dynamique dans les tableaux + document.querySelectorAll('.search-input').forEach(input => { + input.addEventListener('keyup', function() { + const searchTerm = this.value.toLowerCase(); + const tableId = this.getAttribute('data-table'); + const table = document.getElementById(tableId); + + if (table) { + const rows = table.querySelectorAll('tbody tr'); + + rows.forEach(row => { + const text = row.textContent.toLowerCase(); + if (text.indexOf(searchTerm) > -1) { + row.style.display = ""; + } else { + row.style.display = "none"; + } + }); + } + }); + }); + + // Confirmation pour les actions de suppression + document.querySelectorAll('.delete-confirm').forEach(btn => { + btn.addEventListener('click', function(e) { + if (!confirm('Êtes-vous sûr de vouloir supprimer cet élément ? Cette action est irréversible.')) { + e.preventDefault(); + } + }); + }); + + // Sélection automatique d'un client depuis l'URL + const urlParams = new URLSearchParams(window.location.search); + const clientId = urlParams.get('client_id'); + if (clientId) { + const clientSelect = document.getElementById('client_select'); + if (clientSelect) { + // Sélectionner le client dans la liste déroulante + Array.from(clientSelect.options).forEach(option => { + if (option.value === clientId) { + option.selected = true; + + // Déclencher un événement change + const event = new Event('change'); + clientSelect.dispatchEvent(event); + } + }); + } + } + + // Fonction pour définir les couleurs des modules en fonction de la page active + function setModuleColors() { + const navbar = document.querySelector('.navbar'); + const currentPath = window.location.pathname; + + // Récupérer le module défini dans le HTML via l'attribut data-module + const moduleFromBody = document.body.getAttribute('data-module'); + let activeModule = 'default'; + + // Si le module est défini dans le corps, l'utiliser + if (moduleFromBody && moduleFromBody !== 'default') { + activeModule = moduleFromBody; + } + // Sinon, l'inférer à partir du chemin + else if (currentPath.startsWith('/crm')) { + activeModule = 'crm'; + } else if (currentPath.startsWith('/propositions')) { + activeModule = 'propositions'; + } else if (currentPath.startsWith('/devis')) { + activeModule = 'devis'; + } + + // Mettre à jour l'attribut data-module si nécessaire + if (document.body.getAttribute('data-module') !== activeModule) { + document.body.setAttribute('data-module', activeModule); + } + + // Réinitialiser toutes les classes de modules + navbar.classList.remove('navbar-module-crm', 'navbar-module-propositions', 'navbar-module-devis'); + + // Ajouter les classes en fonction du module actif + if (activeModule === 'crm') { + navbar.classList.add('navbar-module-crm'); + updateButtonsForModule('crm'); + } else if (activeModule === 'propositions') { + navbar.classList.add('navbar-module-propositions'); + updateButtonsForModule('propositions'); + } else if (activeModule === 'devis') { + navbar.classList.add('navbar-module-devis'); + updateButtonsForModule('devis'); + } + } + + // Mise à jour des boutons pour le module actif + function updateButtonsForModule(moduleName) { + // Remplacer les classes des boutons primaires par les classes du module + document.querySelectorAll('.btn-primary').forEach(btn => { + btn.classList.remove('btn-primary'); + btn.classList.add(`btn-${moduleName}`); + }); + + document.querySelectorAll('.btn-outline-primary').forEach(btn => { + btn.classList.remove('btn-outline-primary'); + btn.classList.add(`btn-outline-${moduleName}`); + }); + } +}); diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..0570387 --- /dev/null +++ b/test_app.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +from app import app + +if __name__ == "__main__": + # Test des routes + with app.test_client() as client: + # Test route principale + response = client.get('/') + if response.status_code == 200: + print("✅ Route '/' fonctionnelle") + else: + print(f"❌ Route '/' erreur: {response.status_code}") + + # Test route CRM + response = client.get('/crm') + if response.status_code == 200: + print("✅ Route '/crm' fonctionnelle") + else: + print(f"❌ Route '/crm' erreur: {response.status_code}") + + # Test route formulaire d'ajout prospect + response = client.get('/prospect/add') + if response.status_code == 200: + print("✅ Route '/prospect/add' fonctionnelle") + else: + print(f"❌ Route '/prospect/add' erreur: {response.status_code}") + + # Test routes formulaires des propositions + response = client.get('/propositions') + if response.status_code == 200: + print("✅ Route '/propositions' fonctionnelle") + else: + print(f"❌ Route '/propositions' erreur: {response.status_code}") + + # Test routes formulaires des devis + response = client.get('/devis') + if response.status_code == 200: + print("✅ Route '/devis' fonctionnelle") + else: + print(f"❌ Route '/devis' erreur: {response.status_code}") + + # Lancer le serveur si tous les tests sont OK + print("\nDémarrage du serveur Flask...") + app.run(debug=True, host='127.0.0.1', port=5000) diff --git a/test_mail.py b/test_mail.py new file mode 100644 index 0000000..0320498 --- /dev/null +++ b/test_mail.py @@ -0,0 +1,6 @@ +import smtplib + +server = smtplib.SMTP("smtp.gmail.com", 587) +server.starttls() +server.login("violet.anthony90@gmail.com", "gfmo dsys ruau nlip") # mot de passe d'application sans espace +print("Connexion OK") \ No newline at end of file diff --git a/test_proposition_form.py b/test_proposition_form.py new file mode 100644 index 0000000..6498711 --- /dev/null +++ b/test_proposition_form.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +from app import app + +def test_create_proposition_form(): + with app.test_client() as client: + # Test GET requête - pour afficher le formulaire + response = client.get('/proposition/create') + assert response.status_code == 200 + assert b'Nom du client' in response.data + assert b'Nom du projet' in response.data + assert b'Features' in response.data + + # Test POST requête - soumission de formulaire + test_data = { + 'client_name': 'Client Test', + 'email': 'test@example.com', + 'telephone': '0123456789', + 'adresse': '123 Rue Test', + 'project_name': 'Projet Test', + 'project_type': 'Site Web', + 'deadline': '2025-06-01', + 'project_description': 'Description détaillée du projet test.', + 'features': 'Feature 1, Feature 2, Feature 3', + 'budget': '10000', + 'payment_terms': '30% à la commande, 70% à la livraison', + 'contact_info': 'Contact Test', + 'additional_info': 'Informations supplémentaires' + } + + response = client.post('/proposition/create', data=test_data, follow_redirects=True) + assert response.status_code == 200 + assert b'success' in response.data + + # Test validation des champs requis + test_data_missing = { + 'client_name': '', # Champ obligatoire manquant + 'project_name': 'Projet Test', + } + response = client.post('/proposition/create', data=test_data_missing, follow_redirects=True) + assert response.status_code == 200 + assert b'error' in response.data + + # Test avec tous les champs vides + test_data_empty = {} + response = client.post('/proposition/create', data=test_data_empty, follow_redirects=True) + assert response.status_code == 200 + assert b'error' in response.data + +if __name__ == "__main__": + print("Début des tests de formulaire de création de proposition...") + test_create_proposition_form() + print("Tests terminés avec succès !")