Add leads (liidit) tab for tracking potential customers
- New Liidit tab with table, search, add/edit/delete - Lead fields: company, contact, phone, email, address, city, status, notes - Status workflow: Uusi → Kontaktoitu → Kiinnostunut → Odottaa toimitusta - Color-coded status badges - Detail view with notes display - "Muuta asiakkaaksi" converts lead to customer with pre-filled data - Lead CRUD endpoints in api.php with changelog logging - leads.json added to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ data/customers.json
|
|||||||
data/users.json
|
data/users.json
|
||||||
data/changelog.json
|
data/changelog.json
|
||||||
data/archive.json
|
data/archive.json
|
||||||
|
data/leads.json
|
||||||
data/reset_tokens.json
|
data/reset_tokens.json
|
||||||
data/login_attempts.json
|
data/login_attempts.json
|
||||||
data/backups/
|
data/backups/
|
||||||
|
|||||||
128
api.php
128
api.php
@@ -14,6 +14,7 @@ define('DATA_FILE', DATA_DIR . '/customers.json');
|
|||||||
define('USERS_FILE', DATA_DIR . '/users.json');
|
define('USERS_FILE', DATA_DIR . '/users.json');
|
||||||
define('CHANGELOG_FILE', DATA_DIR . '/changelog.json');
|
define('CHANGELOG_FILE', DATA_DIR . '/changelog.json');
|
||||||
define('ARCHIVE_FILE', DATA_DIR . '/archive.json');
|
define('ARCHIVE_FILE', DATA_DIR . '/archive.json');
|
||||||
|
define('LEADS_FILE', DATA_DIR . '/leads.json');
|
||||||
define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
|
define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
|
||||||
define('RATE_FILE', DATA_DIR . '/login_attempts.json');
|
define('RATE_FILE', DATA_DIR . '/login_attempts.json');
|
||||||
define('SITE_URL', 'https://intra.cuitunet.fi');
|
define('SITE_URL', 'https://intra.cuitunet.fi');
|
||||||
@@ -24,7 +25,7 @@ define('MAIL_FROM_NAME', 'CuituNet Intra');
|
|||||||
|
|
||||||
// Varmista data-kansio ja tiedostot
|
// Varmista data-kansio ja tiedostot
|
||||||
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
||||||
foreach ([DATA_FILE, USERS_FILE, CHANGELOG_FILE, ARCHIVE_FILE, TOKENS_FILE, RATE_FILE] as $f) {
|
foreach ([DATA_FILE, USERS_FILE, CHANGELOG_FILE, ARCHIVE_FILE, LEADS_FILE, TOKENS_FILE, RATE_FILE] as $f) {
|
||||||
if (!file_exists($f)) file_put_contents($f, '[]');
|
if (!file_exists($f)) file_put_contents($f, '[]');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,6 +661,131 @@ switch ($action) {
|
|||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ---------- LEADS ----------
|
||||||
|
case 'leads':
|
||||||
|
requireAuth();
|
||||||
|
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
|
||||||
|
echo json_encode($leads);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lead_create':
|
||||||
|
requireAuth();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$lead = [
|
||||||
|
'id' => generateId(),
|
||||||
|
'yritys' => trim($input['yritys'] ?? ''),
|
||||||
|
'yhteyshenkilo' => trim($input['yhteyshenkilo'] ?? ''),
|
||||||
|
'puhelin' => trim($input['puhelin'] ?? ''),
|
||||||
|
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
|
||||||
|
'osoite' => trim($input['osoite'] ?? ''),
|
||||||
|
'kaupunki' => trim($input['kaupunki'] ?? ''),
|
||||||
|
'tila' => trim($input['tila'] ?? 'uusi'),
|
||||||
|
'muistiinpanot' => trim($input['muistiinpanot'] ?? ''),
|
||||||
|
'luotu' => date('Y-m-d H:i:s'),
|
||||||
|
'luoja' => currentUser(),
|
||||||
|
];
|
||||||
|
if (empty($lead['yritys'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
|
||||||
|
$leads[] = $lead;
|
||||||
|
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
addLog('lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
|
||||||
|
echo json_encode($lead);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lead_update':
|
||||||
|
requireAuth();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
|
||||||
|
$found = false;
|
||||||
|
foreach ($leads as &$l) {
|
||||||
|
if ($l['id'] === $id) {
|
||||||
|
$fields = ['yritys','yhteyshenkilo','puhelin','sahkoposti','osoite','kaupunki','tila','muistiinpanot'];
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
if (isset($input[$f])) $l[$f] = trim($input[$f]);
|
||||||
|
}
|
||||||
|
$l['muokattu'] = date('Y-m-d H:i:s');
|
||||||
|
$l['muokkaaja'] = currentUser();
|
||||||
|
$found = true;
|
||||||
|
addLog('lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
|
||||||
|
echo json_encode($l);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($l);
|
||||||
|
if (!$found) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Liidiä ei löydy']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lead_delete':
|
||||||
|
requireAuth();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
|
||||||
|
$deleted = null;
|
||||||
|
foreach ($leads as $l) {
|
||||||
|
if ($l['id'] === $id) { $deleted = $l; break; }
|
||||||
|
}
|
||||||
|
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
|
||||||
|
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
if ($deleted) addLog('lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lead_to_customer':
|
||||||
|
requireAuth();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
|
||||||
|
$lead = null;
|
||||||
|
foreach ($leads as $l) {
|
||||||
|
if ($l['id'] === $id) { $lead = $l; break; }
|
||||||
|
}
|
||||||
|
if (!$lead) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Liidiä ei löydy']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Luo asiakas liidistä
|
||||||
|
$customer = [
|
||||||
|
'id' => generateId(),
|
||||||
|
'yritys' => $lead['yritys'],
|
||||||
|
'yhteyshenkilö' => $lead['yhteyshenkilo'] ?? '',
|
||||||
|
'puhelin' => $lead['puhelin'] ?? '',
|
||||||
|
'sahkoposti' => $lead['sahkoposti'] ?? '',
|
||||||
|
'laskutusosoite' => '',
|
||||||
|
'laskutuspostinumero' => '',
|
||||||
|
'laskutuskaupunki' => '',
|
||||||
|
'laskutussahkoposti' => '',
|
||||||
|
'elaskuosoite' => '',
|
||||||
|
'elaskuvalittaja' => '',
|
||||||
|
'ytunnus' => '',
|
||||||
|
'lisatiedot' => $lead['muistiinpanot'] ?? '',
|
||||||
|
'liittymat' => [['asennusosoite' => $lead['osoite'] ?? '', 'postinumero' => '', 'kaupunki' => $lead['kaupunki'] ?? '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '']],
|
||||||
|
'luotu' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
$customers = loadCustomers();
|
||||||
|
$customers[] = $customer;
|
||||||
|
saveCustomers($customers);
|
||||||
|
// Poista liidi
|
||||||
|
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
|
||||||
|
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
addLog('lead_to_customer', $customer['id'], $customer['yritys'], 'Muutti liidin asiakkaaksi');
|
||||||
|
echo json_encode($customer);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---------- FILES ----------
|
// ---------- FILES ----------
|
||||||
case 'file_upload':
|
case 'file_upload':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
|
|||||||
111
index.html
111
index.html
@@ -72,6 +72,7 @@
|
|||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<button class="tab active" data-tab="customers">Asiakkaat</button>
|
<button class="tab active" data-tab="customers">Asiakkaat</button>
|
||||||
|
<button class="tab" data-tab="leads">Liidit</button>
|
||||||
<button class="tab" data-tab="archive">Arkisto</button>
|
<button class="tab" data-tab="archive">Arkisto</button>
|
||||||
<button class="tab" data-tab="changelog">Muutosloki</button>
|
<button class="tab" data-tab="changelog">Muutosloki</button>
|
||||||
<button class="tab" data-tab="users" id="tab-users" style="display:none">Käyttäjät</button>
|
<button class="tab" data-tab="users" id="tab-users" style="display:none">Käyttäjät</button>
|
||||||
@@ -149,6 +150,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Liidit -->
|
||||||
|
<div class="tab-content" id="tab-content-leads">
|
||||||
|
<div class="main-container">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||||
|
<button class="btn-primary" id="btn-add-lead">+ Lisää liidi</button>
|
||||||
|
<div class="search-bar" style="flex:1;max-width:400px;margin-left:1rem;">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input type="text" id="lead-search-input" placeholder="Hae liidejä...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-card">
|
||||||
|
<table id="leads-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Yritys</th>
|
||||||
|
<th>Yhteyshenkilö</th>
|
||||||
|
<th>Kaupunki</th>
|
||||||
|
<th>Tila</th>
|
||||||
|
<th>Lisätty</th>
|
||||||
|
<th>Toiminnot</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="leads-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="no-leads" class="empty-state" style="display:none">
|
||||||
|
<div class="empty-icon">🎯</div>
|
||||||
|
<p>Ei liidejä vielä.</p>
|
||||||
|
<p class="empty-hint">Klikkaa "+ Lisää liidi" lisätäksesi ensimmäisen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-bar">
|
||||||
|
<span id="lead-count">0 liidiä</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Arkisto -->
|
<!-- Tab: Arkisto -->
|
||||||
<div class="tab-content" id="tab-content-archive">
|
<div class="tab-content" id="tab-content-archive">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
@@ -370,6 +407,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Liidi-modal -->
|
||||||
|
<div id="lead-modal" class="modal" style="display:none">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="lead-modal-title">Lisää liidi</h2>
|
||||||
|
<button class="modal-close" id="lead-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="lead-form">
|
||||||
|
<input type="hidden" id="lead-form-id">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="lead-form-yritys">Yritys *</label>
|
||||||
|
<input type="text" id="lead-form-yritys" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-yhteyshenkilo">Yhteyshenkilö</label>
|
||||||
|
<input type="text" id="lead-form-yhteyshenkilo">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-puhelin">Puhelin</label>
|
||||||
|
<input type="text" id="lead-form-puhelin">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-sahkoposti">Sähköposti</label>
|
||||||
|
<input type="email" id="lead-form-sahkoposti">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-osoite">Osoite</label>
|
||||||
|
<input type="text" id="lead-form-osoite">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-kaupunki">Kaupunki</label>
|
||||||
|
<input type="text" id="lead-form-kaupunki">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lead-form-tila">Tila</label>
|
||||||
|
<select id="lead-form-tila">
|
||||||
|
<option value="uusi">Uusi</option>
|
||||||
|
<option value="kontaktoitu">Kontaktoitu</option>
|
||||||
|
<option value="kiinnostunut">Kiinnostunut</option>
|
||||||
|
<option value="odottaa">Odottaa toimitusta</option>
|
||||||
|
<option value="ei_kiinnosta">Ei kiinnosta</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="lead-form-muistiinpanot">Muistiinpanot</label>
|
||||||
|
<textarea id="lead-form-muistiinpanot" rows="5" placeholder="Mitä on puhuttu, milloin otettu yhteyttä, jne..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn-primary" id="lead-form-submit">Tallenna</button>
|
||||||
|
<button type="button" class="btn-secondary" id="lead-form-cancel">Peruuta</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liidi-tiedot-modal -->
|
||||||
|
<div id="lead-detail-modal" class="modal" style="display:none">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="lead-detail-title">Liidin tiedot</h2>
|
||||||
|
<button class="modal-close" id="lead-detail-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="lead-detail-body"></div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn-primary" id="lead-detail-edit">Muokkaa</button>
|
||||||
|
<button class="btn-add-row" id="lead-detail-convert" style="padding:8px 16px;font-size:0.85rem;">Muuta asiakkaaksi</button>
|
||||||
|
<button class="btn-danger" id="lead-detail-delete">Poista</button>
|
||||||
|
<button class="btn-secondary" id="lead-detail-cancel">Sulje</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
166
script.js
166
script.js
@@ -182,6 +182,7 @@ document.querySelectorAll('.tab').forEach(tab => {
|
|||||||
const target = tab.dataset.tab;
|
const target = tab.dataset.tab;
|
||||||
document.getElementById('tab-content-' + target).classList.add('active');
|
document.getElementById('tab-content-' + target).classList.add('active');
|
||||||
// Lataa sisältö tarvittaessa
|
// Lataa sisältö tarvittaessa
|
||||||
|
if (target === 'leads') loadLeads();
|
||||||
if (target === 'archive') loadArchive();
|
if (target === 'archive') loadArchive();
|
||||||
if (target === 'changelog') loadChangelog();
|
if (target === 'changelog') loadChangelog();
|
||||||
if (target === 'users') loadUsers();
|
if (target === 'users') loadUsers();
|
||||||
@@ -604,6 +605,163 @@ customerForm.addEventListener('submit', async (e) => {
|
|||||||
await loadCustomers();
|
await loadCustomers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== LEADS ====================
|
||||||
|
|
||||||
|
let leads = [];
|
||||||
|
let currentLeadId = null;
|
||||||
|
const leadModal = document.getElementById('lead-modal');
|
||||||
|
const leadDetailModal = document.getElementById('lead-detail-modal');
|
||||||
|
|
||||||
|
const leadStatusLabels = {
|
||||||
|
uusi: 'Uusi',
|
||||||
|
kontaktoitu: 'Kontaktoitu',
|
||||||
|
kiinnostunut: 'Kiinnostunut',
|
||||||
|
odottaa: 'Odottaa toimitusta',
|
||||||
|
ei_kiinnosta: 'Ei kiinnosta',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadLeads() {
|
||||||
|
try {
|
||||||
|
leads = await apiCall('leads');
|
||||||
|
renderLeads();
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLeads() {
|
||||||
|
const query = document.getElementById('lead-search-input').value.toLowerCase().trim();
|
||||||
|
let filtered = leads;
|
||||||
|
if (query) {
|
||||||
|
filtered = leads.filter(l =>
|
||||||
|
(l.yritys || '').toLowerCase().includes(query) ||
|
||||||
|
(l.yhteyshenkilo || '').toLowerCase().includes(query) ||
|
||||||
|
(l.kaupunki || '').toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ltbody = document.getElementById('leads-tbody');
|
||||||
|
const noLeads = document.getElementById('no-leads');
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
ltbody.innerHTML = '';
|
||||||
|
noLeads.style.display = 'block';
|
||||||
|
document.getElementById('leads-table').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
noLeads.style.display = 'none';
|
||||||
|
document.getElementById('leads-table').style.display = 'table';
|
||||||
|
ltbody.innerHTML = filtered.map(l => `<tr data-lead-id="${l.id}">
|
||||||
|
<td><strong>${esc(l.yritys)}</strong></td>
|
||||||
|
<td>${esc(l.yhteyshenkilo || '')}</td>
|
||||||
|
<td>${esc(l.kaupunki || '')}</td>
|
||||||
|
<td><span class="lead-status lead-status-${l.tila || 'uusi'}">${leadStatusLabels[l.tila] || l.tila || 'Uusi'}</span></td>
|
||||||
|
<td class="nowrap">${esc((l.luotu || '').substring(0, 10))}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button onclick="event.stopPropagation();editLead('${l.id}')" title="Muokkaa">✎</button>
|
||||||
|
<button onclick="event.stopPropagation();deleteLead('${l.id}','${esc(l.yritys)}')" title="Poista">🗑</button>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
document.getElementById('lead-count').textContent = `${leads.length} liidiä`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('lead-search-input').addEventListener('input', () => renderLeads());
|
||||||
|
|
||||||
|
document.getElementById('leads-tbody').addEventListener('click', (e) => {
|
||||||
|
const row = e.target.closest('tr');
|
||||||
|
if (row && row.dataset.leadId) showLeadDetail(row.dataset.leadId);
|
||||||
|
});
|
||||||
|
|
||||||
|
function showLeadDetail(id) {
|
||||||
|
const l = leads.find(x => x.id === id);
|
||||||
|
if (!l) return;
|
||||||
|
currentLeadId = id;
|
||||||
|
document.getElementById('lead-detail-title').textContent = l.yritys;
|
||||||
|
document.getElementById('lead-detail-body').innerHTML = `
|
||||||
|
<div style="padding:1.5rem;">
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-item"><div class="detail-label">Yritys</div><div class="detail-value">${detailVal(l.yritys)}</div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Tila</div><div class="detail-value"><span class="lead-status lead-status-${l.tila || 'uusi'}">${leadStatusLabels[l.tila] || 'Uusi'}</span></div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Yhteyshenkilö</div><div class="detail-value">${detailVal(l.yhteyshenkilo)}</div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Puhelin</div><div class="detail-value">${detailLink(l.puhelin, 'tel')}</div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Sähköposti</div><div class="detail-value">${detailLink(l.sahkoposti, 'email')}</div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Osoite</div><div class="detail-value">${detailVal([l.osoite, l.kaupunki].filter(Boolean).join(', '))}</div></div>
|
||||||
|
<div class="detail-item"><div class="detail-label">Lisätty</div><div class="detail-value">${detailVal(l.luotu)} (${esc(l.luoja || '')})</div></div>
|
||||||
|
${l.muokattu ? `<div class="detail-item"><div class="detail-label">Muokattu</div><div class="detail-value">${esc(l.muokattu)} (${esc(l.muokkaaja || '')})</div></div>` : ''}
|
||||||
|
</div>
|
||||||
|
${l.muistiinpanot ? `<div style="margin-top:1.25rem;"><div class="detail-label" style="margin-bottom:0.5rem;">MUISTIINPANOT</div><div style="white-space:pre-wrap;color:#555;background:#f8f9fb;padding:1rem;border-radius:8px;border:1px solid #e8ebf0;font-size:0.9rem;">${esc(l.muistiinpanot)}</div></div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
leadDetailModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lead form
|
||||||
|
document.getElementById('btn-add-lead').addEventListener('click', () => openLeadForm());
|
||||||
|
document.getElementById('lead-modal-close').addEventListener('click', () => leadModal.style.display = 'none');
|
||||||
|
document.getElementById('lead-form-cancel').addEventListener('click', () => leadModal.style.display = 'none');
|
||||||
|
|
||||||
|
function openLeadForm(lead = null) {
|
||||||
|
document.getElementById('lead-modal-title').textContent = lead ? 'Muokkaa liidiä' : 'Lisää liidi';
|
||||||
|
document.getElementById('lead-form-submit').textContent = lead ? 'Päivitä' : 'Tallenna';
|
||||||
|
document.getElementById('lead-form-id').value = lead ? lead.id : '';
|
||||||
|
document.getElementById('lead-form-yritys').value = lead ? lead.yritys : '';
|
||||||
|
document.getElementById('lead-form-yhteyshenkilo').value = lead ? (lead.yhteyshenkilo || '') : '';
|
||||||
|
document.getElementById('lead-form-puhelin').value = lead ? (lead.puhelin || '') : '';
|
||||||
|
document.getElementById('lead-form-sahkoposti').value = lead ? (lead.sahkoposti || '') : '';
|
||||||
|
document.getElementById('lead-form-osoite').value = lead ? (lead.osoite || '') : '';
|
||||||
|
document.getElementById('lead-form-kaupunki').value = lead ? (lead.kaupunki || '') : '';
|
||||||
|
document.getElementById('lead-form-tila').value = lead ? (lead.tila || 'uusi') : 'uusi';
|
||||||
|
document.getElementById('lead-form-muistiinpanot').value = lead ? (lead.muistiinpanot || '') : '';
|
||||||
|
leadModal.style.display = 'flex';
|
||||||
|
document.getElementById('lead-form-yritys').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editLead(id) {
|
||||||
|
const l = leads.find(x => x.id === id);
|
||||||
|
if (l) { leadDetailModal.style.display = 'none'; openLeadForm(l); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteLead(id, name) {
|
||||||
|
if (!confirm(`Poistetaanko liidi "${name}"?`)) return;
|
||||||
|
await apiCall('lead_delete', 'POST', { id });
|
||||||
|
leadDetailModal.style.display = 'none';
|
||||||
|
await loadLeads();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertLeadToCustomer(id) {
|
||||||
|
const l = leads.find(x => x.id === id);
|
||||||
|
if (!l) return;
|
||||||
|
if (!confirm(`Muutetaanko "${l.yritys}" asiakkaaksi?\n\nLiidi poistetaan ja asiakas luodaan sen tiedoilla.`)) return;
|
||||||
|
await apiCall('lead_to_customer', 'POST', { id });
|
||||||
|
leadDetailModal.style.display = 'none';
|
||||||
|
await loadLeads();
|
||||||
|
await loadCustomers();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('lead-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('lead-form-id').value;
|
||||||
|
const data = {
|
||||||
|
yritys: document.getElementById('lead-form-yritys').value,
|
||||||
|
yhteyshenkilo: document.getElementById('lead-form-yhteyshenkilo').value,
|
||||||
|
puhelin: document.getElementById('lead-form-puhelin').value,
|
||||||
|
sahkoposti: document.getElementById('lead-form-sahkoposti').value,
|
||||||
|
osoite: document.getElementById('lead-form-osoite').value,
|
||||||
|
kaupunki: document.getElementById('lead-form-kaupunki').value,
|
||||||
|
tila: document.getElementById('lead-form-tila').value,
|
||||||
|
muistiinpanot: document.getElementById('lead-form-muistiinpanot').value,
|
||||||
|
};
|
||||||
|
if (id) { data.id = id; await apiCall('lead_update', 'POST', data); }
|
||||||
|
else { await apiCall('lead_create', 'POST', data); }
|
||||||
|
leadModal.style.display = 'none';
|
||||||
|
await loadLeads();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lead detail actions
|
||||||
|
document.getElementById('lead-detail-close').addEventListener('click', () => leadDetailModal.style.display = 'none');
|
||||||
|
document.getElementById('lead-detail-cancel').addEventListener('click', () => leadDetailModal.style.display = 'none');
|
||||||
|
document.getElementById('lead-detail-edit').addEventListener('click', () => editLead(currentLeadId));
|
||||||
|
document.getElementById('lead-detail-delete').addEventListener('click', () => {
|
||||||
|
const l = leads.find(x => x.id === currentLeadId);
|
||||||
|
if (l) deleteLead(currentLeadId, l.yritys);
|
||||||
|
});
|
||||||
|
document.getElementById('lead-detail-convert').addEventListener('click', () => convertLeadToCustomer(currentLeadId));
|
||||||
|
|
||||||
// ==================== ARCHIVE ====================
|
// ==================== ARCHIVE ====================
|
||||||
|
|
||||||
async function loadArchive() {
|
async function loadArchive() {
|
||||||
@@ -656,6 +814,10 @@ const actionLabels = {
|
|||||||
user_create: 'Lisäsi käyttäjän',
|
user_create: 'Lisäsi käyttäjän',
|
||||||
user_update: 'Muokkasi käyttäjää',
|
user_update: 'Muokkasi käyttäjää',
|
||||||
user_delete: 'Poisti käyttäjän',
|
user_delete: 'Poisti käyttäjän',
|
||||||
|
lead_create: 'Lisäsi liidin',
|
||||||
|
lead_update: 'Muokkasi liidiä',
|
||||||
|
lead_delete: 'Poisti liidin',
|
||||||
|
lead_to_customer: 'Muutti liidin asiakkaaksi',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadChangelog() {
|
async function loadChangelog() {
|
||||||
@@ -760,11 +922,15 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
|
|||||||
customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; });
|
customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; });
|
||||||
detailModal.addEventListener('click', (e) => { if (e.target === detailModal) detailModal.style.display = 'none'; });
|
detailModal.addEventListener('click', (e) => { if (e.target === detailModal) detailModal.style.display = 'none'; });
|
||||||
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.style.display = 'none'; });
|
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.style.display = 'none'; });
|
||||||
|
leadModal.addEventListener('click', (e) => { if (e.target === leadModal) leadModal.style.display = 'none'; });
|
||||||
|
leadDetailModal.addEventListener('click', (e) => { if (e.target === leadDetailModal) leadDetailModal.style.display = 'none'; });
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
customerModal.style.display = 'none';
|
customerModal.style.display = 'none';
|
||||||
detailModal.style.display = 'none';
|
detailModal.style.display = 'none';
|
||||||
userModal.style.display = 'none';
|
userModal.style.display = 'none';
|
||||||
|
leadModal.style.display = 'none';
|
||||||
|
leadDetailModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
35
style.css
35
style.css
@@ -1053,6 +1053,41 @@ span.empty {
|
|||||||
background: #c0392b;
|
background: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lead status badges */
|
||||||
|
.lead-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-status-uusi {
|
||||||
|
background: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-status-kontaktoitu {
|
||||||
|
background: #f39c12;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-status-kiinnostunut {
|
||||||
|
background: #2ecc71;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-status-odottaa {
|
||||||
|
background: #9b59b6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-status-ei_kiinnosta {
|
||||||
|
background: #e8ebf0;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
/* Changelog */
|
/* Changelog */
|
||||||
.nowrap {
|
.nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user