Initial commit: existing turf_saas codebase

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
ML Engineer
2026-04-25 17:18:43 +02:00
commit ed07c8a3d1
137 changed files with 36398 additions and 0 deletions

395
crm_candidatures.html Executable file
View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>CRM Candidatures</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
header { background: linear-gradient(90deg, #e94560, #7b2cbf); padding: 20px; text-align: center; margin: -20px -20px 20px; border-radius: 0 0 20px 20px; }
h1 { font-size: 28px; margin-bottom: 10px; }
.subtitle { color: #aaa; font-size: 14px; }
.stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 20px; }
.stat-card { background: #16213e; padding: 15px; border-radius: 8px; text-align: center; }
.stat-num { font-size: 24px; color: #00d9ff; font-weight: bold; }
.stat-label { color: #aaa; font-size: 12px; }
.kanban { display: grid; grid-template-columns: repeat(6, 1fr); gap: 15px; overflow-x: auto; }
.column { background: #16213e; border-radius: 12px; padding: 15px; min-width: 250px; }
.column-header { font-size: 14px; font-weight: bold; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid; }
.column-a_postuler { border-color: #ffd700; }
.column-en_attente { border-color: #ff9f43; }
.column-entretien_1 { border-color: #00d9ff; }
.column-entretien_2 { border-color: #7b2cbf; }
.column-offre { border-color: #00ff88; }
.column-refus { border-color: #e94560; }
.card { background: #0f3460; padding: 12px; border-radius: 8px; margin-bottom: 10px; cursor: pointer; }
.card:hover { background: #1a4a7a; }
.card-entreprise { font-weight: bold; color: #fff; font-size: 14px; }
.card-poste { color: #00d9ff; font-size: 12px; margin: 5px 0; }
.card-date { color: #888; font-size: 11px; }
.card-rappel { background: #e94560; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-top: 5px; display: inline-block; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; }
.modal.show { display: flex; justify-content: center; align-items: center; }
.modal-content { background: #16213e; padding: 30px; border-radius: 15px; width: 600px; max-width: 90%; max-height: 80vh; overflow-y: auto; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; color: #aaa; margin-bottom: 5px; font-size: 12px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px; background: #0f3460; border: 1px solid #333; border-radius: 8px; color: #fff; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
.btn-primary { background: #00d9ff; color: #000; }
.btn-secondary { background: #666; color: #fff; }
.btn-danger { background: #e94560; color: #fff; }
.contact-history { margin-top: 20px; padding-top: 20px; border-top: 1px solid #333; }
.contact-item { background: #0f3460; padding: 10px; border-radius: 8px; margin-bottom: 10px; }
.contact-date { color: #00d9ff; font-size: 11px; }
.contact-type { color: #aaa; font-size: 11px; }
.add-form { background: #16213e; padding: 20px; border-radius: 12px; margin-bottom: 20px; }
.form-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.home-btn{position:fixed;top:10px;left:10px;z-index:9999;background:linear-gradient(135deg,#00d9ff,#7b2cbf);color:#fff;border:none;border-radius:8px;padding:10px 15px;font-size:14px;cursor:pointer;text-decoration:none;display:flex;align-items:center;gap:8px;box-shadow:0 2px 10px rgba(0,217,255,0.3);transition:all 0.3s;font-family:-apple-system,BlinkMacSystemFont,sans-serif}.home-btn:hover{transform:translateY(-2px);box-shadow:0 4px 15px rgba(0,217,255,0.5)}
</style>
</head>
<body>
<a href="https://portal-kolifee.duckdns.org/" class="home-btn" title="Retour au portail"><span style="font-size:18px">🏠</span><span>Accueil</span></a>
<header>
<h1>🎯 CRM Candidatures</h1>
<div class="subtitle">Suivi de recherche d'emploi - Pipeline</div>
</header>
<div class="stats" id="stats">
<div class="stat-card"><div class="stat-num" id="total">0</div><div class="stat-label">Total</div></div>
<div class="stat-card"><div class="stat-num" id="a_postuler">0</div><div class="stat-label">À Postuler</div></div>
<div class="stat-card"><div class="stat-num" id="en_attente">0</div><div class="stat-label">En Attente</div></div>
<div class="stat-card"><div class="stat-num" id="entretien">0</div><div class="stat-label">Entretiens</div></div>
<div class="stat-card"><div class="stat-num" id="offres">0</div><div class="stat-label">Offres</div></div>
<div class="stat-card"><div class="stat-num" id="refus">0</div><div class="stat-label">Refus</div></div>
</div>
<div class="add-form">
<h2> Nouvelle Candidature</h2>
<div class="form-row">
<input type="text" id="entreprise" placeholder="Entreprise *">
<input type="text" id="poste" placeholder="Poste Recherché *">
<input type="text" id="source" placeholder="Source (LinkedIn, Indeed...)">
</div>
<div class="form-row" style="margin-top:10px">
<input type="url" id="url" placeholder="URL offre">
<input type="number" id="salaire" placeholder="Salaire (€)">
<input type="date" id="date_publication"> Date publication
</div>
<button class="btn btn-primary" style="margin-top:15px" onclick="addCandidature()">Ajouter</button>
</div>
<div class="kanban">
<div class="column column-a_postuler">
<div class="column-header">📝 À Postuler</div>
<div id="a_postuler-list"></div>
</div>
<div class="column column-en_attente">
<div class="column-header">⏳ En Attente</div>
<div id="en_attente-list"></div>
</div>
<div class="column column-entretien_1">
<div class="column-header">🎤 Entretien 1</div>
<div id="entretien_1-list"></div>
</div>
<div class="column column-entretien_2">
<div class="column-header">🎓 Entretien 2</div>
<div id="entretien_2-list"></div>
</div>
<div class="column column-offre">
<div class="column-header">✅ Offre</div>
<div id="offre-list"></div>
</div>
<div class="column column-refus">
<div class="column-header">❌ Refus</div>
<div id="refus-list"></div>
</div>
</div>
<div class="modal" id="modal">
<div class="modal-content">
<h2 id="modal-title">Modifier Candidature</h2>
<input type="hidden" id="cand-id">
<div class="form-group">
<label>Entreprise</label>
<input type="text" id="edit-entreprise">
</div>
<div class="form-group">
<label>Poste</label>
<input type="text" id="edit-poste">
</div>
<div class="form-group">
<label>URL Offre</label>
<input type="url" id="edit-url">
</div>
<div class="form-group">
<label>Salaire</label>
<input type="number" id="edit-salaire">
</div>
<div class="form-group">
<label>Étape Pipeline</label>
<select id="edit-etape">
<option value="a_postuler">À Postuler</option>
<option value="en_attente">En Attente</option>
<option value="entretien_1">Entretien 1</option>
<option value="entretien_2">Entretien 2</option>
<option value="offre">Offre</option>
<option value="refus">Refus</option>
</select>
</div>
<div class="form-group">
<label>Prochain Rappel</label>
<input type="date" id="edit-rappel">
</div>
<div class="form-group">
<label>Notes</label>
<textarea id="edit-notes" rows="3"></textarea>
</div>
<div class="contact-history">
<h3>📞 Historique Contacts</h3>
<div id="contacts-list"></div>
<div class="form-group" style="margin-top:15px">
<input type="text" id="new-contact-type" placeholder="Type (appel, email, rdv)" style="width:30%">
<input type="text" id="new-contact-interlocuteur" placeholder="Interlocuteur" style="width:30%">
<input type="date" id="new-contact-date" style="width:30%">
<textarea id="new-contact-notes" placeholder="Notes contact" rows="2" style="margin-top:5px"></textarea>
<button class="btn btn-primary" style="margin-top:5px" onclick="addContact()">Ajouter Contact</button>
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px">
<button class="btn btn-primary" onclick="saveCandidature()">Enregistrer</button>
<button class="btn btn-secondary" onclick="closeModal()">Annuler</button>
<button class="btn btn-danger" onclick="deleteCandidature()">Supprimer</button>
</div>
</div>
</div>
<script>
let candidatures = [];
// Load from localStorage or API
function load() {
const stored = localStorage.getItem('candidatures');
if (stored) {
candidatures = JSON.parse(stored);
} else {
// Fetch from API and transform
fetch('api/prospects')
.then(r => r.json())
.then(data => {
// Transform prospects to candidatures
candidatures = data.prospects.map(p => ({
id: p.id,
entreprise: {
nom: p.nom,
adresse: p.adresse,
tel: p.tel,
email: p.email,
categorie: p.categorie
},
poste: {
titre: '',
url: '',
salaire: null,
source: '',
localisation: p.adresse
},
pipeline: {
etape: 'a_postuler',
date_changement: new Date().toISOString().split('T')[0],
historique: []
},
suivi: {
dernier_contact: null,
prochain_rappel: null,
contacts: [],
notes: p.notes || '',
score_confiance: p.score || 0
},
created: p.created,
updated: new Date().toISOString().split('T')[0]
}));
save();
});
}
render();
}
function save() {
localStorage.setItem('candidatures', JSON.stringify(candidatures));
render();
}
function render() {
// Clear columns
['a_postuler', 'en_attente', 'entretien_1', 'entretien_2', 'offre', 'refus'].forEach(etape => {
document.getElementById(etape + '-list').innerHTML = '';
});
// Stats
const stats = { a_postuler: 0, en_attente: 0, entretien_1: 0, entretien_2: 0, offre: 0, refus: 0, total: candidatures.length };
// Render cards
candidatures.forEach((c, i) => {
const etape = c.pipeline.etape;
stats[etape] = (stats[etape] || 0) + 1;
const card = document.createElement('div');
card.className = 'card';
card.onclick = () => openEdit(i);
const today = new Date().toISOString().split('T')[0];
let rappelHtml = '';
if (c.suivi.prochain_rappel && c.suivi.prochain_rappel <= today) {
rappelHtml = '<span class="card-rappel">⚠️ Rappel!</span>';
}
card.innerHTML = `
<div class="card-entreprise">${c.entreprise.nom || '-'}</div>
<div class="card-poste">${c.poste.titre || 'Poste à définir'}</div>
${c.poste.salaire ? '<div class="card-date">💰 ' + c.poste.salaire + '€</div>' : ''}
${rappelHtml}
<div class="card-date">📅 ${c.pipeline.date_changement}</div>
`;
document.getElementById(etape + '-list').appendChild(card);
});
// Update stats
document.getElementById('total').textContent = stats.total;
document.getElementById('a_postuler').textContent = stats.a_postuler;
document.getElementById('en_attente').textContent = stats.en_attente;
document.getElementById('entretien').textContent = (stats.entretien_1 || 0) + (stats.entretien_2 || 0);
document.getElementById('offres').textContent = stats.offre;
document.getElementById('refus').textContent = stats.refus;
}
function addCandidature() {
const entreprise = document.getElementById('entreprise').value;
const poste = document.getElementById('poste').value;
if (!entreprise || !poste) return alert('Entreprise et Poste requis!');
const cand = {
id: 'cand_' + Date.now(),
entreprise: { nom: entreprise },
poste: {
titre: poste,
url: document.getElementById('url').value,
salaire: document.getElementById('salaire').value,
source: document.getElementById('source').value,
date_publication: document.getElementById('date_publication').value
},
pipeline: { etape: 'a_postuler', date_changement: new Date().toISOString().split('T')[0], historique: [] },
suivi: { contacts: [], notes: '' },
created: new Date().toISOString().split('T')[0],
updated: new Date().toISOString().split('T')[0]
};
candidatures.push(cand);
save();
// Clear form
document.getElementById('entreprise').value = '';
document.getElementById('poste').value = '';
document.getElementById('url').value = '';
document.getElementById('salaire').value = '';
}
let currentIdx = null;
function openEdit(idx) {
currentIdx = idx;
const c = candidatures[idx];
document.getElementById('modal-title').textContent = 'Modifier: ' + (c.entreprise.nom || '');
document.getElementById('cand-id').value = c.id;
document.getElementById('edit-entreprise').value = c.entreprise.nom || '';
document.getElementById('edit-poste').value = c.poste.titre || '';
document.getElementById('edit-url').value = c.poste.url || '';
document.getElementById('edit-salaire').value = c.poste.salaire || '';
document.getElementById('edit-etape').value = c.pipeline.etape;
document.getElementById('edit-rappel').value = c.suivi.prochain_rappel || '';
document.getElementById('edit-notes').value = c.suivi.notes || '';
renderContacts(c);
document.getElementById('modal').classList.add('show');
}
function renderContacts(c) {
const list = document.getElementById('contacts-list');
list.innerHTML = (c.suivi.contacts || []).map(contact => `
<div class="contact-item">
<div class="contact-date">${contact.date} - <span class="contact-type">${contact.type}</span></div>
<div>${contact.interlocuteur || ''}</div>
<div style="color:#888;font-size:11px">${contact.notes || ''}</div>
</div>
`).join('');
}
function addContact() {
const c = candidatures[currentIdx];
if (!c.suivi.contacts) c.suivi.contacts = [];
c.suivi.contacts.push({
type: document.getElementById('new-contact-type').value || 'appel',
interlocuteur: document.getElementById('new-contact-interlocuteur').value,
date: document.getElementById('new-contact-date').value || new Date().toISOString().split('T')[0],
notes: document.getElementById('new-contact-notes').value
});
c.suivi.dernier_contact = new Date().toISOString().split('T')[0];
save();
renderContacts(c);
}
function saveCandidature() {
const c = candidatures[currentIdx];
c.entreprise.nom = document.getElementById('edit-entreprise').value;
c.poste.titre = document.getElementById('edit-poste').value;
c.poste.url = document.getElementById('edit-url').value;
c.poste.salaire = document.getElementById('edit-salaire').value;
c.suivi.notes = document.getElementById('edit-notes').value;
c.suivi.prochain_rappel = document.getElementById('edit-rappel').value;
const newEtape = document.getElementById('edit-etape').value;
if (newEtape !== c.pipeline.etape) {
c.pipeline.historique.push({ etape: c.pipeline.etape, date: c.pipeline.date_changement });
c.pipeline.etape = newEtape;
c.pipeline.date_changement = new Date().toISOString().split('T')[0];
}
c.updated = new Date().toISOString().split('T')[0];
save();
closeModal();
}
function deleteCandidature() {
if (!confirm('Supprimer cette candidature?')) return;
candidatures.splice(currentIdx, 1);
save();
closeModal();
}
function closeModal() {
document.getElementById('modal').classList.remove('show');
currentIdx = null;
}
// Initialize
load();
</script>
</body>
</html>