diff --git a/api/create_event.php b/api/create_event.php new file mode 100644 index 0000000..d57ff1e --- /dev/null +++ b/api/create_event.php @@ -0,0 +1,36 @@ +prepare("INSERT INTO events (user_id, lead_id, title, start_time, end_time) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$user_id, $lead_id, $title, $start_datetime, $end_datetime]); + + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } + } else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method not allowed']); +} diff --git a/api/delete_event.php b/api/delete_event.php new file mode 100644 index 0000000..e337085 --- /dev/null +++ b/api/delete_event.php @@ -0,0 +1,29 @@ +prepare("DELETE FROM events WHERE id = ? AND user_id = ?"); + $stmt->execute([$id, $user_id]); + + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } + } else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method not allowed']); +} diff --git a/api/get_events.php b/api/get_events.php new file mode 100644 index 0000000..e393e04 --- /dev/null +++ b/api/get_events.php @@ -0,0 +1,17 @@ +prepare("SELECT * FROM events WHERE user_id = ?"); + $stmt->execute([$user_id]); + $events = $stmt->fetchAll(); + + echo json_encode($events); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} diff --git a/api/get_events_for_day.php b/api/get_events_for_day.php new file mode 100644 index 0000000..37dc8d1 --- /dev/null +++ b/api/get_events_for_day.php @@ -0,0 +1,24 @@ + false, 'message' => 'Invalid input']); + exit; +} + +try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT events.*, leads.name as lead_name FROM events LEFT JOIN leads ON events.lead_id = leads.id WHERE events.user_id = ? AND DATE(events.start_time) = ? ORDER BY events.start_time ASC"); + $stmt->execute([$user_id, $date]); + $events = $stmt->fetchAll(); + + echo json_encode($events); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} diff --git a/api/get_messages.php b/api/get_messages.php new file mode 100644 index 0000000..652b695 --- /dev/null +++ b/api/get_messages.php @@ -0,0 +1,21 @@ +prepare("SELECT * FROM messages WHERE lead_id = ? ORDER BY created_at ASC"); + $stmt->execute([$lead_id]); + $messages = $stmt->fetchAll(); + + echo json_encode($messages); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); +} diff --git a/api/send_message.php b/api/send_message.php new file mode 100644 index 0000000..d53d2e6 --- /dev/null +++ b/api/send_message.php @@ -0,0 +1,85 @@ + $full_prompt); + + // Extract the response from the tool's output + // The exact structure of the response may vary. + $ai_message = $api_response['answer'] ?? 'Sorry, I could not get a response from the AI.'; + + return $ai_message; + */ + + // For now, return a simulated response that includes the prompt. + return "(Simulated AI Response with API Key. Based on prompt: '" . $prompt . "')"; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + + if (isset($data['lead_id']) && isset($data['message'])) { + $lead_id = $data['lead_id']; + $message = $data['message']; + + try { + $pdo = db(); + + // Save user message + $stmt = $pdo->prepare("INSERT INTO messages (lead_id, sender, message) VALUES (?, 'user', ?)"); + $stmt->execute([$lead_id, $message]); + $user_message_id = $pdo->lastInsertId(); + + // Fetch settings + $stmt = $pdo->query("SELECT * FROM settings"); + $settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + $apiKey = $settings['api_key'] ?? ''; + $aiPrompt = $settings['ai_prompt'] ?? 'You are a helpful assistant.'; + + // Fetch conversation history + $stmt = $pdo->prepare("SELECT * FROM messages WHERE lead_id = ? ORDER BY created_at ASC"); + $stmt->execute([$lead_id]); + $conversation_history = $stmt->fetchAll(); + + // Get AI response + $ai_message = get_gemini_response($apiKey, $aiPrompt, $conversation_history, $message); + + $stmt = $pdo->prepare("INSERT INTO messages (lead_id, sender, message) VALUES (?, 'ai', ?)"); + $stmt->execute([$lead_id, $ai_message]); + $ai_message_id = $pdo->lastInsertId(); + + // Fetch the created messages to return them + $stmt = $pdo->prepare("SELECT * FROM messages WHERE id IN (?, ?)"); + $stmt->execute([$user_message_id, $ai_message_id]); + $new_messages = $stmt->fetchAll(); + + echo json_encode(['success' => true, 'messages' => $new_messages]); + + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } + } else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method not allowed']); +} diff --git a/api/update_event.php b/api/update_event.php new file mode 100644 index 0000000..fd789b7 --- /dev/null +++ b/api/update_event.php @@ -0,0 +1,37 @@ +prepare("UPDATE events SET lead_id = ?, title = ?, start_time = ?, end_time = ? WHERE id = ? AND user_id = ?"); + $stmt->execute([$lead_id, $title, $start_datetime, $end_datetime, $id, $user_id]); + + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } + } else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method not allowed']); +} diff --git a/api/update_lead_status.php b/api/update_lead_status.php new file mode 100644 index 0000000..08f7aab --- /dev/null +++ b/api/update_lead_status.php @@ -0,0 +1,28 @@ +prepare("UPDATE leads SET status = ? WHERE id = ?"); + $stmt->execute([$new_status, $lead_id]); + + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); + } + } else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method not allowed']); +} diff --git a/api/webhook.php b/api/webhook.php new file mode 100644 index 0000000..b8af506 --- /dev/null +++ b/api/webhook.php @@ -0,0 +1,47 @@ +prepare("SELECT * FROM leads WHERE company = ?"); // Using company to store phone number for simplicity + $stmt->execute([$phone_number]); + $lead = $stmt->fetch(); + + if (!$lead) { + // Create new lead + $stmt = $pdo->prepare("INSERT INTO leads (name, company, status) VALUES (?, ?, 'New')"); + $stmt->execute([$name, $phone_number]); + $lead_id = $pdo->lastInsertId(); + } else { + $lead_id = $lead['id']; + } + + // Save the message + $stmt = $pdo->prepare("INSERT INTO messages (lead_id, sender, message) VALUES (?, 'user', ?)"); + $stmt->execute([$lead_id, $message_body]); + + } catch (PDOException $e) { + // Log errors + file_put_contents('webhook_error_log.txt', $e->getMessage() . "\n", FILE_APPEND); + } +} + diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..d88a4b9 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,273 @@ +/* General */ +body { + background-color: #F3F4F6; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: #111827; +} + +.main-container { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 260px; + background-color: #FFFFFF; + border-right: 1px solid #E5E7EB; + padding: 24px; + display: flex; + flex-direction: column; +} + +.sidebar .logo { + font-weight: 700; + font-size: 1.5rem; + color: #4F46E5; + margin-bottom: 2rem; +} + +.sidebar .nav-link { + color: #374151; + font-size: 1rem; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + display: flex; + align-items: center; + margin-bottom: 0.5rem; + transition: all 0.2s ease-in-out; +} + +.sidebar .nav-link .icon { + margin-right: 0.75rem; + width: 24px; + height: 24px; +} + +.sidebar .nav-link.active, +.sidebar .nav-link:hover { + background-color: #EEF2FF; + color: #4F46E5; +} + +/* Main Content */ +.main-content { + flex-grow: 1; + padding: 32px 56px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 700; +} + +/* Kanban Board */ +.kanban-board { + display: flex; + gap: 2rem; + width: 100%; + overflow-x: auto; + padding-bottom: 1.5rem; +} + +.kanban-column { + flex: 1 0 320px; + background-color: #F9FAFB; + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.kanban-column h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #E5E7EB; + display: flex; + align-items: center; +} + +.kanban-column .count { + margin-left: 0.75rem; + background-color: #E5E7EB; + color: #4B5563; + font-size: 0.875rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 9999px; +} + +.kanban-cards { + min-height: 200px; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.kanban-card { + background-color: #FFFFFF; + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + cursor: grab; + border-left: 4px solid transparent; + transition: all 0.2s ease-in-out; +} + +.kanban-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.kanban-card:active { + cursor: grabbing; + background-color: #F3F4F6; +} + +.kanban-card.dragging { + opacity: 0.6; + transform: rotate(3deg); +} + +.kanban-card .card-title { + font-weight: 600; + font-size: 1.125rem; + margin-bottom: 0.5rem; +} + +.kanban-card .card-subtitle { + font-size: 0.875rem; + color: #6B7280; + margin-bottom: 1rem; +} + +.kanban-card .card-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: #9CA3AF; +} + +.kanban-card .tag { + padding: 4px 12px; + border-radius: 9999px; + font-weight: 600; + font-size: 0.75rem; +} + +.tag-prospect { + background-color: #DBEAFE; + color: #2563EB; +} + +.tag-deal { + background-color: #D1FAE5; + color: #059669; +} + +.kanban-column.drag-over { + background-color: #EEF2FF; + border: 2px dashed #C7D2FE; +} + +/* Forms */ +.form-control:focus { + border-color: #4F46E5; + box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25); +} + +/* Modals */ +.modal-content { + border-radius: 0.75rem; +} + +/* Calendar */ +.event-marker { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #4F46E5; + position: absolute; + top: 5px; + right: 5px; +} + +.jsCalendar-cell { + position: relative; +} + +/* Chat Modal */ +.chat-container { + display: flex; + flex-direction: column; + height: 450px; +} + +.chat-messages { + flex-grow: 1; + overflow-y: auto; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.message { + padding: 0.75rem 1.25rem; + border-radius: 1.25rem; + max-width: 80%; + line-height: 1.5; +} + +.message p { + margin: 0; +} + +.message .timestamp { + font-size: 0.75rem; + color: #9CA3AF; + margin-top: 0.25rem; + display: block; +} + +.message.received { + background-color: #F3F4F6; + align-self: flex-start; +} + +.message.sent { + background-color: #4F46E5; + color: #FFFFFF; + align-self: flex-end; +} + +.message.sent .timestamp { + color: #E0E7FF; +} + +.chat-input { + display: flex; + padding: 1.5rem; + border-top: 1px solid #E5E7EB; +} + +.chat-input input { + flex-grow: 1; + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.chat-input button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..65f17b8 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,161 @@ +document.addEventListener('DOMContentLoaded', () => { + const cards = document.querySelectorAll('.kanban-card'); + const columns = document.querySelectorAll('.kanban-cards'); + const leadModal = new bootstrap.Modal(document.getElementById('leadModal')); + const chatTab = document.getElementById('chat-tab'); + const chatMessages = document.querySelector('.chat-messages'); + const chatInput = document.querySelector('.chat-input input'); + const chatSendButton = document.querySelector('.chat-input button'); + let currentLeadId = null; + + cards.forEach(card => { + card.addEventListener('dragstart', () => { + card.classList.add('dragging'); + }); + + card.addEventListener('dragend', () => { + card.classList.remove('dragging'); + }); + + card.addEventListener('click', () => { + const name = card.dataset.name; + const company = card.dataset.company; + const value = card.dataset.value; + const status = card.dataset.status; + currentLeadId = card.dataset.id; + + document.getElementById('leadName').textContent = name; + document.getElementById('leadCompany').textContent = company; + document.getElementById('leadValue').textContent = '$' + value; + document.getElementById('leadStatus').textContent = status; + document.getElementById('leadModalLabel').textContent = name; + + leadModal.show(); + }); + }); + + columns.forEach(column => { + column.addEventListener('dragover', e => { + e.preventDefault(); + const afterElement = getDragAfterElement(column, e.clientY); + const draggable = document.querySelector('.dragging'); + if (afterElement == null) { + column.appendChild(draggable); + } else { + column.insertBefore(draggable, afterElement); + } + column.parentElement.classList.add('drag-over'); + }); + + column.addEventListener('dragleave', () => { + column.parentElement.classList.remove('drag-over'); + }); + + column.addEventListener('drop', (e) => { + e.preventDefault(); + const draggable = document.querySelector('.dragging'); + const newStatus = column.dataset.status; + const leadId = draggable.dataset.id; + + // Optimistically update the UI + column.appendChild(draggable); + draggable.dataset.status = newStatus; + column.parentElement.classList.remove('drag-over'); + + // Update the status in the database + fetch('api/update_lead_status.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: leadId, status: newStatus }) + }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + console.error('Failed to update lead status'); + } + }) + .catch(error => { + console.error('Error updating lead status:', error); + }); + }); + }); + + chatTab.addEventListener('shown.bs.tab', () => { + if (currentLeadId) { + fetch(`api/get_messages.php?lead_id=${currentLeadId}`) + .then(response => response.json()) + .then(messages => { + renderMessages(messages); + }); + } + }); + + chatSendButton.addEventListener('click', () => { + const message = chatInput.value.trim(); + if (message && currentLeadId) { + sendMessage(currentLeadId, message); + chatInput.value = ''; + } + }); + + chatInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + chatSendButton.click(); + } + }); + + function sendMessage(leadId, message) { + fetch('api/send_message.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ lead_id: leadId, message: message }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + renderMessages(data.messages, true); + } + }); + } + + function renderMessages(messages, append = false) { + if (!append) { + chatMessages.innerHTML = ''; + } + messages.forEach(message => { + const messageEl = document.createElement('div'); + messageEl.classList.add('message', message.sender === 'user' ? 'sent' : 'received'); + + const textEl = document.createElement('p'); + textEl.textContent = message.message; + + const timeEl = document.createElement('span'); + timeEl.classList.add('timestamp'); + const messageTime = new Date(message.created_at); + timeEl.textContent = messageTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + messageEl.appendChild(textEl); + messageEl.appendChild(timeEl); + chatMessages.appendChild(messageEl); + }); + chatMessages.scrollTop = chatMessages.scrollHeight; + } + + function getDragAfterElement(container, y) { + const draggableElements = [...container.querySelectorAll('.kanban-card:not(.dragging)')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + } +}); \ No newline at end of file diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..28fe35c --- /dev/null +++ b/auth.php @@ -0,0 +1,11 @@ +prepare("SELECT id, name FROM leads WHERE status != 'Closed'"); +$stmt->execute(); +$leads = $stmt->fetchAll(); + +?> + + +
+ + += ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWiZZy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-This page will update automatically as the plan is implemented.
-Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>