Initial commit: existing turf_saas codebase
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
395
crm_candidatures.html
Executable file
395
crm_candidatures.html
Executable 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>
|
||||
Reference in New Issue
Block a user