// ============================================
// CONFIGURATION
// ============================================
const WEBHOOK_URL = 'https://n8n.srv1129246.hstgr.cloud/webhook/cleaner-crm';
const GOOGLE_MAPS_API_KEY = 'AIzaSyDMjVgA7IyJjtC6cxEOn9b_05KKePA-k0s';
// ============================================
// STATE
// ============================================
let orders = [];
let workers = [];
let schedules = [];
let filteredOrders = [];
let filteredWorkers = [];
let currentTab = 'all';
let currentMonth = new Date();
let sortOrder = 'desc';
let selectedOrder = null;
let selectedWorkerForAssign = null;
// Map state
let map = null;
let markers = [];
let infoWindow = null;
let mapVisible = false;
let geocoder = null;
const statusMap = {
'NEW ORDER': { label: 'חדש', class: 'status-new' },
'Worker Assigend': { label: 'משובץ', class: 'status-assigned' },
'Order Completed': { label: 'הושלם', class: 'status-completed' },
'Order Cancelled': { label: 'בוטל', class: 'status-cancelled' }
};
const hebrewMonths = ['ינואר','פברואר','מרץ','אפריל','מאי','יוני','יולי','אוגוסט','ספטמבר','אוקטובר','נובמבר','דצמבר'];
const hebrewDays = ['א׳','ב׳','ג׳','ד׳','ה׳','ו׳','ש׳'];
// ============================================
// INIT
// ============================================
document.addEventListener('DOMContentLoaded', () => {
loadAllData();
renderCalendar();
});
// Initialize Google Map (called by API callback)
function initMap() {
// Default center: Israel
const israelCenter = { lat: 32.0853, lng: 34.7818 };
map = new google.maps.Map(document.getElementById('mapContainer'), {
zoom: 8,
center: israelCenter,
mapTypeControl: false,
streetViewControl: false,
fullscreenControl: true,
styles: [
{ featureType: "poi", stylers: [{ visibility: "off" }] }
]
});
infoWindow = new google.maps.InfoWindow();
geocoder = new google.maps.Geocoder();
console.log('Google Maps initialized');
}
function toggleMap() {
mapVisible = !mapVisible;
const container = document.getElementById('mapContainer');
const legend = document.getElementById('mapLegend');
const toggleText = document.getElementById('mapToggleText');
container.classList.toggle('visible', mapVisible);
legend.classList.toggle('visible', mapVisible);
toggleText.textContent = mapVisible ? 'הסתר מפה' : 'הצג מפה';
if (mapVisible && map) {
// Trigger resize and update markers
google.maps.event.trigger(map, 'resize');
updateMapMarkers();
}
}
function updateMapMarkers() {
if (!map) return;
// Clear existing markers
markers.forEach(m => m.setMap(null));
markers = [];
// Get orders with coordinates from Airtable
const ordersWithLocation = filteredOrders.filter(o => {
const lat = o.latitude || o.Latitude || o.lat;
const lng = o.longitude || o.Longitude || o.lng;
return lat && lng && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lng));
});
document.getElementById('mapOrdersCount').textContent = `(${ordersWithLocation.length}/${filteredOrders.length})`;
if (ordersWithLocation.length === 0) {
console.log('No orders with coordinates, trying geocoding...');
geocodeOrdersAndAddMarkers();
return;
}
const bounds = new google.maps.LatLngBounds();
ordersWithLocation.forEach(order => {
const lat = parseFloat(order.latitude || order.Latitude || order.lat);
const lng = parseFloat(order.longitude || order.Longitude || order.lng);
const position = { lat, lng };
const marker = createOrderMarker(order, position);
markers.push(marker);
bounds.extend(position);
});
if (markers.length > 0) {
map.fitBounds(bounds);
// Don't zoom too much for single marker
if (markers.length === 1) {
map.setZoom(14);
}
}
console.log(`Map: ${markers.length} markers added`);
}
function geocodeOrdersAndAddMarkers() {
if (!geocoder) return;
const bounds = new google.maps.LatLngBounds();
let geocodedCount = 0;
// Take first 10 orders without coords to avoid API limits
const ordersToGeocode = filteredOrders.slice(0, 10).filter(o => {
const city = o.city || o['עיר'] || '';
const address = o.address || o['כתובת - שם רחוב ומספר בית'] || '';
return (city || address) && !(o.latitude || o.lat);
});
if (ordersToGeocode.length === 0) return;
document.getElementById('mapOrdersCount').textContent = `(טוען...)`;
ordersToGeocode.forEach((order, index) => {
const city = order.city || order['עיר'] || '';
const address = order.address || order['כתובת - שם רחוב ומספר בית'] || '';
const fullAddress = `${address}, ${city}, Israel`;
// Delay geocoding to avoid rate limits
setTimeout(() => {
geocoder.geocode({ address: fullAddress }, (results, status) => {
geocodedCount++;
if (status === 'OK' && results[0]) {
const position = results[0].geometry.location;
const marker = createOrderMarker(order, position);
markers.push(marker);
bounds.extend(position);
if (geocodedCount === ordersToGeocode.length) {
document.getElementById('mapOrdersCount').textContent = `(${markers.length})`;
if (markers.length > 0) {
map.fitBounds(bounds);
if (markers.length === 1) map.setZoom(14);
}
}
}
});
}, index * 200); // 200ms delay between requests
});
}
function createOrderMarker(order, position) {
const isAssigned = order.WorkerAssigned || order['Worker Assigned'] || false;
const status = order.OrderStatus || order['Order Status'] || 'NEW ORDER';
// Marker color based on status
let markerColor = '#f59e0b'; // Orange - new
if (isAssigned || status === 'Worker Assigend') {
markerColor = '#10b981'; // Green - assigned
} else if (status === 'Order Completed') {
markerColor = '#3b82f6'; // Blue - completed
} else if (status === 'Order Cancelled') {
markerColor = '#ef4444'; // Red - cancelled
}
const marker = new google.maps.Marker({
position: position,
map: map,
title: order.Name || 'הזמנה',
icon: {
path: google.maps.SymbolPath.CIRCLE,
scale: 10,
fillColor: markerColor,
fillOpacity: 1,
strokeColor: '#ffffff',
strokeWeight: 2
}
});
// Info window content
const date = order.date || order['בחרו תאריך לביצוע הניקיון'] || '';
const time = order.time || order['בחרו שעה לניקיון'] || '';
const city = order.city || order['עיר'] || '';
const address = order.address || order['כתובת - שם רחוב ומספר בית'] || '';
const price = order.price || order['total-price'] || 0;
const orderId = order.order_id || order.id?.slice(-6) || '';
const workerNames = getWorkerNames(order.Workers || []);
const infoContent = `
#${orderId} - ${order.Name || 'ללא שם'}
???? ${formatDate(date)} ${time}
???? ${address}, ${city}
???? ${price}₪
${workerNames ? `
???? ${workerNames}
` : ''}
פתח הזמנה
`;
marker.addListener('click', () => {
infoWindow.setContent(infoContent);
infoWindow.open(map, marker);
});
return marker;
}
// ============================================
// DATA LOADING - SINGLE REQUEST
// ============================================
async function loadAllData() {
const btn = document.getElementById('refreshBtn');
btn.classList.add('loading');
document.getElementById('ordersList').innerHTML = '
';
try {
// Single request to get all data
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'getAllData' })
});
const data = await response.json();
console.log('Response:', data);
// Check response format
if (data && data.orders && Array.isArray(data.orders)) {
// getAllData response: { orders: [], workers: [], schedules: [] }
orders = data.orders;
workers = data.workers || [];
schedules = data.schedules || [];
console.log(`getAllData: ${orders.length} orders, ${workers.length} workers, ${schedules.length} schedules`);
} else if (Array.isArray(data)) {
// Fallback: getOrders response (just array of orders)
orders = data;
workers = [];
schedules = [];
console.log(`getOrders fallback: ${orders.length} orders`);
} else {
throw new Error('Invalid response format');
}
// Render everything
filterOrders();
filteredWorkers = [...workers];
renderWorkers();
updateStats();
renderCalendar();
if (mapVisible && map) {
updateMapMarkers();
}
} catch (error) {
console.error('Error loading data:', error);
showToast('שגיאה בטעינת נתונים', 'error');
document.getElementById('ordersList').innerHTML = '
שגיאה בטעינת נתוניםבדוק את החיבור ל-n8n
';
}
btn.classList.remove('loading');
}
// NO SEPARATE FALLBACK CALLS - only single request above
// Old separate load functions removed - now using getAllData
// ============================================
// FILTERING & SORTING
// ============================================
function filterOrders() {
const search = document.getElementById('searchInput').value.toLowerCase();
const city = document.getElementById('cityFilter').value;
const status = document.getElementById('statusFilter').value;
// Get today and week dates for comparison
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const weekFromNow = new Date(todayDate.getTime() + 7 * 24 * 60 * 60 * 1000);
filteredOrders = orders.filter(order => {
// Search filter
if (search) {
const name = (order.Name || '').toLowerCase();
const phone = (order.phone || '').toLowerCase();
const orderId = (order.order_id || '').toString().toLowerCase();
if (!name.includes(search) && !phone.includes(search) && !orderId.includes(search)) {
return false;
}
}
// City filter
if (city && order.city !== city && order['עיר'] !== city) {
return false;
}
// Status filter
if (status && order.OrderStatus !== status && order['Order Status'] !== status) {
return false;
}
// Tab filter
const orderDateStr = order.date || order['בחרו תאריך לביצוע הניקיון'] || '';
const orderDate = parseDateString(orderDateStr);
const isAssigned = order.WorkerAssigned || order['Worker Assigned'] || false;
switch (currentTab) {
case 'unassigned':
return !isAssigned;
case 'assigned':
return isAssigned;
case 'today':
return orderDate.toDateString() === todayDate.toDateString();
case 'week':
return orderDate >= todayDate && orderDate <= weekFromNow;
default:
return true;
}
});
// Sort - UNASSIGNED FIRST, then by date (newest first)
filteredOrders.sort((a, b) => {
const aAssigned = a.WorkerAssigned || a['Worker Assigned'] || false;
const bAssigned = b.WorkerAssigned || b['Worker Assigned'] || false;
// Unassigned orders always first
if (!aAssigned && bAssigned) return -1;
if (aAssigned && !bAssigned) return 1;
// Then sort by date - parse D/M/YYYY format
const dateA = parseDateString(a.date || a['בחרו תאריך לביצוע הניקיון'] || '');
const dateB = parseDateString(b.date || b['בחרו תאריך לביצוע הניקיון'] || '');
if (sortOrder === 'desc') {
return dateB - dateA; // Newest first
} else {
return dateA - dateB; // Oldest first
}
});
}
// Parse date string in D/M/YYYY or DD/MM/YYYY format
function parseDateString(dateStr) {
if (!dateStr) return new Date(0);
// Try D/M/YYYY or DD/MM/YYYY format (Israeli format)
const parts = dateStr.split('/');
if (parts.length === 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // JS months are 0-indexed
const year = parseInt(parts[2], 10);
return new Date(year, month, day);
}
// Try ISO format YYYY-MM-DD
if (dateStr.includes('-')) {
return new Date(dateStr);
}
return new Date(dateStr);
}
renderOrders();
updateTabCounts();
// Update map if visible
if (mapVisible && map) {
updateMapMarkers();
}
}
function toggleSort() {
sortOrder = sortOrder === 'desc' ? 'asc' : 'desc';
document.getElementById('sortLabel').textContent = sortOrder === 'desc' ? 'תאריך ↓' : 'תאריך ↑';
document.getElementById('sortBtn').classList.toggle('active', sortOrder === 'asc');
filterOrders();
}
function setTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`.tab[data-tab="${tab}"]`).classList.add('active');
filterOrders();
}
function filterWorkers() {
const search = document.getElementById('workerSearchInput').value.toLowerCase();
filteredWorkers = workers.filter(w => {
const name = (w.Name || '').toLowerCase();
const phone = (w.phone || '').toLowerCase();
return name.includes(search) || phone.includes(search);
});
renderWorkers();
}
// ============================================
// RENDERING
// ============================================
function renderOrders() {
const container = document.getElementById('ordersList');
if (filteredOrders.length === 0) {
container.innerHTML = `
`;
document.getElementById('ordersCount').textContent = '0 תוצאות';
return;
}
document.getElementById('ordersCount').textContent = `${filteredOrders.length} תוצאות`;
container.innerHTML = filteredOrders.map(order => {
const status = statusMap[order.OrderStatus || order['Order Status']] || statusMap['NEW ORDER'];
const isAssigned = order.WorkerAssigned || order['Worker Assigned'] || false;
const workerNames = getWorkerNames(order.Workers || []);
const phone = order.phone || '';
const date = order.date || order['בחרו תאריך לביצוע הניקיון'] || '';
const time = order.time || order['בחרו שעה לניקיון'] || '';
const city = order.city || order['עיר'] || '';
const price = order.price || order['total-price'] || 0;
const orderId = order.order_id || order.id?.slice(-6) || '';
return `
${order.Name || 'ללא שם'}
${formatDate(date)} ${time}
${isAssigned && workerNames ? `
???? ${workerNames}
` : ''}
`;
}).join('');
}
function renderWorkers() {
const container = document.getElementById('workersList');
document.getElementById('workersCount').textContent = workers.length;
if (filteredWorkers.length === 0) {
container.innerHTML = '
אין עובדים
';
return;
}
container.innerHTML = filteredWorkers.map(worker => {
const initial = (worker.Name || '?')[0].toUpperCase();
const ordersCount = (worker.Orders || []).length;
return `
${initial}
${worker.Name || 'ללא שם'}
${ordersCount} הזמנות
`;
}).join('');
}
function renderCalendar() {
const grid = document.getElementById('calendarGrid');
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
document.getElementById('calendarMonth').textContent = `${hebrewMonths[month]} ${year}`;
// Day names
let html = hebrewDays.map(d => `
${d}
`).join('');
// First day of month
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date().toISOString().split('T')[0];
// Previous month days
const prevMonthDays = new Date(year, month, 0).getDate();
for (let i = firstDay - 1; i >= 0; i--) {
html += `
${prevMonthDays - i}
`;
}
// Current month days
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isToday = dateStr === today;
const hasOrders = orders.some(o => (o.date || o['בחרו תאריך לביצוע הניקיון']) === dateStr);
html += `
${day}
`;
}
// Next month days
const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7;
for (let i = 1; i <= totalCells - firstDay - daysInMonth; i++) {
html += `
${i}
`;
}
grid.innerHTML = html;
}
function changeMonth(delta) {
currentMonth.setMonth(currentMonth.getMonth() + delta);
renderCalendar();
}
function filterByDate(dateStr) {
document.getElementById('searchInput').value = dateStr;
filterOrders();
}
// ============================================
// MODALS
// ============================================
function openOrderModal(orderId) {
selectedOrder = orders.find(o => o.id === orderId);
if (!selectedOrder) return;
selectedWorkerForAssign = null;
const modal = document.getElementById('orderModal');
const body = document.getElementById('modalBody');
const status = statusMap[selectedOrder.OrderStatus || selectedOrder['Order Status']] || statusMap['NEW ORDER'];
const phone = selectedOrder.phone || '';
const date = selectedOrder.date || selectedOrder['בחרו תאריך לביצוע הניקיון'] || '';
const time = selectedOrder.time || selectedOrder['בחרו שעה לניקיון'] || '';
const hours = selectedOrder.hours || selectedOrder['לכמה שעות תרצו להזמין ניקיון?'] || 4;
const city = selectedOrder.city || selectedOrder['עיר'] || '';
const address = selectedOrder.address || selectedOrder['כתובת - שם רחוב ומספר בית'] || '';
const price = selectedOrder.price || selectedOrder['total-price'] || 0;
const mapLink = selectedOrder.mapLink || selectedOrder['Google Map Link'] || '';
const orderId_display = selectedOrder.order_id || selectedOrder.id?.slice(-6) || '';
document.getElementById('modalOrderId').textContent = `הזמנה #${orderId_display}`;
// Get available workers for this date
const availableWorkers = getAvailableWorkersForDate(date);
// Get client order history (by phone or name)
const clientHistory = getClientOrderHistory(phone, selectedOrder.Name);
const otherOrders = clientHistory.filter(o => o.id !== orderId);
// Calculate client stats
const totalOrders = clientHistory.length;
const completedOrders = clientHistory.filter(o =>
(o.OrderStatus || o['Order Status']) === 'Order Completed'
).length;
const totalSpent = clientHistory.reduce((sum, o) =>
sum + (parseFloat(o.price || o['total-price']) || 0), 0
);
// Get client level and badges
const clientLevel = getClientLevel(totalOrders);
const clientBadges = getClientBadges(totalOrders, totalSpent);
const earnedBadges = clientBadges.filter(b => b.earned);
body.innerHTML = `
פרטי לקוח
${clientLevel.title}
פרטי הזמנה נוכחית
${status.label} תאריך
${formatDate(date)} ${time}
${mapLink ? `
` : ''}
${otherOrders.length > 0 ? `
היסטוריית הזמנות (${otherOrders.length})
${otherOrders.slice(0, 5).map(order => {
const oStatus = statusMap[order.OrderStatus || order['Order Status']] || statusMap['NEW ORDER'];
const oDate = order.date || order['בחרו תאריך לביצוע הניקיון'] || '';
const oPrice = order.price || order['total-price'] || 0;
const oCity = order.city || order['עיר'] || '';
const oId = order.order_id || order.id?.slice(-6) || '';
return `
#${oId} ${formatDate(oDate)} ${oCity}
${oPrice}₪ ${oStatus.label}
`;
}).join('')}
${otherOrders.length > 5 ? `
+ עוד ${otherOrders.length - 5} הזמנות
` : ''}
` : ''}
שיבוץ עובד
${availableWorkers.length} זמינים
${renderAssignWorkers(availableWorkers)}
`;
modal.classList.add('open');
}
function renderAssignWorkers(availableWorkers, searchTerm = '') {
let filteredList = workers;
if (searchTerm) {
filteredList = workers.filter(w =>
(w.Name || '').toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filteredList.map(w => {
const isAvailable = availableWorkers.includes(w.id);
const ordersCount = (w.Orders || []).length;
return `
${(w.Name || '?')[0].toUpperCase()}
${w.Name}
${ordersCount} הזמנות • ${isAvailable ? 'זמין' : 'לא זמין'}
${isAvailable ? '
✓
' : ''}
`;
}).join('');
}
function filterAssignWorkers(searchTerm) {
const date = selectedOrder?.date || selectedOrder?.['בחרו תאריך לביצוע הניקיון'] || '';
const availableWorkers = getAvailableWorkersForDate(date);
document.getElementById('assignWorkersGrid').innerHTML = renderAssignWorkers(availableWorkers, searchTerm);
}
function getClientOrderHistory(phone, name) {
if (!phone && !name) return [];
return orders.filter(order => {
const orderPhone = order.phone || '';
const orderName = order.Name || '';
// Match by phone (primary) or by exact name match
if (phone && orderPhone) {
const cleanPhone1 = phone.replace(/[^0-9]/g, '');
const cleanPhone2 = orderPhone.replace(/[^0-9]/g, '');
if (cleanPhone1 === cleanPhone2) return true;
}
if (name && orderName && name.toLowerCase() === orderName.toLowerCase()) {
return true;
}
return false;
}).sort((a, b) => {
const dateA = a.date || a['בחרו תאריך לביצוע הניקיון'] || '';
const dateB = b.date || b['בחרו תאריך לביצוע הניקיון'] || '';
return dateB.localeCompare(dateA); // Newest first
});
}
function getClientBadges(totalOrders, totalSpent) {
return [
{
svg: '
',
name: 'לקוח חדש',
earned: totalOrders >= 1,
progress: ''
},
{
svg: '
',
name: 'לקוח חוזר',
earned: totalOrders >= 3,
progress: totalOrders >= 3 ? '' : `${totalOrders}/3`
},
{
svg: '
',
name: 'לקוח VIP',
earned: totalOrders >= 10,
progress: totalOrders >= 10 ? '' : `${totalOrders}/10`
},
{
svg: '
',
name: 'לקוח פלטינום',
earned: totalOrders >= 20,
progress: totalOrders >= 20 ? '' : `${totalOrders}/20`
},
{
svg: '
',
name: '1000₪',
earned: totalSpent >= 1000,
progress: totalSpent >= 1000 ? '' : `${totalSpent}/1000`
},
{
svg: '
',
name: '5000₪',
earned: totalSpent >= 5000,
progress: totalSpent >= 5000 ? '' : `${totalSpent}/5000`
}
];
}
function getClientLevel(totalOrders) {
if (totalOrders >= 20) return { level: 4, title: 'פלטינום', color: '#9333ea' };
if (totalOrders >= 10) return { level: 3, title: 'VIP', color: '#f59e0b' };
if (totalOrders >= 3) return { level: 2, title: 'חוזר', color: '#10b981' };
return { level: 1, title: 'חדש', color: '#6b7280' };
}
function closeOrderModal() {
document.getElementById('orderModal').classList.remove('open');
selectedOrder = null;
selectedWorkerForAssign = null;
}
function selectWorkerForAssign(workerId, element) {
selectedWorkerForAssign = workerId;
document.querySelectorAll('.assign-worker-btn').forEach(btn => btn.classList.remove('selected'));
element.classList.add('selected');
}
async function assignSelectedWorker() {
if (!selectedOrder || !selectedWorkerForAssign) {
showToast('בחר עובד לשיבוץ', 'error');
return;
}
const btn = document.getElementById('modalAssignBtn');
btn.disabled = true;
btn.textContent = 'משבץ...';
try {
await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'assignWorker',
orderId: selectedOrder.id,
workerId: selectedWorkerForAssign
})
});
showToast('העובד שובץ בהצלחה', 'success');
closeOrderModal();
loadAllData();
} catch (error) {
console.error('Error assigning worker:', error);
showToast('שגיאה בשיבוץ', 'error');
}
btn.disabled = false;
btn.textContent = 'שבץ עובד';
}
function openWorkerModal(workerId) {
const worker = workers.find(w => w.id === workerId);
if (!worker) return;
const modal = document.getElementById('workerModal');
const body = document.getElementById('workerModalBody');
const whatsappBtn = document.getElementById('workerWhatsappBtn');
const initial = (worker.Name || '?')[0].toUpperCase();
const phone = worker.phone || '';
const workerOrders = orders.filter(o => (o.Workers || []).includes(workerId));
const completedOrders = workerOrders.filter(o =>
(o.OrderStatus || o['Order Status']) === 'Order Completed'
);
// Calculate stats (will come from Airtable later)
const totalOrders = workerOrders.length;
const totalCompleted = completedOrders.length;
const totalHours = completedOrders.reduce((sum, o) => sum + (parseFloat(o.hours) || 4), 0);
const totalEarnings = totalHours * 65; // 65₪ per hour
const avgRating = 4.8; // Will come from Airtable
const thisMonthOrders = completedOrders.filter(o => {
const d = o.date || o['בחרו תאריך לביצוע הניקיון'] || '';
const orderDate = parseDateString(d);
const now = new Date();
return orderDate.getMonth() === now.getMonth() && orderDate.getFullYear() === now.getFullYear();
}).length;
const thisMonthHours = thisMonthOrders * 4;
const thisMonthEarnings = thisMonthHours * 65;
// Calculate level
const level = Math.floor(totalCompleted / 10) + 1;
const levelProgress = (totalCompleted % 10) * 10;
const nextLevelOrders = 10 - (totalCompleted % 10);
// Check badges
const badges = getWorkerBadges(totalCompleted, totalHours, totalEarnings, avgRating);
// Check if worker of the month (most orders this month)
const isWorkerOfMonth = checkWorkerOfMonth(workerId);
whatsappBtn.href = `https://wa.me/${phone.replace(/[^0-9]/g, '')}`;
body.innerHTML = `
${isWorkerOfMonth ? `
עובדת החודש!
הכי הרבה שעות החודש
` : ''}
${initial}
${worker.Name || 'ללא שם'}
מנקה מקצועית
${totalCompleted}
הזמנות הושלמו
${totalEarnings}₪
סה״כ הכנסות
${totalCompleted % 10}/10 הזמנות עוד ${nextLevelOrders} לרמה הבאה
${badges.map(b => `
${b.svg}
${b.name}
${!b.earned ? `
${b.progress}
` : ''}
`).join('')}
${thisMonthOrders}
הזמנות
${thisMonthEarnings}₪
הכנסות
`;
modal.classList.add('open');
}
function getWorkerBadges(orders, hours, earnings, rating) {
return [
{
svg: '
',
name: 'מתחילה',
earned: orders >= 1,
progress: orders >= 1 ? '' : '0/1'
},
{
svg: '
',
name: '10 הזמנות',
earned: orders >= 10,
progress: orders >= 10 ? '' : `${orders}/10`
},
{
svg: '
',
name: '50 הזמנות',
earned: orders >= 50,
progress: orders >= 50 ? '' : `${orders}/50`
},
{
svg: '
',
name: '100 הזמנות',
earned: orders >= 100,
progress: orders >= 100 ? '' : `${orders}/100`
},
{
svg: '
',
name: '100 שעות',
earned: hours >= 100,
progress: hours >= 100 ? '' : `${hours}/100`
},
{
svg: '
',
name: '5000₪',
earned: earnings >= 5000,
progress: earnings >= 5000 ? '' : `${earnings}/5000`
},
{
svg: '
',
name: 'דירוג 5',
earned: rating >= 4.9,
progress: rating >= 4.9 ? '' : `${rating}/5`
},
{
svg: '
',
name: 'עובדת החודש',
earned: checkWorkerOfMonth(null),
progress: ''
}
];
}
function getLevelTitle(level) {
const titles = {
1: 'מתחילה',
2: 'מתקדמת',
3: 'מקצועית',
4: 'מומחית',
5: 'אלופה',
6: 'מאסטר',
7: 'אגדה',
8: 'סופר סטאר',
9: 'אייקון',
10: 'מלכה'
};
return titles[Math.min(level, 10)] || 'מלכה';
}
function checkWorkerOfMonth(workerId) {
// Calculate who has most hours this month
const thisMonth = new Date().toISOString().slice(0, 7);
const monthlyStats = {};
orders.forEach(order => {
const date = order.date || order['בחרו תאריך לביצוע הניקיון'] || '';
if (!date.startsWith(thisMonth)) return;
if ((order.OrderStatus || order['Order Status']) !== 'Order Completed') return;
const workerIds = order.Workers || [];
workerIds.forEach(wId => {
if (!monthlyStats[wId]) monthlyStats[wId] = 0;
monthlyStats[wId] += parseFloat(order.hours) || 4;
});
});
const topWorker = Object.entries(monthlyStats).sort((a, b) => b[1] - a[1])[0];
if (workerId) {
return topWorker && topWorker[0] === workerId;
}
return topWorker ? topWorker[0] : null;
}
// Download profile as image
async function downloadProfileImage(elementId, name) {
try {
showToast('מכין תמונה...', '');
// Use html2canvas library
if (typeof html2canvas === 'undefined') {
// Load library dynamically
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
script.onload = () => captureAndDownload(elementId, name);
document.head.appendChild(script);
} else {
captureAndDownload(elementId, name);
}
} catch (error) {
console.error('Error downloading image:', error);
showToast('שגיאה בשמירת תמונה', 'error');
}
}
function captureAndDownload(elementId, name) {
const element = document.getElementById(elementId);
html2canvas(element, {
backgroundColor: '#f1f5f9',
scale: 2
}).then(canvas => {
const link = document.createElement('a');
link.download = `${name || 'profile'}-cleaner4you.png`;
link.href = canvas.toDataURL('image/png');
link.click();
showToast('התמונה נשמרה!', 'success');
});
}
// Share profile via WhatsApp
function shareProfileWhatsApp(phone, name, type) {
const message = type === 'worker'
? `היי ${name}! ????\nצפי בהישגים שלך ב-Cleaner4You!\nאת עושה עבודה מדהימה! ????`
: `היי ${name}! ????\nתודה שאתה לקוח של Cleaner4You!\nאנחנו מעריכים אותך! ❤️`;
const url = `https://wa.me/${phone.replace(/[^0-9]/g, '')}?text=${encodeURIComponent(message)}`;
window.open(url, '_blank');
}
function closeWorkerModal() {
document.getElementById('workerModal').classList.remove('open');
}
function openClientProfileModal(phone, name) {
const modal = document.getElementById('clientModal');
const body = document.getElementById('clientModalBody');
const clientHistory = getClientOrderHistory(phone, name);
const totalOrders = clientHistory.length;
const completedOrders = clientHistory.filter(o =>
(o.OrderStatus || o['Order Status']) === 'Order Completed'
).length;
const totalSpent = clientHistory.reduce((sum, o) =>
sum + (parseFloat(o.price || o['total-price']) || 0), 0
);
const totalHours = clientHistory.reduce((sum, o) =>
sum + (parseFloat(o.hours) || 4), 0
);
const clientLevel = getClientLevel(totalOrders);
const clientBadges = getClientBadges(totalOrders, totalSpent);
const initial = (name || '?')[0].toUpperCase();
// Level progress
const levelThresholds = [1, 3, 10, 20];
const currentThreshold = levelThresholds[clientLevel.level - 1] || 1;
const nextThreshold = levelThresholds[clientLevel.level] || 50;
const levelProgress = Math.min(100, ((totalOrders - currentThreshold) / (nextThreshold - currentThreshold)) * 100);
body.innerHTML = `
${initial}
${name || 'לקוח'}
לקוח ${clientLevel.title}
${completedOrders}
הושלמו
${totalOrders} הזמנות ${clientLevel.level < 4 ? `עוד ${nextThreshold - totalOrders} לרמה הבאה` : 'רמה מקסימלית!'}
${clientBadges.map(b => `
${b.svg}
${b.name}
${!b.earned && b.progress ? `
${b.progress}
` : ''}
`).join('')}
${totalHours}
שעות ניקיון
${Math.round(totalSpent / Math.max(totalOrders, 1))}₪
ממוצע להזמנה
`;
modal.classList.add('open');
}
function closeClientModal() {
document.getElementById('clientModal').classList.remove('open');
}
// ============================================
// HELPERS
// ============================================
function updateStats() {
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const total = orders.length;
const unassigned = orders.filter(o => !(o.WorkerAssigned || o['Worker Assigned'])).length;
const assigned = orders.filter(o => (o.WorkerAssigned || o['Worker Assigned'])).length;
const todayOrders = orders.filter(o => {
const orderDate = parseDateString(o.date || o['בחרו תאריך לביצוע הניקיון'] || '');
return orderDate.toDateString() === todayDate.toDateString();
}).length;
document.getElementById('statTotal').textContent = total;
document.getElementById('statUnassigned').textContent = unassigned;
document.getElementById('statAssigned').textContent = assigned;
document.getElementById('statToday').textContent = todayOrders;
}
function updateTabCounts() {
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const weekFromNow = new Date(todayDate.getTime() + 7 * 24 * 60 * 60 * 1000);
document.getElementById('tabAll').textContent = orders.length;
document.getElementById('tabUnassigned').textContent = orders.filter(o => !(o.WorkerAssigned || o['Worker Assigned'])).length;
document.getElementById('tabAssigned').textContent = orders.filter(o => (o.WorkerAssigned || o['Worker Assigned'])).length;
document.getElementById('tabToday').textContent = orders.filter(o => {
const orderDate = parseDateString(o.date || o['בחרו תאריך לביצוע הניקיון'] || '');
return orderDate.toDateString() === todayDate.toDateString();
}).length;
document.getElementById('tabWeek').textContent = orders.filter(o => {
const orderDate = parseDateString(o.date || o['בחרו תאריך לביצוע הניקיון'] || '');
return orderDate >= todayDate && orderDate <= weekFromNow;
}).length;
}
function getWorkerNames(workerIds) {
if (!workerIds || workerIds.length === 0) return '';
return workerIds.map(id => {
const w = workers.find(w => w.id === id);
return w ? w.Name : '';
}).filter(Boolean).join(', ');
}
function getAvailableWorkersForDate(dateStr) {
// If no schedules loaded, return all workers as available
if (!schedules || schedules.length === 0) {
console.log('No schedules, returning all workers');
return workers.map(w => w.id);
}
// Parse the order date
const orderDate = new Date(dateStr);
const dayOfWeek = orderDate.getDay(); // 0=Sunday, 1=Monday...
// Find the week string (format: "01-07/01" or similar)
// For now, get current week's schedules
const currentWeekSchedules = schedules; // You can filter by Week field if needed
const availableIds = [];
currentWeekSchedules.forEach(schedule => {
const scheduleOptions = schedule.Schedule || [];
const workerIds = schedule.Workers || [];
// Check if worker has shifts (not just Free Day)
const hasWorkingShifts = scheduleOptions.some(opt =>
opt.includes('Morning') ||
opt.includes('Afternoon') ||
opt.includes('All Day') ||
opt.includes('8:00') ||
opt.includes('14:00')
);
const isFreeDay = scheduleOptions.some(opt =>
opt.toLowerCase().includes('free') ||
opt.includes('חופש') ||
opt.includes('יום חופשי')
);
if (hasWorkingShifts && !isFreeDay) {
// Add all workers from this schedule
workerIds.forEach(wId => {
if (!availableIds.includes(wId)) {
availableIds.push(wId);
}
});
}
});
console.log(`Available workers for ${dateStr}:`, availableIds.length);
// If no available workers found in schedules, return all as fallback
if (availableIds.length === 0) {
return workers.map(w => w.id);
}
return availableIds;
}
function getWorkerSchedule(workerId) {
// Return array of 6 days with availability status
// This is simplified - adjust based on your data structure
return ['available', 'available', 'off', 'available', 'busy', 'off'];
}
function formatDate(dateStr) {
if (!dateStr) return '';
// Already in D/M/YYYY format - just return D/M
if (dateStr.includes('/')) {
const parts = dateStr.split('/');
return `${parts[0]}/${parts[1]}`; // D/M
}
// ISO format YYYY-MM-DD
if (dateStr.includes('-')) {
const [year, month, day] = dateStr.split('-');
return `${parseInt(day)}/${parseInt(month)}`; // D/M
}
return dateStr;
}
function showToast(message, type = '') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}