modified: bot.db
modified: db/db.go modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html
This commit is contained in:
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
db/db.go
14
db/db.go
@@ -28,6 +28,20 @@ func Init() {
|
||||
customer_phone TEXT,
|
||||
appointment_date TEXT,
|
||||
status TEXT DEFAULT 'confirmed'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT DEFAULT 'New Chat',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER,
|
||||
role TEXT, -- 'user' or 'assistant'
|
||||
content TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(chat_id) REFERENCES chats(id)
|
||||
);`
|
||||
Conn.Exec(schema)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"whatsapp-bot/db"
|
||||
"whatsapp-bot/services"
|
||||
|
||||
@@ -9,23 +11,52 @@ import (
|
||||
)
|
||||
|
||||
func ShowDashboard(c *gin.Context) {
|
||||
rows, err := db.Conn.Query("SELECT customer_phone, appointment_date, status FROM appointments")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "DB Error")
|
||||
return
|
||||
// 1. Fetch Appointments for the Table
|
||||
type Appt struct {
|
||||
ID int
|
||||
CustomerPhone string
|
||||
Date string
|
||||
Status string
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type Appt struct{ CustomerPhone, Date, Status string }
|
||||
var appts []Appt
|
||||
for rows.Next() {
|
||||
var a Appt
|
||||
rows.Scan(&a.CustomerPhone, &a.Date, &a.Status)
|
||||
appts = append(appts, a)
|
||||
|
||||
approws, err := db.Conn.Query("SELECT id, customer_phone, appointment_date, status FROM appointments ORDER BY id DESC")
|
||||
if err != nil {
|
||||
log.Println("Appt Query Error:", err)
|
||||
} else {
|
||||
defer approws.Close()
|
||||
for approws.Next() {
|
||||
var a Appt
|
||||
approws.Scan(&a.ID, &a.CustomerPhone, &a.Date, &a.Status)
|
||||
appts = append(appts, a)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch Chats for the Sidebar
|
||||
type Chat struct {
|
||||
ID int
|
||||
Title string
|
||||
}
|
||||
var chats []Chat
|
||||
|
||||
chatrows, err := db.Conn.Query("SELECT id FROM chats ORDER BY id DESC")
|
||||
if err != nil {
|
||||
log.Println("Chat Query Error:", err)
|
||||
} else {
|
||||
defer chatrows.Close()
|
||||
for chatrows.Next() {
|
||||
var ch Chat
|
||||
chatrows.Scan(&ch.ID)
|
||||
// Give it a default title so the sidebar isn't empty
|
||||
ch.Title = "Chat #" + strconv.Itoa(ch.ID)
|
||||
chats = append(chats, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Render the Template with BOTH data sets
|
||||
c.HTML(http.StatusOK, "dashboard.html", gin.H{
|
||||
"Appointments": appts,
|
||||
"Chats": chats,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,7 +71,10 @@ func TestAIHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Calling the service we wrote earlier
|
||||
response, err := services.GetAIResponse(body.Prompt)
|
||||
response, err := services.GetAIResponse([]services.Message{
|
||||
{Role: "user", Content: body.Prompt},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"response": "AI Error: " + err.Error()})
|
||||
return
|
||||
@@ -79,3 +113,84 @@ func DeleteAppointmentHandler(c *gin.Context) {
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// PUT /admin/appointment/:id
|
||||
func UpdateAppointmentHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var body struct {
|
||||
Phone string `json:"phone"`
|
||||
Date string `json:"date"`
|
||||
}
|
||||
if err := c.BindJSON(&body); err != nil {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := db.Conn.Exec(
|
||||
"UPDATE appointments SET customer_phone = ?, appointment_date = ? WHERE id = ?",
|
||||
body.Phone, body.Date, id,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
// POST /admin/chat
|
||||
func NewChatHandler(c *gin.Context) {
|
||||
// Insert a new chat record. In SQLite, this is enough to generate an ID.
|
||||
res, err := db.Conn.Exec("INSERT INTO chats (title) VALUES ('New Chat')")
|
||||
if err != nil {
|
||||
log.Println("Database Error in NewChat:", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create chat"})
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := res.LastInsertId()
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// GET /admin/chat/:id/messages
|
||||
func GetMessagesHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", id)
|
||||
defer rows.Close()
|
||||
|
||||
var msgs []services.Message
|
||||
for rows.Next() {
|
||||
var m services.Message
|
||||
rows.Scan(&m.Role, &m.Content)
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
c.JSON(200, msgs)
|
||||
}
|
||||
|
||||
// POST /admin/chat/:id/message
|
||||
func PostMessageHandler(c *gin.Context) {
|
||||
chatId := c.Param("id")
|
||||
var body struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
c.BindJSON(&body)
|
||||
|
||||
// 1. Save User Message
|
||||
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content)
|
||||
|
||||
// 2. Fetch history for AI
|
||||
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId)
|
||||
var history []services.Message
|
||||
for rows.Next() {
|
||||
var m services.Message
|
||||
rows.Scan(&m.Role, &m.Content)
|
||||
history = append(history, m)
|
||||
}
|
||||
|
||||
// 3. Get AI Response
|
||||
aiResp, _ := services.GetAIResponse(history)
|
||||
|
||||
// 4. Save Assistant Message
|
||||
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, aiResp)
|
||||
|
||||
c.JSON(200, gin.H{"response": aiResp})
|
||||
}
|
||||
|
||||
5
main.go
5
main.go
@@ -31,6 +31,11 @@ func main() {
|
||||
r.POST("/admin/test-ai", handlers.TestAIHandler)
|
||||
r.DELETE("/admin/appointment/:id", handlers.DeleteAppointmentHandler)
|
||||
r.POST("/admin/appointment", handlers.CreateAppointmentHandler)
|
||||
r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler)
|
||||
|
||||
r.POST("/admin/chat", handlers.NewChatHandler) // THE BUTTON HITS THIS
|
||||
r.GET("/admin/chat/:id/messages", handlers.GetMessagesHandler)
|
||||
r.POST("/admin/chat/:id/message", handlers.PostMessageHandler)
|
||||
|
||||
// A little something for the root so you don't get a 404 again, seki
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
bytes "bytes"
|
||||
json "encoding/json"
|
||||
io "io"
|
||||
http "net/http"
|
||||
os "os"
|
||||
"whatsapp-bot/db" // Ensure this matches your module name
|
||||
)
|
||||
|
||||
// The structs to map OpenRouter's response
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type OpenRouterResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
} `json:"tool_calls"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
func GetAIResponse(input string) (string, error) {
|
||||
func GetAIResponse(chatHistory []Message) (string, error) {
|
||||
apiKey := os.Getenv("OPENROUTER_API_KEY")
|
||||
payload := map[string]interface{}{
|
||||
"model": "google/gemini-2.0-flash-001",
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": "You are a Chilean business assistant. Be brief."},
|
||||
{"role": "user", "content": input},
|
||||
url := "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
// 1. Build the full message list with a System Prompt
|
||||
fullMessages := append([]Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: "You are a helpful Chilean business assistant. You can book appointments. If a user wants to schedule something, use the create_appointment tool. Always be concise and polite.",
|
||||
},
|
||||
}, chatHistory...)
|
||||
|
||||
// 2. Define the tools the AI can use
|
||||
payload := map[string]interface{}{
|
||||
"model": "stepfun/step-3.5-flash:free",
|
||||
"messages": fullMessages,
|
||||
"tools": []map[string]interface{}{
|
||||
{
|
||||
"type": "function",
|
||||
@@ -34,8 +55,8 @@ func GetAIResponse(input string) (string, error) {
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"customer_phone": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string", "description": "ISO format date and time"},
|
||||
"customer_phone": map[string]string{"type": "string", "description": "The user's phone number"},
|
||||
"date": map[string]string{"type": "string", "description": "The date and time in YYYY-MM-DD HH:MM format"},
|
||||
},
|
||||
"required": []string{"customer_phone", "date"},
|
||||
},
|
||||
@@ -44,8 +65,8 @@ func GetAIResponse(input string) (string, error) {
|
||||
},
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(data))
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -56,10 +77,8 @@ func GetAIResponse(input string) (string, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// If it's not a 200, return the error body so you can see why it failed
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "API Error: " + string(bodyBytes), nil
|
||||
return "Error from OpenRouter: " + string(bodyBytes), nil
|
||||
}
|
||||
|
||||
var result OpenRouterResponse
|
||||
@@ -68,8 +87,33 @@ func GetAIResponse(input string) (string, error) {
|
||||
}
|
||||
|
||||
if len(result.Choices) > 0 {
|
||||
return result.Choices[0].Message.Content, nil
|
||||
aiMsg := result.Choices[0].Message
|
||||
|
||||
// 3. Handle Tool Calls (The "Logic" part)
|
||||
if len(aiMsg.ToolCalls) > 0 {
|
||||
tc := aiMsg.ToolCalls[0].Function
|
||||
if tc.Name == "create_appointment" {
|
||||
var args struct {
|
||||
Phone string `json:"customer_phone"`
|
||||
Date string `json:"date"`
|
||||
}
|
||||
// Unmarshal the AI-generated arguments
|
||||
json.Unmarshal([]byte(tc.Arguments), &args)
|
||||
|
||||
// Save to DB using your helper
|
||||
err := db.SaveAppointment(args.Phone, args.Date)
|
||||
if err != nil {
|
||||
return "I tried to book it, but the database hates me: " + err.Error(), nil
|
||||
}
|
||||
return "✅ [SYSTEM] Appointment automatically booked for " + args.Phone + " at " + args.Date, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Return plain text if no tool was called
|
||||
if aiMsg.Content != "" {
|
||||
return aiMsg.Content, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "No response from AI.", nil
|
||||
return "The AI is giving me the silent treatment, seki.", nil
|
||||
}
|
||||
|
||||
@@ -1,198 +1,99 @@
|
||||
{{ define "dashboard.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SekiBot Admin | Discord Edition</title>
|
||||
<title>SekiBot | Conversations</title>
|
||||
<style>
|
||||
:root {
|
||||
--blurple: #5865F2;
|
||||
--background-dark: #36393f;
|
||||
--background-darker: #2f3136;
|
||||
--background-deep: #202225;
|
||||
--text-normal: #dcddde;
|
||||
--text-muted: #b9bbbe;
|
||||
--header-primary: #ffffff;
|
||||
--danger: #ed4245;
|
||||
--success: #3ba55c;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-deep);
|
||||
color: var(--text-normal);
|
||||
font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar Look */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background-color: var(--background-darker);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #202225;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
background-color: var(--background-dark);
|
||||
padding: 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3 { color: var(--header-primary); margin-top: 0; }
|
||||
/* DISCORD THEME COLORS */
|
||||
:root { --blurple: #5865F2; --bg-sidebar: #2f3136; --bg-chat: #36393f; --bg-input: #40444b; --text: #dcddde; }
|
||||
body { margin: 0; display: flex; height: 100vh; font-family: sans-serif; background: var(--bg-chat); color: var(--text); }
|
||||
|
||||
.card {
|
||||
background-color: var(--background-darker);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Inputs & Buttons */
|
||||
input {
|
||||
background-color: var(--background-deep);
|
||||
border: 1px solid #202225;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
width: calc(100% - 22px);
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--blurple);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button:hover { background-color: #4752c4; }
|
||||
button.delete { background-color: var(--danger); }
|
||||
button.delete:hover { background-color: #c03537; }
|
||||
|
||||
/* Table Styling */
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th { text-align: left; color: var(--text-muted); text-transform: uppercase; font-size: 12px; padding-bottom: 10px; }
|
||||
td { padding: 12px 0; border-top: 1px solid #42454a; vertical-align: middle; }
|
||||
|
||||
.status-pill {
|
||||
background-color: var(--success);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#ai-response-box {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background-color: var(--background-deep);
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid var(--blurple);
|
||||
display: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.sidebar { width: 240px; background: var(--bg-sidebar); display: flex; flex-direction: column; padding: 15px; }
|
||||
.chat-list { flex-grow: 1; overflow-y: auto; }
|
||||
.chat-item { padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 5px; background: rgba(255,255,255,0.05); }
|
||||
.chat-item:hover, .active { background: var(--bg-input); color: white; }
|
||||
|
||||
.chat-area { flex-grow: 1; display: flex; flex-direction: column; }
|
||||
.messages { flex-grow: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
|
||||
.msg { max-width: 80%; padding: 10px 15px; border-radius: 8px; line-height: 1.4; }
|
||||
.user { align-self: flex-end; background: var(--blurple); color: white; }
|
||||
.assistant { align-self: flex-start; background: var(--bg-input); }
|
||||
|
||||
.input-container { padding: 20px; background: var(--bg-chat); }
|
||||
#user-input { width: 100%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white; outline: none; }
|
||||
|
||||
button.new-chat { background: var(--blurple); color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2>SekiBot</h2>
|
||||
<p style="font-size: 12px; color: var(--text-muted);">v1.0.0-beta</p>
|
||||
<hr style="border: 0.5px solid #42454a; width: 100%;">
|
||||
<nav>
|
||||
<p style="color: var(--blurple); font-weight: bold;"># dashboard</p>
|
||||
<p style="color: var(--text-muted);"># analytics</p>
|
||||
<p style="color: var(--text-muted);"># settings</p>
|
||||
<div class="chat-item active" onclick="showPanel('chat-panel')" id="nav-chat"># conversations</div>
|
||||
<div class="chat-item" onclick="showPanel('appt-panel')" id="nav-appt"># appointments-manager</div>
|
||||
</nav>
|
||||
<hr style="border: 0.5px solid #42454a; margin: 15px 0;">
|
||||
<button class="new-chat" onclick="newChat()">+ New Chat</button>
|
||||
<div class="chat-list">
|
||||
{{range .Chats}}
|
||||
<div class="chat-item" onclick="loadChat({{.ID}})"># chat-{{.ID}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<h1>Welcome back, seki</h1>
|
||||
|
||||
<!-- AI Tester -->
|
||||
<div class="card">
|
||||
<h3>Test OpenRouter AI</h3>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" id="ai-input" placeholder="Message the bot..." style="margin-bottom: 0;">
|
||||
<button onclick="testAI()">Send</button>
|
||||
</div>
|
||||
<div id="ai-response-box"></div>
|
||||
<!-- MAIN CHAT PANEL -->
|
||||
<div class="main-content" id="chat-panel">
|
||||
<div class="messages" id="message-pane" style="height: calc(100vh - 120px); overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<p style="text-align: center; color: var(--text-muted);">Select a chat to begin.</p>
|
||||
</div>
|
||||
<div class="input-container" style="padding: 20px; background: var(--bg-chat);">
|
||||
<input type="text" id="user-input" placeholder="Type a message..." onkeypress="if(event.key==='Enter') sendMessage()" style="width: 100%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Entry -->
|
||||
<div class="card">
|
||||
<h3>Quick Create Appointment</h3>
|
||||
<!-- APPOINTMENTS CRUD PANEL -->
|
||||
<div class="main-content" id="appt-panel" style="display: none; padding: 40px;">
|
||||
<h1>Appointments Manager</h1>
|
||||
<div style="background: var(--bg-sidebar); padding: 20px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h3>Create Mock Appointment</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px;">
|
||||
<input type="text" id="phone" placeholder="Customer Phone (+569...)">
|
||||
<input type="datetime-local" id="date">
|
||||
<button onclick="createAppt()">Add</button>
|
||||
<input type="text" id="m-phone" placeholder="Phone">
|
||||
<input type="datetime-local" id="m-date">
|
||||
<button onclick="createMockAppt()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Table -->
|
||||
<div class="card">
|
||||
<h3>Scheduled Appointments</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Date & Time</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Appointments}}
|
||||
<tr id="row-{{.ID}}">
|
||||
<td>#{{.ID}}</td>
|
||||
<td style="font-weight: bold;">{{.CustomerPhone}}</td>
|
||||
<td>{{.Date}}</td>
|
||||
<td><span class="status-pill">{{.Status}}</span></td>
|
||||
<td>
|
||||
<button class="delete" onclick="deleteAppt({{.ID}})">Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Phone</th><th>Date</th><th>Status</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="appt-table-body">
|
||||
{{range .Appointments}}
|
||||
<tr id="appt-{{.ID}}">
|
||||
<td>#{{.ID}}</td>
|
||||
<td><input type="text" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}" style="width: 120px; margin:0;"></td>
|
||||
<td><input type="text" value="{{.Date}}" id="edit-date-{{.ID}}" style="width: 160px; margin:0;"></td>
|
||||
<td><span class="status-pill">{{.Status}}</span></td>
|
||||
<td>
|
||||
<button style="background: var(--success);" onclick="updateAppt({{.ID}})">Save</button>
|
||||
<button class="delete" onclick="deleteAppt({{.ID}})">Del</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testAI() {
|
||||
const input = document.getElementById('ai-input').value;
|
||||
const box = document.getElementById('ai-response-box');
|
||||
box.style.display = 'block';
|
||||
box.innerText = "Processing message...";
|
||||
|
||||
const res = await fetch('/admin/test-ai', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({prompt: input})
|
||||
});
|
||||
const data = await res.json();
|
||||
box.innerText = data.response;
|
||||
function showPanel(panelId) {
|
||||
document.getElementById('chat-panel').style.display = panelId === 'chat-panel' ? 'flex' : 'none';
|
||||
document.getElementById('appt-panel').style.display = panelId === 'appt-panel' ? 'block' : 'none';
|
||||
document.querySelectorAll('nav .chat-item').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(panelId === 'chat-panel' ? 'nav-chat' : 'nav-appt').classList.add('active');
|
||||
}
|
||||
|
||||
async function createAppt() {
|
||||
const phone = document.getElementById('phone').value;
|
||||
const date = document.getElementById('date').value;
|
||||
if(!phone || !date) return alert("Fill the fields, seki.");
|
||||
|
||||
async function createMockAppt() {
|
||||
const phone = document.getElementById('m-phone').value;
|
||||
const date = document.getElementById('m-date').value;
|
||||
await fetch('/admin/appointment', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -201,11 +102,24 @@
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function deleteAppt(id) {
|
||||
if(!confirm("Terminate this appointment?")) return;
|
||||
await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
|
||||
document.getElementById(`row-${id}`).remove();
|
||||
async function updateAppt(id) {
|
||||
const phone = document.getElementById(`edit-phone-${id}`).value;
|
||||
const date = document.getElementById(`edit-date-${id}`).value;
|
||||
await fetch(`/admin/appointment/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({phone, date})
|
||||
});
|
||||
alert("Updated, seki.");
|
||||
}
|
||||
|
||||
async function deleteAppt(id) {
|
||||
if(!confirm("Kill it?")) return;
|
||||
await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
|
||||
document.getElementById(`appt-${id}`).remove();
|
||||
}
|
||||
|
||||
// ... (Keep your existing newChat, loadChat, and sendMessage scripts here) ...
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user