modified: .env
new file: __debug_bin.exe modified: bot.db modified: db/db.go modified: go.mod new file: handlers/auth.go modified: handlers/dashboard.go new file: handlers/saas.go modified: handlers/webhook.go modified: main.go new file: saas_bot.db modified: services/openrouter.go new file: services/types.go modified: services/whatsapp.go new file: static/style.css modified: templates/dashboard.html new file: templates/landing.html new file: templates/login.html new file: templates/register.html deleted: types/types.go
This commit is contained in:
@@ -1,434 +1,103 @@
|
||||
{{ define "dashboard.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SekiBot | Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-tertiary: #202225;
|
||||
--bg-secondary: #2f3136;
|
||||
--bg-primary: #36393f;
|
||||
--bg-input: #40444b;
|
||||
--text-normal: #dcddde;
|
||||
--text-muted: #72767d;
|
||||
--blurple: #5865F2;
|
||||
--blurple-hover: #4752c4;
|
||||
--danger: #ed4245;
|
||||
--success: #3ba55c;
|
||||
--interactive-hover: #3b3d42;
|
||||
}
|
||||
|
||||
body { margin: 0; font-family: 'Inter', sans-serif; background: var(--bg-primary); color: var(--text-normal); overflow: hidden; display: flex; height: 100vh; }
|
||||
|
||||
/* SCROLLBARS */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; background-color: var(--bg-secondary); }
|
||||
::-webkit-scrollbar-thumb { background-color: #202225; border-radius: 4px; }
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar { width: 240px; background: var(--bg-secondary); display: flex; flex-direction: column; flex-shrink: 0; }
|
||||
.sidebar-header { padding: 16px; border-bottom: 1px solid #202225; font-weight: 600; color: white; display: flex; align-items: center; justify-content: space-between; }
|
||||
.sidebar-nav { padding: 10px; border-bottom: 1px solid #202225; }
|
||||
.sidebar-scroll { flex: 1; overflow-y: auto; padding: 8px; }
|
||||
|
||||
.nav-item { padding: 10px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; font-weight: 500; }
|
||||
.nav-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
|
||||
.nav-item.active { background: rgba(79,84,92,0.6); color: white; }
|
||||
|
||||
.channel-item { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; }
|
||||
.channel-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
|
||||
.channel-item.active { background: rgba(79,84,92,0.6); color: white; }
|
||||
.channel-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.channel-actions { display: none; gap: 4px; }
|
||||
.channel-item:hover .channel-actions { display: flex; }
|
||||
|
||||
/* ICONS */
|
||||
.icon-btn { color: var(--text-muted); padding: 2px; cursor: pointer; border-radius: 3px; font-size: 0.8rem; }
|
||||
.icon-btn:hover { color: white; background: var(--bg-tertiary); }
|
||||
.icon-btn.del:hover { color: var(--danger); }
|
||||
|
||||
/* PANELS */
|
||||
.main-panel { flex: 1; display: none; flex-direction: column; background: var(--bg-primary); position: relative; height: 100vh; }
|
||||
.main-panel.show { display: flex; }
|
||||
|
||||
/* CHAT STYLES */
|
||||
.chat-header { height: 48px; border-bottom: 1px solid #26272d; display: flex; align-items: center; padding: 0 16px; font-weight: 600; color: white; box-shadow: 0 1px 0 rgba(4,4,5,0.02); }
|
||||
.messages-wrapper { flex: 1; overflow-y: scroll; display: flex; flex-direction: column; padding: 0 16px; }
|
||||
.message-group { margin-top: 16px; display: flex; gap: 16px; }
|
||||
.avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--blurple); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold; color: white; font-size: 18px; }
|
||||
.avatar.bot { background: var(--success); }
|
||||
.msg-content { flex: 1; min-width: 0; }
|
||||
.msg-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
|
||||
.username { font-weight: 500; color: white; }
|
||||
.timestamp { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.msg-text { white-space: pre-wrap; line-height: 1.375rem; color: var(--text-normal); }
|
||||
.input-wrapper { padding: 0 16px 24px; flex-shrink: 0; margin-top: 10px; }
|
||||
.input-bg { background: var(--bg-input); border-radius: 8px; padding: 12px; display: flex; align-items: center; }
|
||||
.chat-input { background: transparent; border: none; color: white; flex: 1; font-size: 1rem; outline: none; }
|
||||
|
||||
/* APPOINTMENTS TABLE STYLES */
|
||||
.appt-container { padding: 40px; overflow-y: auto; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; background: var(--bg-secondary); border-radius: 8px; overflow: hidden; }
|
||||
th, td { padding: 15px; text-align: left; border-bottom: 1px solid var(--bg-input); }
|
||||
th { background: #202225; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 1px; color: var(--text-muted); }
|
||||
input.edit-field { background: var(--bg-input); border: 1px solid #202225; color: white; padding: 8px; border-radius: 4px; width: 100%; box-sizing: border-box; }
|
||||
.status-pill { background: #faa61a; color: black; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; }
|
||||
|
||||
/* BUTTONS */
|
||||
.btn-blurple { background: var(--blurple); color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: 500; cursor: pointer; transition: 0.2s; }
|
||||
.btn-blurple:hover { background: var(--blurple-hover); }
|
||||
.btn-icon { background: transparent; border: none; cursor: pointer; padding: 5px; color: var(--text-muted); border-radius: 4px; }
|
||||
.btn-icon:hover { background: var(--bg-tertiary); color: white; }
|
||||
|
||||
/* MODAL */
|
||||
.modal-overlay { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center;}
|
||||
.modal { background: var(--bg-primary); padding: 20px; border-radius: 8px; width: 300px; text-align: center; }
|
||||
</style>
|
||||
<title>SekiBot Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<span>SekiBot Server</span>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<div class="nav-item active" onclick="switchPanel('chat')" id="nav-chat">💬 Conversations</div>
|
||||
<div class="nav-item" onclick="switchPanel('appt')" id="nav-appt">📅 Appointments</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-scroll" id="channel-list">
|
||||
<div style="padding: 10px;">
|
||||
<button class="btn-blurple" style="width: 100%;" onclick="createNewChat()">+ New Chat</button>
|
||||
</div>
|
||||
|
||||
{{range .Chats}}
|
||||
<div class="channel-item" id="chat-item-{{.ID}}" onclick="loadChat({{.ID}}, '{{.Title}}')">
|
||||
<div style="display: flex; align-items: center; overflow: hidden;">
|
||||
<span style="color: #72767d; margin-right: 6px;">#</span>
|
||||
<span class="channel-name" id="title-{{.ID}}">{{.Title}}</span>
|
||||
</div>
|
||||
<div class="channel-actions" onclick="event.stopPropagation()">
|
||||
<span class="icon-btn" onclick="openRenameModal({{.ID}})">✎</span>
|
||||
<span class="icon-btn del" onclick="deleteChat({{.ID}})">✖</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-panel show" id="panel-chat">
|
||||
<div class="chat-header">
|
||||
<span style="color: #72767d; margin-right: 6px; font-size: 1.2em;">#</span>
|
||||
<span id="current-channel-name">general</span>
|
||||
</div>
|
||||
|
||||
<div class="messages-wrapper" id="messages-pane">
|
||||
<div style="margin: auto; text-align: center; color: var(--text-muted);">
|
||||
<h3>Welcome back, Seki.</h3>
|
||||
<p>Select a chat on the left to start.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
<div class="input-bg">
|
||||
<input type="text" class="chat-input" id="user-input" placeholder="Message #general" onkeypress="handleEnter(event)">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<main class="main-panel" id="panel-appt">
|
||||
<div class="appt-container">
|
||||
<h1>Appointments Manager</h1>
|
||||
|
||||
<div style="background: var(--bg-secondary); padding: 20px; border-radius: 8px; margin-bottom: 30px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px;">Add Manual Appointment</h3>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" class="edit-field" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
|
||||
<input type="datetime-local" class="edit-field" id="m-date" style="flex: 1;">
|
||||
<button class="btn-blurple" style="background: var(--success);" onclick="createMockAppt()">Add Booking</button>
|
||||
<div class="dashboard-layout">
|
||||
<div class="sidebar">
|
||||
<h2 style="color: white;">🤖 SekiBot</h2>
|
||||
<div style="margin-bottom: 20px; color: var(--text-muted);">
|
||||
User: {{ .UserEmail }} <br>
|
||||
<small>Plan: {{ .Tier }}</small>
|
||||
</div>
|
||||
<a href="#settings" class="btn-outline" style="text-align:center; border:none; text-align: left;">⚙️ Settings</a>
|
||||
<a href="#appointments" class="btn-outline" style="text-align:center; border:none; text-align: left;">📅 Appointments</a>
|
||||
<a href="/logout" style="margin-top: auto; color: var(--danger);">🚪 Logout</a>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">ID</th>
|
||||
<th>Phone</th>
|
||||
<th>Date (YYYY-MM-DD HH:MM)</th>
|
||||
<th>Status</th>
|
||||
<th style="width: 100px; text-align: right;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Appointments}}
|
||||
<tr id="appt-{{.ID}}">
|
||||
<td>#{{.ID}}</td>
|
||||
<td><input type="text" class="edit-field" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}"></td>
|
||||
<td><input type="text" class="edit-field" value="{{.Date}}" id="edit-date-{{.ID}}"></td>
|
||||
<td><span class="status-pill">{{.Status}}</span></td>
|
||||
<td style="text-align: right;">
|
||||
<button class="btn-icon" title="Save" onclick="updateAppt({{.ID}})">💾</button>
|
||||
<button class="btn-icon" title="Delete" style="color: var(--danger);" onclick="deleteAppt({{.ID}})">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal-overlay" id="rename-modal">
|
||||
<div class="modal">
|
||||
<h3 style="margin-top:0; color:white;">Rename Channel</h3>
|
||||
<input type="text" id="new-name-input" class="edit-field" style="margin-bottom: 15px;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button onclick="closeModal()" style="background: transparent; color: white; border: none; cursor: pointer;">Cancel</button>
|
||||
<button class="btn-blurple" onclick="submitRename()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentChatId = null;
|
||||
let chatToRename = null;
|
||||
|
||||
// --- NAVIGATION ---
|
||||
function switchPanel(panel) {
|
||||
document.getElementById('panel-chat').classList.toggle('show', panel === 'chat');
|
||||
document.getElementById('panel-appt').classList.toggle('show', panel === 'appt');
|
||||
|
||||
document.getElementById('nav-chat').classList.toggle('active', panel === 'chat');
|
||||
document.getElementById('nav-appt').classList.toggle('active', panel === 'appt');
|
||||
}
|
||||
|
||||
// --- CHAT CRUD ---
|
||||
|
||||
async function createNewChat() {
|
||||
try {
|
||||
const res = await fetch('/admin/chat', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
<div class="main-content">
|
||||
|
||||
if (data.id) {
|
||||
// Create Element
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.className = 'channel-item active'; // Set active immediately
|
||||
newDiv.id = `chat-item-${data.id}`;
|
||||
newDiv.onclick = () => loadChat(data.id, data.title);
|
||||
|
||||
newDiv.innerHTML = `
|
||||
<div style="display: flex; align-items: center; overflow: hidden;">
|
||||
<span style="color: #72767d; margin-right: 6px;">#</span>
|
||||
<span class="channel-name" id="title-${data.id}">${data.title}</span>
|
||||
<div id="settings" class="card">
|
||||
<h3 style="color: white;">Business Configuration</h3>
|
||||
<form action="/update-bot" method="POST">
|
||||
<label style="color: var(--text-muted);">Bot Name</label>
|
||||
<input type="text" name="bot_name" value="{{ .BotConfig.Name }}">
|
||||
|
||||
<label style="color: var(--text-muted);">System Prompt</label>
|
||||
<textarea name="system_prompt" rows="3" style="width:100%; background:var(--bg-dark); color:white; border:1px solid #40444b; padding:10px;">{{ .BotConfig.Prompt }}</textarea>
|
||||
|
||||
<h4 style="color: white; margin-top: 20px;">Open Hours</h4>
|
||||
|
||||
{{ range $day := .Days }}
|
||||
<div class="hours-grid">
|
||||
<span style="color:white;">{{ $day }}</span>
|
||||
<select name="{{$day}}_open">
|
||||
<option value="">Closed</option>
|
||||
<option value="08:00">08:00 AM</option>
|
||||
<option value="09:00" selected>09:00 AM</option>
|
||||
<option value="10:00">10:00 AM</option>
|
||||
</select>
|
||||
<select name="{{$day}}_close">
|
||||
<option value="">Closed</option>
|
||||
<option value="17:00" selected>05:00 PM</option>
|
||||
<option value="18:00">06:00 PM</option>
|
||||
<option value="19:00">07:00 PM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="channel-actions" onclick="event.stopPropagation()">
|
||||
<span class="icon-btn" onclick="openRenameModal(${data.id})">✎</span>
|
||||
<span class="icon-btn del" onclick="deleteChat(${data.id})">✖</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert after the button container
|
||||
const list = document.getElementById('channel-list');
|
||||
const btnContainer = list.firstElementChild; // The div containing the button
|
||||
if (btnContainer.nextSibling) {
|
||||
list.insertBefore(newDiv, btnContainer.nextSibling);
|
||||
} else {
|
||||
list.appendChild(newDiv);
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
<button type="submit" style="margin-top: 20px;">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
// Switch and Load
|
||||
switchPanel('chat');
|
||||
loadChat(data.id, data.title);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to create chat.");
|
||||
}
|
||||
}
|
||||
<div id="appointments" class="card">
|
||||
<h3 style="color: white;">Manage Appointments</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Client</th><th>Date</th><th>Status</th><th>Action</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Appointments }}
|
||||
<tr>
|
||||
<td>{{ .Phone }}</td>
|
||||
<td>{{ .Date }}</td>
|
||||
<td class="{{ if eq .Status "cancelled" }}status-cancelled{{ else }}status-confirmed{{ end }}">
|
||||
{{ .Status }}
|
||||
</td>
|
||||
<td>
|
||||
{{ if ne .Status "cancelled" }}
|
||||
<form action="/admin/appointment/{{.ID}}/cancel" method="POST" style="margin:0;">
|
||||
<button style="background:var(--danger); padding: 5px; font-size: 0.8rem;">Cancel</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr><td colspan="4" style="text-align:center; color:gray;">No appointments yet.</td></tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<div id="calendar" style="background: white; padding: 10px; border-radius: 8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
async function deleteChat(id) {
|
||||
if (!confirm("Delete this chat permanently?")) return;
|
||||
await fetch(`/admin/chat/${id}`, { method: 'DELETE' });
|
||||
document.getElementById(`chat-item-${id}`).remove();
|
||||
if (currentChatId === id) {
|
||||
document.getElementById('messages-pane').innerHTML = '<div style="margin: auto; color: var(--text-muted);">Chat deleted.</div>';
|
||||
document.getElementById('current-channel-name').innerText = 'deleted';
|
||||
currentChatId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openRenameModal(id) {
|
||||
chatToRename = id;
|
||||
document.getElementById('rename-modal').style.display = 'flex';
|
||||
document.getElementById('new-name-input').value = document.getElementById(`title-${id}`).innerText;
|
||||
document.getElementById('new-name-input').focus();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('rename-modal').style.display = 'none';
|
||||
chatToRename = null;
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
const newTitle = document.getElementById('new-name-input').value;
|
||||
if (newTitle && chatToRename) {
|
||||
await fetch(`/admin/chat/${chatToRename}/rename`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: newTitle })
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
events: '/api/my-appointments',
|
||||
height: 400
|
||||
});
|
||||
document.getElementById(`title-${chatToRename}`).innerText = newTitle;
|
||||
if (currentChatId === chatToRename) {
|
||||
document.getElementById('current-channel-name').innerText = newTitle;
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// --- MESSAGING ---
|
||||
|
||||
async function loadChat(id, title) {
|
||||
currentChatId = id;
|
||||
|
||||
// Highlight active channel
|
||||
document.querySelectorAll('.channel-item').forEach(el => el.classList.remove('active'));
|
||||
const activeItem = document.getElementById(`chat-item-${id}`);
|
||||
if(activeItem) activeItem.classList.add('active');
|
||||
|
||||
document.getElementById('current-channel-name').innerText = title;
|
||||
document.getElementById('user-input').placeholder = `Message #${title}`;
|
||||
document.getElementById('user-input').focus();
|
||||
|
||||
const pane = document.getElementById('messages-pane');
|
||||
pane.innerHTML = '<div style="margin: auto; color: var(--text-muted);">Loading history...</div>';
|
||||
|
||||
const res = await fetch(`/admin/chat/${id}/messages`);
|
||||
const messages = await res.json();
|
||||
|
||||
pane.innerHTML = '';
|
||||
if (messages) {
|
||||
messages.forEach(msg => renderMessage(msg.role, msg.content));
|
||||
} else {
|
||||
pane.innerHTML = '<div style="margin: auto; color: var(--text-muted);">No messages yet. Say hello!</div>';
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function renderMessage(role, text) {
|
||||
const pane = document.getElementById('messages-pane');
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.className = 'message-group';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = `avatar ${role === 'assistant' ? 'bot' : ''}`;
|
||||
avatar.innerText = role === 'user' ? 'U' : 'AI';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'msg-content';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'msg-header';
|
||||
header.innerHTML = `<span class="username">${role === 'user' ? 'User' : 'SekiBot'}</span> <span class="timestamp">${new Date().toLocaleTimeString()}</span>`;
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'msg-text';
|
||||
body.innerText = text;
|
||||
|
||||
contentDiv.appendChild(header);
|
||||
contentDiv.appendChild(body);
|
||||
group.appendChild(avatar);
|
||||
group.appendChild(contentDiv);
|
||||
|
||||
pane.appendChild(group);
|
||||
return body;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!currentChatId) return alert("Select a chat first!");
|
||||
|
||||
const input = document.getElementById('user-input');
|
||||
const content = input.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
input.value = '';
|
||||
renderMessage('user', content);
|
||||
scrollToBottom();
|
||||
|
||||
const streamContainer = renderMessage('assistant', '');
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/chat/${currentChatId}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
streamContainer.innerText += chunk;
|
||||
scrollToBottom();
|
||||
}
|
||||
} catch (e) {
|
||||
streamContainer.innerText += "\n[Error: Connection Failed]";
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const pane = document.getElementById('messages-pane');
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
}
|
||||
|
||||
function handleEnter(e) {
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
}
|
||||
|
||||
// --- APPOINTMENTS MANAGER ---
|
||||
|
||||
async function createMockAppt() {
|
||||
const phone = document.getElementById('m-phone').value;
|
||||
const date = document.getElementById('m-date').value;
|
||||
if(!phone || !date) return alert("Fill in fields, seki.");
|
||||
|
||||
await fetch('/admin/appointment', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({phone, date})
|
||||
calendar.render();
|
||||
});
|
||||
location.reload(); // Reload to refresh table
|
||||
}
|
||||
|
||||
async function updateAppt(id) {
|
||||
const phone = document.getElementById(`edit-phone-${id}`).value;
|
||||
const date = document.getElementById(`edit-date-${id}`).value;
|
||||
|
||||
const res = await fetch(`/admin/appointment/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({phone, date})
|
||||
});
|
||||
|
||||
if(res.ok) alert("Saved.");
|
||||
else alert("Update failed.");
|
||||
}
|
||||
|
||||
async function deleteAppt(id) {
|
||||
if(!confirm("Are you sure?")) return;
|
||||
const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
|
||||
if(res.ok) {
|
||||
document.getElementById(`appt-${id}`).remove();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
31
templates/landing.html
Normal file
31
templates/landing.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{{ define "landing.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SekiBot - AI Receptionist</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<h1 class="hero-title">Automate Your Appointments</h1>
|
||||
<p class="hero-sub">The AI receptionist that lives in WhatsApp.</p>
|
||||
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<a href="/login"><button style="width: 150px;">Login</button></a>
|
||||
<a href="/register"><button class="btn-outline" style="width: 150px;">Get Started</button></a>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 50px; display: flex; gap: 20px;">
|
||||
<div class="auth-box" style="width: 200px;">
|
||||
<h3>24/7 Booking</h3>
|
||||
<p style="color: grey;">Never miss a client.</p>
|
||||
</div>
|
||||
<div class="auth-box" style="width: 200px;">
|
||||
<h3>WhatsApp Native</h3>
|
||||
<p style="color: grey;">No apps to install.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
20
templates/login.html
Normal file
20
templates/login.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{ define "login.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Login</title><link rel="stylesheet" href="/static/style.css"></head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<div class="auth-box">
|
||||
<h2 style="color: white;">Welcome Back</h2>
|
||||
{{ if .Error }}<div style="color: red; margin-bottom: 10px;">{{ .Error }}</div>{{ end }}
|
||||
<form action="/login" method="POST">
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p style="margin-top: 15px;"><a href="/register">Create an account</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
20
templates/register.html
Normal file
20
templates/register.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{ define "register.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Register</title><link rel="stylesheet" href="/static/style.css"></head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<div class="auth-box">
|
||||
<h2 style="color: white;">Start Free Trial</h2>
|
||||
{{ if .Error }}<div style="color: red; margin-bottom: 10px;">{{ .Error }}</div>{{ end }}
|
||||
<form action="/register" method="POST">
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Create Account</button>
|
||||
</form>
|
||||
<p style="margin-top: 15px;"><a href="/login">Already have an account?</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user