@אa מי שרוצה כתבתי קוד PHP שמקבל וובהוק מנדרים ומוסיף למערכת בהערות צריך לכתוב את שם המתרים
<?php
// הגדרות API
$token = "{token}";
$api_url = "https://www.call2all.co.il/ym/api/";
// נתיבים
$path_names = 'ivr2:Points/EnterIDValName.ini';
$path_total = 'ivr2:Points/points_total.ymgr';
$path_log = 'ivr2:Points/manual_log.ini';
// קבצי דיבוג מקומיים בשרת ה-PHP
$debug_file = 'debug_webhook.log';
$unassigned_csv = 'unassigned_donations.csv';
// 1. קבלת הנתונים ושמירה לדיבוג ראשוני
$json_data = file_get_contents('php://input');
$data = json_decode($json_data, true);
// רישום כל כניסה לקובץ דיבוג כדי שלא יאבד כלום
$timestamp_log = date("Y-m-d H:i:s");
file_put_contents($debug_file, "[$timestamp_log] Raw Data: " . $json_data . PHP_EOL, FILE_APPEND);
if (!$data) die("No data received");
$comment_name = trim($data['Comments'] ?? '');
$amount = floatval($data['Amount'] ?? 0);
function call_yemot($method, $path, $token, $contents = null) {
global $api_url;
$url = $api_url . $method . "?token=" . $token . "&what=" . $path;
if ($contents !== null) $url .= "&contents=" . urlencode($contents);
$res = file_get_contents($url);
return json_decode($res, true);
}
// 2. חיפוש גמיש (Fuzzy Matching)
$fundraiser_id = null;
$best_score = -1;
$res_names = call_yemot('GetTextFile', $path_names, $token);
if (isset($res_names['contents']) && !empty($comment_name)) {
$lines = explode("\n", $res_names['contents']);
foreach ($lines as $line) {
$parts = explode('=', $line);
if (count($parts) === 2) {
$current_id = trim($parts[0]);
$current_name = trim($parts[1]);
// חישוב דמיון
similar_text($comment_name, $current_name, $percent);
if ($percent > $best_score) {
$best_score = $percent;
$fundraiser_id = $current_id;
}
}
}
}
// 3. ביצוע עדכון או שמירה ל-CSV במקרה של כישלון
// הורדתי את הסף ל-50% בגלל תארים כמו "הרב" ו"שליט"א"
if ($fundraiser_id && $best_score > 50) {
// א. עדכון YMGR
$res_total = call_yemot('GetTextFile', $path_total, $token);
$t_lines = explode("\n", trim($res_total['contents'] ?? ""));
$found = false;
foreach ($t_lines as &$line) {
if (strpos($line, "EnterId#$fundraiser_id") !== false) {
$found = true;
preg_match('/PointsTotalAll#([\d.]+)/', $line, $matches);
$new_val = (isset($matches[1]) ? floatval($matches[1]) : 0) + $amount;
$line = preg_replace('/PointsTotalAll#[\d.]+/', "PointsTotalAll#$new_val", $line);
$line = preg_replace('/PointsTotal#[\d.]+/', "PointsTotal#$new_val", $line);
}
}
if (!$found) $t_lines[] = "EnterId#{$fundraiser_id}%PointsTotalAll#{$amount}%PointsTotal#{$amount}";
call_yemot('UploadTextFile', $path_total, $token, implode("\n", $t_lines));
// ב. רישום ללוג ה-HTML
$res_log = call_yemot('GetTextFile', $path_log, $token);
$time_ms = round(microtime(true) * 1000);
$time_hi = date("H:i");
$new_entry = "{$time_ms}|{$fundraiser_id}|{$amount}|{$time_hi}";
call_yemot('UploadTextFile', $path_log, $token, $new_entry . "\n" . ($res_log['contents'] ?? ""));
file_put_contents($debug_file, "[$timestamp_log] SUCCESS: Matched $comment_name to ID $fundraiser_id ($best_score%)" . PHP_EOL, FILE_APPEND);
echo "Success";
} else {
// שמירה ל-CSV עבור תרומות שלא זוהו
$is_new = !file_exists($unassigned_csv);
$fp = fopen($unassigned_csv, 'a');
if ($is_new) {
fputs($fp, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM לעברית
fputcsv($fp, ['תאריך', 'שם מהערה', 'סכום', 'טלפון', 'אחוז התאמה הכי קרוב']);
}
fputcsv($fp, [$timestamp_log, $comment_name, $amount, $data['Phone'] ?? '', $best_score . "%"]);
fclose($fp);
file_put_contents($debug_file, "[$timestamp_log] FAILED: No match for $comment_name (Best: $best_score%)" . PHP_EOL, FILE_APPEND);
echo "Saved to CSV";
}
Spoiler
מי שרוצה שידרגתי את שתי הקודים כך שיש גם יעד אישי לכל מתרים
מסך ניהול
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<title>מנהל קמפיין - Points Sync</title>
<style>
:root { --primary: #0ea5e9; --success: #22c55e; --bg: #0f172a; --card: #1e293b; --border: #334155; --gold: #f59e0b; }
body { background: var(--bg); color: #f1f5f9; font-family: system-ui, sans-serif; margin: 0; padding: 20px; text-align: right; }
.manual-entry-box { max-width: 600px; margin: 50px auto; background: var(--card); padding: 30px; border-radius: 20px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); border: 1px solid var(--border); }
h1 { text-align: center; color: var(--primary); }
label { display: block; margin-top: 15px; font-weight: bold; }
input { width: 100%; padding: 15px; margin: 10px 0; border-radius: 10px; border: 1px solid var(--border); background: #0f172a; color: white; font-size: 1.2rem; box-sizing: border-box; }
.main-btn { background: var(--success); color: white; border: none; padding: 15px; border-radius: 10px; cursor: pointer; font-weight: bold; width: 100%; font-size: 1.1rem; margin-top: 20px; transition: 0.3s; }
.main-btn:hover { filter: brightness(1.1); transform: translateY(-2px); }
.settings-trigger { position: fixed; top: 20px; left: 20px; background: #334155; color: white; border: none; padding: 12px 20px; border-radius: 50px; cursor: pointer; font-weight: bold; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 1000; backdrop-filter: blur(5px); }
.modal-content { background: var(--card); width: 90%; max-width: 800px; margin: 40px auto; border-radius: 20px; padding: 25px; border: 1px solid var(--border); position: relative; max-height: 85vh; overflow-y: auto; }
.close-btn { position: absolute; top: 15px; right: 20px; font-size: 1.5rem; cursor: pointer; }
.tabs { display: flex; gap: 5px; border-bottom: 2px solid var(--border); margin-bottom: 20px; }
.tab-btn { background: none; border: none; color: #8b949e; padding: 10px 15px; cursor: pointer; font-size: 0.95rem; }
.tab-btn.active { color: var(--primary); border-bottom: 2px solid var(--primary); font-weight: bold; }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
.log-list { background: #0f172a; padding: 10px; border-radius: 8px; max-height: 350px; overflow-y: auto; }
.log-item { display: flex; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--border); align-items: center; }
.log-item label { display: flex; align-items: center; cursor: pointer; width: 100%; gap: 15px; }
.badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 5px; color: white; font-weight: bold; }
.badge-manual { background: var(--primary); }
.badge-phone { background: var(--success); }
.status { margin-top: 15px; font-size: 0.9rem; text-align: center; font-weight: bold; }
</style>
</head>
<body>
<button class="settings-trigger" onclick="openModal()"><span>⚙️</span> הגדרות קמפיין</button>
<div class="manual-entry-box">
<h1>💰 הזנת תרומה ידנית</h1>
<label>מספר אישי של המתרים:</label>
<input type="number" id="manualId" placeholder="למשל 215" autofocus>
<label>סכום התרומה (₪):</label>
<input type="number" id="manualAmount" placeholder="0.00">
<button class="main-btn" onclick="executeManualDonation()">✅ אשר והוסף למערכת</button>
<div id="mainStatus" class="status"></div>
</div>
<div id="settingsModal" class="modal">
<div class="modal-content">
<span class="close-btn" onclick="closeModal()">✖️</span>
<h2 style="border-bottom: 1px solid var(--border); padding-bottom: 10px;">⚙️ ניהול והגדרות</h2>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab(event, 'tabNames')">👥 שמות ויעדים</button>
<button class="tab-btn" onclick="switchTab(event, 'tabGoal')">📈 יעד ותאריך</button>
<button class="tab-btn" onclick="switchTab(event, 'tabTotals')">📊 עריכת סכומים</button>
<button class="tab-btn" onclick="switchTab(event, 'tabLogs')">🗑️ מחיקת לוג</button>
</div>
<div id="tabNames" class="tab-pane active">
<div style="display:flex; gap:10px;">
<input type="text" id="editId" placeholder="מספר אישי">
<button onclick="findUser()" style="width:100px; background:var(--primary); border:none; border-radius:8px; color:white; cursor:pointer;">חפש</button>
</div>
<input type="text" id="editName" placeholder="שם המתרים">
<input type="number" id="editGoal" placeholder="יעד אישי (₪) - אופציונלי">
<button class="main-btn" style="background:var(--primary)" onclick="saveUser()">שמור פרטים ויעד</button>
</div>
<div id="tabGoal" class="tab-pane">
<label>יעד הקמפיין הכולל (₪):</label><input type="number" id="goalVal">
<label>תאריך ושעת סיום:</label><input type="datetime-local" id="dateVal">
<button class="main-btn" onclick="saveConfig()">🔄 עדכן יעד ותאריך</button>
<hr style="border:0; border-top:1px solid var(--border); margin:25px 0;">
<label style="color:var(--gold)">מפתח אבטחה (Token) API:</label>
<input type="password" id="tokenInput" placeholder="הכנס טוקן חדש">
<button class="main-btn" style="background:#6366f1" onclick="updateToken()">🔄 עדכן מפתח Token</button>
</div>
<div id="tabTotals" class="tab-pane"><div id="totalsList" class="log-list">טוען נתונים...</div></div>
<div id="tabLogs" class="tab-pane">
<div id="logsList" class="log-list">טוען לוג פעולות מהשרת...</div>
<button class="main-btn" style="background:#ef4444" onclick="deleteLogs()">❌ מחק פעולות וסנכרן שרת</button>
</div>
<div id="modalStatus" class="status"></div>
</div>
</div>
<script>
let CURRENT_TOKEN = localStorage.getItem('campaign_token') || 'YOUR_TOKEN_HERE';
let nameMap = {};
let goalMap = {};
// נתיבים בשרת - הכל בתיקיית Points
const PATH_TOTAL = 'ivr2:Points/points_total.ymgr';
const PATH_PHONE_LOG = 'ivr2:Points/points_log.2026-02.ymgr';
const PATH_MANUAL_LOG = 'ivr2:Points/manual_log.ini';
const PATH_CONFIG = 'ivr2:Points/config.ini';
const PATH_NAMES = 'ivr2:Points/EnterIDValName.ini';
const PATH_GOALS = 'ivr2:Points/EnterIDGoal.ini';
document.getElementById('tokenInput').value = CURRENT_TOKEN;
function updateToken() {
const newToken = document.getElementById('tokenInput').value.trim();
if(!newToken) return alert("נא להזין טוקן");
CURRENT_TOKEN = newToken;
localStorage.setItem('campaign_token', newToken);
alert("הטוקן עודכן במחשב זה!");
location.reload();
}
function openModal() { document.getElementById('settingsModal').style.display = 'block'; }
function closeModal() { document.getElementById('settingsModal').style.display = 'none'; }
async function callApi(cmd, path, content = "") {
let url = `https://www.call2all.co.il/ym/api/${cmd}?token=${CURRENT_TOKEN}&what=${path}`;
if(content) url += `&contents=${encodeURIComponent(content)}`;
return fetch(url).then(r => r.json());
}
async function switchTab(e, id) {
document.querySelectorAll('.tab-pane, .tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById(id).classList.add('active');
e.currentTarget.classList.add('active');
await fetchNames();
if(id === 'tabGoal') loadCurrentConfig();
if(id === 'tabTotals') fetchTotals();
if(id === 'tabLogs') fetchLogs();
}
async function fetchNames() {
const [resNames, resGoals] = await Promise.all([
callApi('GetTextFile', PATH_NAMES),
callApi('GetTextFile', PATH_GOALS)
]);
nameMap = {};
goalMap = {};
if (resNames.contents) {
resNames.contents.split(/[\r\n]+/).forEach(line => {
const parts = line.split('=');
if (parts.length === 2) nameMap[parts[0].trim()] = parts[1].trim();
});
}
if (resGoals.contents) {
resGoals.contents.split(/[\r\n]+/).forEach(line => {
const parts = line.split('=');
if (parts.length === 2) goalMap[parts[0].trim()] = parseFloat(parts[1].trim());
});
}
}
async function findUser() {
const id = document.getElementById('editId').value;
if (!id) return alert("נא להזין מספר אישי");
await fetchNames();
document.getElementById('editName').value = nameMap[id] || "";
document.getElementById('editGoal').value = goalMap[id] || "";
}
async function saveUser() {
const id = document.getElementById('editId').value;
const name = document.getElementById('editName').value;
const goal = document.getElementById('editGoal').value;
if (!id) return alert("חובה להזין מספר אישי");
let [resNames, resGoals] = await Promise.all([
callApi('GetTextFile', PATH_NAMES),
callApi('GetTextFile', PATH_GOALS)
]);
let linesNames = (resNames.contents || "").split('\n').filter(l => l.trim() && !l.startsWith(`${id}=`));
if (name) linesNames.push(`${id}=${name}`);
let linesGoals = (resGoals.contents || "").split('\n').filter(l => l.trim() && !l.startsWith(`${id}=`));
if (goal) linesGoals.push(`${id}=${goal}`);
await Promise.all([
callApi('UploadTextFile', PATH_NAMES, linesNames.join('\n')),
callApi('UploadTextFile', PATH_GOALS, linesGoals.join('\n'))
]);
alert("פרטי המתרים והיעד האישי עודכנו בהצלחה!");
await fetchNames();
}
async function executeManualDonation() {
const id = document.getElementById('manualId').value;
const amt = parseFloat(document.getElementById('manualAmount').value);
if(!id || !amt) return alert("מלא מספר וסכום");
document.getElementById('mainStatus').innerText = "⏳ מעדכן שרת...";
let resT = await callApi('GetTextFile', PATH_TOTAL);
let tLines = (resT.contents || "").trim().split('\n');
let found = false;
let updatedT = tLines.map(l => {
if(l.includes(`EnterId#${id}`)) {
found = true;
let old = parseFloat((l.match(/PointsTotalAll#([\d.]+)/) || [0,0])[1]);
let sum = old + amt;
return l.replace(/PointsTotalAll#[\d.]+/, `PointsTotalAll#${sum}`).replace(/PointsTotal#[\d.]+/, `PointsTotal#${sum}`);
}
return l;
});
if(!found) updatedT.push(`EnterId#${id}%PointsTotalAll#${amt}%PointsTotal#${amt}`);
let resL = await callApi('GetTextFile', PATH_MANUAL_LOG);
let currentManualLog = resL.contents || "";
const timeStr = new Date().toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'});
const newEntry = `${Date.now()}|${id}|${amt}|${timeStr}`;
const updatedManualLog = newEntry + "\n" + currentManualLog;
await Promise.all([
callApi('UploadTextFile', PATH_TOTAL, updatedT.join('\n')),
callApi('UploadTextFile', PATH_MANUAL_LOG, updatedManualLog)
]);
document.getElementById('mainStatus').innerText = "✅ נרשם וסונכרן לשרת!";
document.getElementById('manualAmount').value = "";
}
async function fetchLogs() {
let [resPhone, resManual] = await Promise.all([
callApi('GetTextFile', PATH_PHONE_LOG),
callApi('GetTextFile', PATH_MANUAL_LOG)
]);
let allEntries = [];
const todayStr = new Date().toISOString().split('T')[0];
if (resPhone.contents) {
resPhone.contents.trim().split('\n').forEach(l => {
let id = (l.match(/id#(\d+)/) || [])[1];
let pts = (l.match(/Points#(\d+)/) || [])[1];
let timeM = (l.match(/(\d{2}:\d{2})/) || [""])[0];
if (id) allEntries.push({id, pts, time: timeM, type: 'phone', fullLine: l, ts: timeM ? new Date(`${todayStr}T${timeM}:00`).getTime() : 0});
});
}
if (resManual.contents) {
resManual.contents.trim().split('\n').forEach(l => {
let [ts, id, amt, time] = l.split('|');
if (id) allEntries.push({id, pts: amt, time, type: 'manual', fullLine: l, ts: parseInt(ts)});
});
}
allEntries.sort((a, b) => b.ts - a.ts);
let html = allEntries.map(entry => {
let name = nameMap[entry.id] || entry.id;
return `<div class="log-item"><label><input type="checkbox" class="log-cb" data-id="${entry.id}" data-amt="${entry.pts}" data-type="${entry.type}" data-line="${entry.fullLine}"><span><span class="badge ${entry.type === 'manual' ? 'badge-manual' : 'badge-phone'}">${entry.type === 'manual' ? 'ידני' : 'טלפון'}</span> [${entry.time}] <b>${name}</b> - ₪${entry.pts}</span></label></div>`;
}).join('');
document.getElementById('logsList').innerHTML = html || "אין היסטוריה.";
}
async function deleteLogs() {
const checkboxes = document.querySelectorAll('.log-cb:checked');
if (checkboxes.length === 0) return alert("בחר פעולות");
if (!confirm("זה ימחק את השורות מכל המחשבים ויקזז סכומים. המשך?")) return;
document.getElementById('modalStatus').innerText = "⏳ מסנכרן שרת...";
let [resP, resM, resT] = await Promise.all([
callApi('GetTextFile', PATH_PHONE_LOG),
callApi('GetTextFile', PATH_MANUAL_LOG),
callApi('GetTextFile', PATH_TOTAL)
]);
let phoneLines = resP.contents ? resP.contents.trim().split('\n') : [];
let manualLines = resM.contents ? resM.contents.trim().split('\n') : [];
let totalLines = resT.contents ? resT.contents.trim().split('\n') : [];
checkboxes.forEach(cb => {
const id = cb.getAttribute('data-id'), amt = parseFloat(cb.getAttribute('data-amt')), type = cb.getAttribute('data-type'), line = cb.getAttribute('data-line');
if (type === 'phone') phoneLines = phoneLines.filter(l => l !== line);
else manualLines = manualLines.filter(l => l !== line);
totalLines = totalLines.map(tL => {
if(tL.includes(`EnterId#${id}`)) {
let old = parseFloat((tL.match(/PointsTotalAll#([\d.]+)/) || [0,0])[1]);
let sum = Math.max(0, old - amt);
return tL.replace(/PointsTotalAll#[\d.]+/, `PointsTotalAll#${sum}`).replace(/PointsTotal#[\d.]+/, `PointsTotal#${sum}`);
} return tL;
});
});
await Promise.all([
callApi('UploadTextFile', PATH_PHONE_LOG, phoneLines.join('\n')),
callApi('UploadTextFile', PATH_MANUAL_LOG, manualLines.join('\n')),
callApi('UploadTextFile', PATH_TOTAL, totalLines.join('\n'))
]);
document.getElementById('modalStatus').innerText = "✅ הסנכרון הושלם בכל המחשבים!";
fetchLogs();
}
async function loadCurrentConfig() {
const res = await callApi('GetTextFile', PATH_CONFIG);
if(res.contents) {
const goal = (res.contents.match(/goal=(.*)/) || [])[1];
const date = (res.contents.match(/date=(.*)/) || [])[1];
if(goal) document.getElementById('goalVal').value = goal;
if(date) document.getElementById('dateVal').value = date;
}
}
async function saveConfig() {
const content = `goal=${document.getElementById('goalVal').value}\ndate=${document.getElementById('dateVal').value}`;
await callApi('UploadTextFile', PATH_CONFIG, content);
alert("עודכן בשרת!");
}
async function fetchTotals() {
let res = await callApi('GetTextFile', PATH_TOTAL);
if(!res.contents) return;
let html = res.contents.trim().split('\n').map(l => {
let id = (l.match(/EnterId#(\d+)/) || [])[1];
let val = (l.match(/PointsTotalAll#([\d.]+)/) || [])[1];
if(!id) return '';
let name = nameMap[id] || id;
return `<div class="log-item"><span><b>${name}</b> (${id})</span><div style="display:flex; gap:5px;"><input type="number" id="inp-${id}" value="${val}" style="width:110px;"><button onclick="updateTotal('${id}')" style="background:var(--primary); color:white; border:none; padding:8px 12px; border-radius:8px; cursor:pointer;">עדכן</button></div></div>`;
}).join('');
document.getElementById('totalsList').innerHTML = html;
}
async function updateTotal(id) {
const newVal = document.getElementById(`inp-${id}`).value;
let res = await callApi('GetTextFile', PATH_TOTAL);
let updated = res.contents.trim().split('\n').map(l => l.includes(`EnterId#${id}`) ? l.replace(/PointsTotalAll#[\d.]+/, `PointsTotalAll#${newVal}`).replace(/PointsTotal#[\d.]+/, `PointsTotal#${newVal}`) : l);
await callApi('UploadTextFile', PATH_TOTAL, updated.join('\n'));
alert("סכום עודכן בשרת!");
}
</script>
</body>
</html>
מסך התרמה
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<title>מערכת התרמה LIVE - Points Sync</title>
<style>
:root { --accent: #0ea5e9; --gold: #fbbf24; --bg: #020617; }
body { background: var(--bg); color: #f8fafc; font-family: system-ui, sans-serif; margin: 0; padding: 10px; overflow: hidden; }
.container { display: grid; grid-template-columns: 320px 1fr 320px; gap: 15px; height: 95vh; }
.card { background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 20px; padding: 15px; backdrop-filter: blur(10px); display: flex; flex-direction: column; overflow: hidden; }
.center-card { display: flex; flex-direction: column; justify-content: space-between; align-items: center; text-align: center; padding: 40px 20px; }
h2 { font-size: 1.3rem; border-bottom: 2px solid var(--accent); padding-bottom: 8px; margin: 0 0 10px 0; color: white; text-align: center; }
.countdown-timer { display: flex; gap: 15px; margin-bottom: 20px; }
.time-unit { background: rgba(14, 165, 233, 0.2); padding: 10px 15px; border-radius: 12px; border: 1px solid var(--accent); min-width: 60px; }
.time-num { display: block; font-size: 2rem; font-weight: bold; color: white; }
.time-label { font-size: 0.8rem; opacity: 0.7; }
.total-amount { font-size: 8rem; font-weight: 900; color: var(--accent); text-shadow: 0 0 30px rgba(14, 165, 233, 0.4); margin: 10px 0; line-height: 1; }
.progress-wrapper { width: 90%; }
.progress-bg { background: #1e293b; height: 40px; border-radius: 50px; border: 2px solid #334155; overflow: hidden; position: relative; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #0ea5e9, #22d3ee); width: 0%; transition: width 2s; }
.list-box { flex-grow: 1; overflow: hidden; display: flex; flex-direction: column; padding-right: 5px; }
.item { display: flex; flex-direction: column; align-items: stretch; padding: 10px; background: rgba(255,255,255,0.05); margin-bottom: 8px; border-radius: 10px; border-right: 4px solid var(--accent); flex-shrink: 0; transition: all 0.5s; }
.item-header { display: flex; justify-content: space-between; align-items: center; }
.item-info { display: flex; flex-direction: column; }
.item-name { font-weight: bold; font-size: 1.1rem; display: flex; align-items: center; gap: 6px; }
.item-time { font-size: 0.8rem; color: #94a3b8; font-weight: bold; }
.item-val { font-weight: 900; color: var(--gold); font-size: 1.3rem; }
.badge { font-size: 0.6rem; padding: 2px 5px; border-radius: 4px; font-weight: bold; }
.badge-manual { background: #0ea5e9; color: white; }
.badge-phone { background: #22c55e; color: white; }
.live-dot { height: 10px; width: 10px; background: #ef4444; border-radius: 50%; display: inline-block; margin-left: 8px; animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
/* עיצוב לאנימציה של הגעה ליעד */
@keyframes goldGlow {
0% { box-shadow: 0 0 5px rgba(251, 191, 36, 0.2); border-color: rgba(251, 191, 36, 0.5); }
50% { box-shadow: 0 0 20px rgba(251, 191, 36, 0.8); border-color: rgba(251, 191, 36, 1); }
100% { box-shadow: 0 0 5px rgba(251, 191, 36, 0.2); border-color: rgba(251, 191, 36, 0.5); }
}
.goal-reached {
animation: goldGlow 2s infinite;
background: rgba(251, 191, 36, 0.1) !important;
border-right-color: var(--gold) !important;
}
.goal-badge {
background: var(--gold); color: black; font-size: 0.75rem; padding: 2px 8px; border-radius: 12px; font-weight: bold; margin-right: 10px; display: inline-block;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h2>🏆 מובילים</h2>
<div id="leaderboard" class="list-box" style="overflow-y: auto;"></div>
</div>
<div class="card center-card">
<div>
<div style="margin-bottom: 10px; font-size: 1.2rem; font-weight: bold;"><span class="live-dot"></span>הקמפיין מסתיים בעוד:</div>
<div id="countdown" class="countdown-timer">
<div class="time-unit"><span id="days" class="time-num">00</span><span class="time-label">ימים</span></div>
<div class="time-unit"><span id="hours" class="time-num">00</span><span class="time-label">שעות</span></div>
<div class="time-unit"><span id="minutes" class="time-num">00</span><span class="time-label">דקות</span></div>
<div class="time-unit"><span id="seconds" class="time-num">00</span><span class="time-label">שניות</span></div>
</div>
</div>
<div>
<h1 style="margin:0; font-size: 1.8rem; opacity: 0.9;">סך הכל נאסף:</h1>
<div id="main-total" class="total-amount">₪0</div>
</div>
<div class="progress-wrapper">
<div class="progress-bg"><div id="progress-bar" class="progress-fill"></div></div>
<div id="percent-label" style="font-size: 3rem; color: var(--gold); font-weight: bold; margin-top: 10px;">0%</div>
<div id="goal-text" style="font-size: 1.5rem; opacity: 0.6;">מחשב נתונים...</div>
</div>
</div>
<div class="card">
<h2>🔔 תרומות אחרונות</h2>
<div id="recent-log" class="list-box" style="overflow-y: auto;"></div>
</div>
</div>
<script>
// הגדרות שלוחה - הכל תחת תיקיית Points
const TOKEN = 'WU1BUElL.apik_MmtjfW7MhNRZGw4h--R54Q.zPUwopxRh5JxRpUutkWFKUhyDWtLfvYwWKQ30PTuXZw';
const PATH_TOTAL = 'ivr2:Points/points_total.ymgr';
const PATH_PHONE_LOG = 'ivr2:Points/points_log.2026-02.ymgr';
const PATH_MANUAL_LOG = 'ivr2:Points/manual_log.ini';
const PATH_CONFIG = 'ivr2:Points/config.ini';
const PATH_NAMES = 'ivr2:Points/EnterIDValName.ini';
const PATH_GOALS = 'ivr2:Points/EnterIDGoal.ini';
let GOAL = 500000;
let DEADLINE = new Date();
let nameMap = {};
let goalMap = {};
function updateCountdown() {
const now = new Date();
const diff = DEADLINE - now;
if (diff <= 0) {
document.querySelectorAll('.time-num').forEach(el => el.innerText = "00");
return;
}
document.getElementById('days').innerText = String(Math.floor(diff / (1000 * 60 * 60 * 24))).padStart(2, '0');
document.getElementById('hours').innerText = String(Math.floor((diff / (1000 * 60 * 60)) % 24)).padStart(2, '0');
document.getElementById('minutes').innerText = String(Math.floor((diff / 1000 / 60) % 60)).padStart(2, '0');
document.getElementById('seconds').innerText = String(Math.floor((diff / 1000) % 60)).padStart(2, '0');
}
async function update() {
try {
const [resTotal, resLogPhone, resNames, resConfig, resLogManual, resGoals] = await Promise.all([
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_TOTAL}`).then(r => r.json()),
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_PHONE_LOG}`).then(r => r.json()),
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_NAMES}`).then(r => r.json()),
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_CONFIG}`).then(r => r.json()),
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_MANUAL_LOG}`).then(r => r.json()),
fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${TOKEN}&what=${PATH_GOALS}`).then(r => r.json())
]);
if (resConfig.contents) {
const g = resConfig.contents.match(/goal=(.*)/);
const d = resConfig.contents.match(/date=(.*)/);
if (g) {
GOAL = parseFloat(g[1]);
document.getElementById('goal-text').innerText = `מתוך יעד של ${GOAL.toLocaleString()} ₪`;
}
if (d) DEADLINE = new Date(d[1]);
}
if (resNames.contents) {
resNames.contents.split(/[\r\n]+/).forEach(line => {
const parts = line.split('=');
if (parts.length === 2) nameMap[parts[0].trim()] = parts[1].trim();
});
}
if (resGoals.contents) {
goalMap = {};
resGoals.contents.split(/[\r\n]+/).forEach(line => {
const parts = line.split('=');
if (parts.length === 2) goalMap[parts[0].trim()] = parseFloat(parts[1].trim());
});
}
if (resTotal.contents) {
const lines = resTotal.contents.trim().split(/[\r\n]+/);
let totalSum = 0; let users = [];
lines.forEach(l => {
const idMatch = l.match(/EnterId#(\d+)/);
const scoreMatch = l.match(/PointsTotalAll#([\d.]+)/);
if (idMatch && scoreMatch) {
const val = parseFloat(scoreMatch[1]);
totalSum += val;
users.push({id: idMatch[1], val: val});
}
});
document.getElementById('main-total').innerText = '₪' + totalSum.toLocaleString();
document.getElementById('progress-bar').style.width = Math.min((totalSum/GOAL)*100, 100) + '%';
document.getElementById('percent-label').innerText = ((totalSum/GOAL)*100).toFixed(1) + '%';
users.sort((a,b) => b.val - a.val);
document.getElementById('leaderboard').innerHTML = users.map((u,i) => {
const personalGoal = goalMap[u.id];
let goalHtml = '';
let reachedClass = '';
let goalBadgeHtml = '';
if (personalGoal && personalGoal > 0) {
const rawPct = (u.val / personalGoal) * 100;
const pct = Math.min(rawPct, 100).toFixed(0);
if (rawPct >= 100) {
reachedClass = 'goal-reached';
goalBadgeHtml = `<span class="goal-badge">הגיע ליעד! 👑</span>`;
}
goalHtml = `
<div style="font-size: 0.8rem; color: #94a3b8; display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<div style="background: rgba(255,255,255,0.1); height: 6px; flex-grow: 1; border-radius: 5px; overflow: hidden;">
<div style="background: var(--gold); height: 100%; width: ${pct}%; transition: width 1s;"></div>
</div>
<span style="min-width: 40px; text-align: left; font-weight: bold; color: ${rawPct >= 100 ? 'var(--gold)' : 'inherit'}">${pct}%</span>
</div>
`;
}
return `
<div class="item ${reachedClass}">
<div class="item-header">
<span class="item-name"><b>${i+1}.</b> ${nameMap[u.id] || u.id} ${goalBadgeHtml}</span>
<span style="color:var(--accent); font-weight:bold;">₪${u.val.toLocaleString()}</span>
</div>
${goalHtml}
</div>
`;
}).join('');
}
let allItems = [];
const todayStr = new Date().toISOString().split('T')[0];
if (resLogPhone.contents) {
resLogPhone.contents.trim().split(/[\r\n]+/).forEach((line, idx) => {
const idM = line.match(/id#(\d+)/);
const ptM = line.match(/Points#(\d+)/);
const timeM = line.match(/(\d{2}:\d{2})/);
if (idM && ptM) {
let ts = timeM ? new Date(`${todayStr}T${timeM[1]}:00`).getTime() : (Date.now() - 500000);
allItems.push({ id: idM[1], amount: ptM[1], type: 'phone', timestamp: ts, timeStr: timeM ? timeM[1] : "--:--" });
}
});
}
if (resLogManual.contents) {
resLogManual.contents.trim().split('\n').forEach(l => {
let [ts, id, amt, time] = l.split('|');
if (id) allItems.push({ id, amount: amt, type: 'manual', timestamp: parseInt(ts), timeStr: time });
});
}
allItems.sort((a, b) => b.timestamp - a.timestamp);
document.getElementById('recent-log').innerHTML = allItems.map(item => `
<div class="item" style="flex-direction: row; justify-content: space-between; align-items: center;">
<div class="item-info">
<span class="item-name">
<span class="badge ${item.type === 'manual' ? 'badge-manual' : 'badge-phone'}">${item.type === 'manual' ? 'ידני' : 'טלפון'}</span>
${nameMap[item.id] || 'מתרים ' + item.id}
</span>
<span class="item-time">${item.timeStr}</span>
</div>
<span class="item-val">₪${parseFloat(item.amount).toLocaleString()}</span>
</div>
`).join('');
} catch (e) { console.error("Update Error:", e); }
}
setInterval(updateCountdown, 1000);
setInterval(update, 5000);
updateCountdown();
update();
</script>
</body>
</html>
קרדיט ל @אa על הפיתוח המועיל