Shoaib-33 commited on
Commit
e3f7780
Β·
1 Parent(s): 2fdc19c
.gitignore ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===== Python =====
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.pyc
7
+ *.pdb
8
+ *.egg
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ eggs/
13
+ parts/
14
+ var/
15
+ sdist/
16
+ develop-eggs/
17
+ .installed.cfg
18
+ lib/
19
+ lib64/
20
+
21
+ # ===== Virtual Environment =====
22
+ venv/
23
+ env/
24
+ ENV/
25
+ .venv/
26
+
27
+ # ===== Environment Variables =====
28
+ .env
29
+ .env.*
30
+ !.env.example
31
+
32
+ # ===== Database =====
33
+ *.db
34
+ *.sqlite
35
+ *.sqlite3
36
+
37
+ # ===== Vector Store / ChromaDB =====
38
+ vectorstore/
39
+ chroma_db/
40
+ chunks.txt
41
+
42
+ # ===== Logs =====
43
+ *.log
44
+ logs/
45
+
46
+ # ===== OS Files =====
47
+ .DS_Store
48
+ Thumbs.db
49
+ desktop.ini
50
+
51
+ # ===== IDE =====
52
+ .vscode/
53
+ .idea/
54
+ *.suo
55
+ *.ntvs*
56
+ *.njsproj
57
+ *.sln
58
+ *.sw?
59
+
60
+ # ===== Docker =====
61
+ .dockerignore
62
+
63
+ # ===== Misc =====
64
+ *.bak
65
+ *.tmp
66
+ *.swp
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first (for caching)
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy entire project
19
+ COPY . .
20
+
21
+ # Expose port
22
+ EXPOSE 8000
23
+
24
+ # Run the app
25
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Md Shoaib Shahriar Ibrahim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Bus Ticket Booking Application
3
- emoji: πŸ“ˆ
4
- colorFrom: green
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: mit
 
1
  ---
2
  title: Bus Ticket Booking Application
3
+ emoji: 🐒
4
+ colorFrom: pink
5
+ colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  license: mit
attachment/desh_travel.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Desh Travel Privacy Policy
3
+
4
+ Desh Travel is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: 70A Progoti Shoroni, Lift 4, Rupayan Millennium Square, Dhaka 1212, Bangladesh
7
+ Contact Information: Email: info@deshtravelbd.com
8
+ Privacy Policy / Terms Link: https://www.deshtravelbd.com/termsandcondition?utm_source=chatgpt.com
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Desh Travel services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
attachment/ena.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Ena Privacy Policy
3
+
4
+ Ena is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: StarNet, Plot 97/A, 2nd Floor, Road-7, Sector-4, Uttara C/A, Dhaka-1230
7
+ Contact Information: Mohakhali Bus Stand: 01760-737650 (Non-AC) & 01619-737650 (AC)
8
+ Privacy Policy / Terms Link: See website for full Privacy Policy
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Ena services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
14
+
attachment/green line.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Green Line Privacy Policy
3
+
4
+ Green Line is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: 9/2, Outer Circular Road, Momen Bagh, Rajarbagh, Dhaka 1217
7
+ Contact Information: Tel: +88 02 8315380, Call Center Mobile: 09613316557
8
+ Privacy Policy / Terms Link: Refer to website for Privacy Policy
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Green Line services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
attachment/hanif.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Hanif Privacy Policy
3
+
4
+ Hanif is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: Gabtoli / Mirpur region, Dhaka
7
+ Contact Information: Customer Support: 16460, Counter: 01713-049540
8
+ Privacy Policy / Terms Link: https://hanifenterprisebd.com/privacy-policy?utm_source=chatgpt.com
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Hanif services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
14
+
attachment/shyamoli.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Shyamoli Privacy Policy
3
+
4
+ Shyamoli is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: 27/ka, Pisciculture Housing Society, Shyamoli, Mohammadpur, Dhaka-1207
7
+ Contact Information: Reservations/Complaints: 01908899544, 01908899522, 01908899499
8
+ Privacy Policy / Terms Link: Full Privacy Policy available on website
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Shyamoli services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
attachment/soudia.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Soudia Privacy Policy
3
+
4
+ Soudia is committed to protecting the privacy and personal data of our customers. We collect personal details such as names, phone numbers, travel history, and booking preferences to provide efficient booking and travel services. This information helps us improve service quality, offer relevant promotions, and ensure smooth operational management.
5
+
6
+ Official Address: Panthapath, Dhaka 1205
7
+ Contact Information: Dhaka Panthapath Counter: 01919-654926
8
+ Privacy Policy / Terms Link: See website or counter for Privacy Policy
9
+
10
+ All collected information is stored securely, with access limited to authorized personnel only. Data sharing with external entities occurs strictly when required for legal or operational needs. Security protocols include encryption, access restrictions, and regular audits.
11
+
12
+ By utilizing Soudia services, you consent to the policies outlined here. Updates to this privacy policy will be posted on our website and are effective immediately upon publication.
13
+
backend/data_loader.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/data_loader.py
2
+ import json
3
+ from pathlib import Path
4
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
5
+
6
+ DATASET_FILE = Path("data.json")
7
+ POLICY_FOLDER = Path("attachment")
8
+
9
+ # ---------------------------
10
+ # Load main dataset
11
+ # ---------------------------
12
+ with open(DATASET_FILE, "r", encoding="utf-8") as f:
13
+ data = json.load(f)
14
+
15
+ districts = data["districts"]
16
+ providers = data["bus_providers"]
17
+
18
+ # ---------------------------
19
+ # Chunk storage container
20
+ # ---------------------------
21
+ all_chunks = []
22
+
23
+ # ---------------------------
24
+ # District & Dropping Point Chunks
25
+ # ---------------------------
26
+ for district in districts:
27
+ # District block summary
28
+ text = f"District: {district['name']}\nDropping points:\n"
29
+
30
+ for dp in district["dropping_points"]:
31
+ text += f"β€’ {dp['name']} β€” {dp['price']} Taka\n"
32
+
33
+ all_chunks.append({
34
+ "content": text,
35
+ "metadata": {
36
+ "type": "district",
37
+ "district": district["name"]
38
+ }
39
+ })
40
+
41
+ # Individual dropping points
42
+ for dp in district["dropping_points"]:
43
+ dp_text = (
44
+ f"Dropping point: {dp['name']} in {district['name']}.\n"
45
+ f"Fare: {dp['price']} Taka."
46
+ )
47
+
48
+ all_chunks.append({
49
+ "content": dp_text,
50
+ "metadata": {
51
+ "type": "dropping_point",
52
+ "district": district["name"],
53
+ "point": dp["name"],
54
+ "price": dp["price"]
55
+ }
56
+ })
57
+
58
+ # ---------------------------
59
+ # Provider Chunks (Desh, Hanif, Ena, etc.)
60
+ # ---------------------------
61
+ for provider in providers:
62
+ provider_text = (
63
+ f"Bus Provider: {provider['name']}\n"
64
+ f"Coverage Districts: {', '.join(provider['coverage_districts'])}"
65
+ )
66
+
67
+ all_chunks.append({
68
+ "content": provider_text,
69
+ "metadata": {
70
+ "type": "provider",
71
+ "provider": provider["name"].lower(),
72
+ "districts": provider["coverage_districts"]
73
+ }
74
+ })
75
+
76
+ # ---------------------------
77
+ # Policy Chunks (Hanif.txt, Ena.txt, etc.)
78
+ # ---------------------------
79
+ text_splitter = RecursiveCharacterTextSplitter(
80
+ chunk_size=500,
81
+ chunk_overlap=150
82
+ )
83
+
84
+ policy_files = list(POLICY_FOLDER.glob("*.txt"))
85
+
86
+ for txt_file in policy_files:
87
+ provider_name = txt_file.stem.lower()
88
+
89
+ with open(txt_file, "r", encoding="utf-8") as f:
90
+ policy_text = f.read().strip()
91
+
92
+ wrapped_text = f"Policy of {provider_name} bus:\n\n{policy_text}"
93
+
94
+ chunks = text_splitter.split_text(wrapped_text)
95
+
96
+ for i, chunk in enumerate(chunks):
97
+ all_chunks.append({
98
+ "content": chunk,
99
+ "metadata": {
100
+ "type": "policy",
101
+ "provider": provider_name,
102
+ "chunk_index": i
103
+ }
104
+ })
105
+
106
+ # --------------- SUMMARY ----------------
107
+ print("\n======================================")
108
+ print("Total Chunks Created:", len(all_chunks))
109
+ print("======================================\n")
110
+
111
+ OUTPUT_FILE = "chunks.txt"
112
+
113
+ with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
114
+ for index, chunk in enumerate(all_chunks):
115
+ f.write("============================================================\n")
116
+ f.write(f"CHUNK #{index}\n")
117
+ f.write("------------------------------------------------------------\n")
118
+ f.write("CONTENT:\n")
119
+ f.write(chunk["content"] + "\n\n")
120
+ f.write("METADATA:\n")
121
+ for k, v in chunk["metadata"].items():
122
+ f.write(f" {k}: {v}\n")
123
+ f.write("============================================================\n\n")
124
+
125
+ print(f"Dumped {len(all_chunks)} chunks to chunks.txt")
backend/database.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/database.py
2
+ import sqlite3
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import List, Optional, Dict
6
+ import json
7
+
8
+ DATABASE_FILE = Path("bus_bookings.db")
9
+
10
+ def get_db_connection():
11
+ """Create database connection"""
12
+ conn = sqlite3.connect(DATABASE_FILE)
13
+ conn.row_factory = sqlite3.Row # Return rows as dictionaries
14
+ return conn
15
+
16
+ def init_database():
17
+ """Initialize database tables"""
18
+ conn = get_db_connection()
19
+ cursor = conn.cursor()
20
+
21
+ # Bookings table
22
+ cursor.execute("""
23
+ CREATE TABLE IF NOT EXISTS bookings (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ booking_id TEXT UNIQUE NOT NULL,
26
+ name TEXT NOT NULL,
27
+ phone TEXT NOT NULL,
28
+ bus_provider TEXT NOT NULL,
29
+ from_district TEXT NOT NULL,
30
+ to_district TEXT NOT NULL,
31
+ dropping_point TEXT NOT NULL,
32
+ travel_date TEXT NOT NULL,
33
+ num_passengers INTEGER NOT NULL,
34
+ fare INTEGER NOT NULL,
35
+ total_amount INTEGER NOT NULL,
36
+ booking_date TEXT NOT NULL,
37
+ status TEXT NOT NULL DEFAULT 'active',
38
+ cancelled_date TEXT,
39
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
40
+ )
41
+ """)
42
+
43
+ # Chat history table
44
+ cursor.execute("""
45
+ CREATE TABLE IF NOT EXISTS chat_history (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ session_id TEXT NOT NULL,
48
+ phone TEXT,
49
+ role TEXT NOT NULL,
50
+ message TEXT NOT NULL,
51
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
52
+ )
53
+ """)
54
+
55
+ # Deleted bookings history table
56
+ cursor.execute("""
57
+ CREATE TABLE IF NOT EXISTS deleted_bookings (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ booking_id TEXT NOT NULL,
60
+ booking_data TEXT NOT NULL,
61
+ deleted_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
62
+ deleted_by_phone TEXT
63
+ )
64
+ """)
65
+
66
+ conn.commit()
67
+ conn.close()
68
+ print("βœ… Database initialized")
69
+
70
+ # Initialize database on import
71
+ init_database()
72
+
73
+ # ==================== Booking Operations ====================
74
+
75
+ def create_booking(booking_data: dict) -> dict:
76
+ """Create a new booking"""
77
+ conn = get_db_connection()
78
+ cursor = conn.cursor()
79
+
80
+ cursor.execute("""
81
+ INSERT INTO bookings (
82
+ booking_id, name, phone, bus_provider, from_district, to_district,
83
+ dropping_point, travel_date, num_passengers, fare, total_amount, booking_date, status
84
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
+ """, (
86
+ booking_data['booking_id'],
87
+ booking_data['name'],
88
+ booking_data['phone'],
89
+ booking_data['bus_provider'],
90
+ booking_data['from_district'],
91
+ booking_data['to_district'],
92
+ booking_data['dropping_point'],
93
+ booking_data['travel_date'],
94
+ booking_data['num_passengers'],
95
+ booking_data['fare'],
96
+ booking_data['total_amount'],
97
+ booking_data['booking_date'],
98
+ 'active'
99
+ ))
100
+
101
+ conn.commit()
102
+ conn.close()
103
+
104
+ return booking_data
105
+
106
+ def get_all_bookings() -> List[dict]:
107
+ """Get all bookings"""
108
+ conn = get_db_connection()
109
+ cursor = conn.cursor()
110
+
111
+ cursor.execute("SELECT * FROM bookings ORDER BY created_at DESC")
112
+ bookings = [dict(row) for row in cursor.fetchall()]
113
+
114
+ conn.close()
115
+ return bookings
116
+
117
+ def get_bookings_by_phone(phone: str) -> List[dict]:
118
+ """Get bookings by phone number"""
119
+ conn = get_db_connection()
120
+ cursor = conn.cursor()
121
+
122
+ cursor.execute("SELECT * FROM bookings WHERE phone = ? ORDER BY created_at DESC", (phone,))
123
+ bookings = [dict(row) for row in cursor.fetchall()]
124
+
125
+ conn.close()
126
+ return bookings
127
+
128
+ def get_booking_by_id(booking_id: str) -> Optional[dict]:
129
+ """Get a specific booking"""
130
+ conn = get_db_connection()
131
+ cursor = conn.cursor()
132
+
133
+ cursor.execute("SELECT * FROM bookings WHERE booking_id = ?", (booking_id,))
134
+ row = cursor.fetchone()
135
+
136
+ conn.close()
137
+ return dict(row) if row else None
138
+
139
+ def delete_booking_permanently(booking_id: str) -> bool:
140
+ """Permanently delete a booking"""
141
+ conn = get_db_connection()
142
+ cursor = conn.cursor()
143
+
144
+ # Get booking data first
145
+ cursor.execute("SELECT * FROM bookings WHERE booking_id = ?", (booking_id,))
146
+ booking = cursor.fetchone()
147
+
148
+ if not booking:
149
+ conn.close()
150
+ return False
151
+
152
+ # Save to deleted bookings history
153
+ booking_data = dict(booking)
154
+ cursor.execute("""
155
+ INSERT INTO deleted_bookings (booking_id, booking_data, deleted_by_phone)
156
+ VALUES (?, ?, ?)
157
+ """, (booking_id, json.dumps(booking_data), booking_data.get('phone')))
158
+
159
+ # Delete from bookings table
160
+ cursor.execute("DELETE FROM bookings WHERE booking_id = ?", (booking_id,))
161
+
162
+ conn.commit()
163
+ conn.close()
164
+
165
+ return True
166
+
167
+ def cancel_booking(booking_id: str) -> bool:
168
+ """Mark booking as cancelled (soft delete)"""
169
+ conn = get_db_connection()
170
+ cursor = conn.cursor()
171
+
172
+ cursor.execute("""
173
+ UPDATE bookings
174
+ SET status = 'cancelled', cancelled_date = ?
175
+ WHERE booking_id = ? AND status = 'active'
176
+ """, (datetime.now().isoformat(), booking_id))
177
+
178
+ rows_affected = cursor.rowcount
179
+ conn.commit()
180
+ conn.close()
181
+
182
+ return rows_affected > 0
183
+
184
+ def generate_booking_id() -> str:
185
+ """Generate unique booking ID"""
186
+ conn = get_db_connection()
187
+ cursor = conn.cursor()
188
+
189
+ cursor.execute("SELECT COUNT(*) as count FROM bookings")
190
+ count = cursor.fetchone()['count']
191
+
192
+ conn.close()
193
+ return f"BK{count + 1:05d}"
194
+
195
+ # ==================== Chat History Operations ====================
196
+
197
+ def save_chat_message(session_id: str, role: str, message: str, phone: str = None):
198
+ """Save a chat message"""
199
+ conn = get_db_connection()
200
+ cursor = conn.cursor()
201
+
202
+ cursor.execute("""
203
+ INSERT INTO chat_history (session_id, phone, role, message)
204
+ VALUES (?, ?, ?, ?)
205
+ """, (session_id, phone, role, message))
206
+
207
+ conn.commit()
208
+ conn.close()
209
+
210
+ def get_chat_history(session_id: str, limit: int = 10) -> List[dict]:
211
+ """Get chat history for a session"""
212
+ conn = get_db_connection()
213
+ cursor = conn.cursor()
214
+
215
+ cursor.execute("""
216
+ SELECT role, message, timestamp
217
+ FROM chat_history
218
+ WHERE session_id = ?
219
+ ORDER BY timestamp DESC
220
+ LIMIT ?
221
+ """, (session_id, limit))
222
+
223
+ history = [dict(row) for row in cursor.fetchall()]
224
+ history.reverse() # Oldest first
225
+
226
+ conn.close()
227
+ return history
228
+
229
+ def clear_chat_history(session_id: str):
230
+ """Clear chat history for a session"""
231
+ conn = get_db_connection()
232
+ cursor = conn.cursor()
233
+
234
+ cursor.execute("DELETE FROM chat_history WHERE session_id = ?", (session_id,))
235
+
236
+ conn.commit()
237
+ conn.close()
238
+
239
+ # ==================== Statistics ====================
240
+
241
+ def get_booking_statistics() -> dict:
242
+ """Get booking statistics"""
243
+ conn = get_db_connection()
244
+ cursor = conn.cursor()
245
+
246
+ # Total bookings
247
+ cursor.execute("SELECT COUNT(*) as count FROM bookings")
248
+ total = cursor.fetchone()['count']
249
+
250
+ # Active bookings
251
+ cursor.execute("SELECT COUNT(*) as count FROM bookings WHERE status = 'active'")
252
+ active = cursor.fetchone()['count']
253
+
254
+ # Cancelled bookings
255
+ cursor.execute("SELECT COUNT(*) as count FROM bookings WHERE status = 'cancelled'")
256
+ cancelled = cursor.fetchone()['count']
257
+
258
+ # Total revenue (active only)
259
+ cursor.execute("SELECT SUM(total_amount) as revenue FROM bookings WHERE status = 'active'")
260
+ revenue = cursor.fetchone()['revenue'] or 0
261
+
262
+ # Provider statistics
263
+ cursor.execute("""
264
+ SELECT bus_provider, COUNT(*) as count, SUM(total_amount) as revenue
265
+ FROM bookings
266
+ WHERE status = 'active'
267
+ GROUP BY bus_provider
268
+ """)
269
+
270
+ provider_stats = {}
271
+ for row in cursor.fetchall():
272
+ provider_stats[row['bus_provider']] = {
273
+ 'count': row['count'],
274
+ 'revenue': row['revenue']
275
+ }
276
+
277
+ conn.close()
278
+
279
+ return {
280
+ 'total_bookings': total,
281
+ 'active_bookings': active,
282
+ 'cancelled_bookings': cancelled,
283
+ 'total_revenue': revenue,
284
+ 'provider_statistics': provider_stats
285
+ }
backend/main.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/main.py
2
+ from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.templating import Jinja2Templates
5
+ from fastapi.staticfiles import StaticFiles
6
+ from pydantic import BaseModel, Field
7
+ from typing import List, Optional
8
+ from datetime import datetime
9
+ import json
10
+ from pathlib import Path
11
+ import uuid
12
+
13
+ from .database import (
14
+ create_booking, get_all_bookings, get_bookings_by_phone,
15
+ get_booking_by_id, cancel_booking, delete_booking_permanently,
16
+ generate_booking_id, get_booking_statistics, save_chat_message, get_chat_history
17
+ )
18
+ from .rag_pipeline import get_answer, get_answer_with_sources
19
+
20
+ app = FastAPI(title="Bus Ticket Booking System")
21
+
22
+ # Static files & templates
23
+ BASE_DIR = Path(__file__).parent.parent
24
+ app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
25
+ templates = Jinja2Templates(directory=BASE_DIR / "templates")
26
+
27
+ # CORS middleware
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ # Load bus data
37
+ DATA_FILE = BASE_DIR / "data.json"
38
+ with open(DATA_FILE, "r", encoding="utf-8") as f:
39
+ bus_data = json.load(f)
40
+
41
+ # ==================== Models ====================
42
+
43
+ class BookingCreate(BaseModel):
44
+ name: str = Field(..., min_length=2, max_length=100)
45
+ phone: str = Field(..., min_length=11, max_length=15)
46
+ bus_provider: str
47
+ from_district: str
48
+ to_district: str
49
+ dropping_point: str
50
+ travel_date: str
51
+ num_passengers: int = Field(default=1, ge=1, le=10)
52
+
53
+ class BookingResponse(BaseModel):
54
+ booking_id: str
55
+ name: str
56
+ phone: str
57
+ bus_provider: str
58
+ from_district: str
59
+ to_district: str
60
+ dropping_point: str
61
+ travel_date: str
62
+ num_passengers: int
63
+ fare: int
64
+ total_amount: int
65
+ booking_date: str
66
+ status: str
67
+
68
+ class QueryRequest(BaseModel):
69
+ query: str
70
+ phone: Optional[str] = None
71
+ session_id: Optional[str] = None
72
+
73
+ # ==================== Helper Functions ====================
74
+
75
+ def get_fare(district: str, dropping_point: str) -> int:
76
+ for dist in bus_data["districts"]:
77
+ if dist["name"].lower() == district.lower():
78
+ for dp in dist["dropping_points"]:
79
+ if dp["name"].lower() == dropping_point.lower():
80
+ return dp["price"]
81
+ return 0
82
+
83
+ def validate_route(provider: str, from_district: str, to_district: str) -> bool:
84
+ for p in bus_data["bus_providers"]:
85
+ if p["name"].lower() == provider.lower():
86
+ coverage_lower = [d.lower() for d in p["coverage_districts"]]
87
+ return from_district.lower() in coverage_lower and to_district.lower() in coverage_lower
88
+ return False
89
+
90
+ def get_available_providers(from_district: str, to_district: str) -> List[str]:
91
+ available = []
92
+ for provider in bus_data["bus_providers"]:
93
+ coverage_lower = [d.lower() for d in provider["coverage_districts"]]
94
+ if from_district.lower() in coverage_lower and to_district.lower() in coverage_lower:
95
+ available.append(provider["name"])
96
+ return available
97
+
98
+ def get_dropping_points_by_district(district: str):
99
+ for d in bus_data["districts"]:
100
+ if d["name"].lower() == district.lower():
101
+ return [{"name": dp["name"], "price": dp["price"]} for dp in d["dropping_points"]]
102
+ return []
103
+
104
+ # ==================== Session Storage ====================
105
+ sessions = {}
106
+
107
+ # ==================== Page Routes (HTML) ====================
108
+
109
+ @app.get("/")
110
+ def index(request: Request):
111
+ return templates.TemplateResponse("index.html", {
112
+ "request": request,
113
+ "active": "book",
114
+ "providers": bus_data["bus_providers"],
115
+ "districts": bus_data["districts"]
116
+ })
117
+
118
+ @app.get("/bookings-page")
119
+ def bookings_page(request: Request):
120
+ return templates.TemplateResponse("bookings.html", {
121
+ "request": request,
122
+ "active": "bookings"
123
+ })
124
+
125
+ @app.get("/providers-page")
126
+ def providers_page(request: Request):
127
+ return templates.TemplateResponse("providers.html", {
128
+ "request": request,
129
+ "active": "providers",
130
+ "providers": bus_data["bus_providers"]
131
+ })
132
+
133
+ @app.get("/routes-page")
134
+ def routes_page(request: Request):
135
+ return templates.TemplateResponse("routes.html", {
136
+ "request": request,
137
+ "active": "routes",
138
+ "districts": bus_data["districts"]
139
+ })
140
+
141
+ @app.get("/assistant-page")
142
+ def assistant_page(request: Request):
143
+ return templates.TemplateResponse("assistant.html", {
144
+ "request": request,
145
+ "active": "assistant"
146
+ })
147
+
148
+ # ==================== API Endpoints ====================
149
+
150
+ @app.get("/districts")
151
+ def get_districts():
152
+ return {"districts": bus_data["districts"]}
153
+
154
+ @app.get("/providers")
155
+ def get_providers():
156
+ return {"providers": bus_data["bus_providers"]}
157
+
158
+ @app.get("/providers/{provider_name}/policy")
159
+ def get_provider_policy(provider_name: str):
160
+ provider_map = {
161
+ "desh travel": "desh_travel.txt",
162
+ "ena": "ena.txt",
163
+ "green line": "green line.txt",
164
+ "greenline": "green line.txt",
165
+ "hanif": "hanif.txt",
166
+ "shyamoli": "shyamoli.txt",
167
+ "soudia": "soudia.txt"
168
+ }
169
+ normalized = provider_name.lower().strip()
170
+ if normalized not in provider_map:
171
+ raise HTTPException(status_code=404, detail="Policy not available for this provider")
172
+ file_name = provider_map[normalized]
173
+ file_path = BASE_DIR / "attachment" / file_name
174
+ try:
175
+ with open(file_path, "r", encoding="utf-8") as f:
176
+ content = f.read()
177
+ except FileNotFoundError:
178
+ raise HTTPException(status_code=404, detail="Policy file not found")
179
+ return {"provider": provider_name, "policy": content}
180
+
181
+ @app.get("/available-providers")
182
+ def available_providers(from_district: str, to_district: str):
183
+ providers = get_available_providers(from_district, to_district)
184
+ if not providers:
185
+ return {"message": f"No providers between {from_district} and {to_district}", "providers": []}
186
+ return {"providers": providers}
187
+
188
+ @app.get("/dropping-points/{district}")
189
+ def dropping_points(district: str):
190
+ points = get_dropping_points_by_district(district)
191
+ if not points:
192
+ return {"message": f"No dropping points found for {district}", "dropping_points": []}
193
+ return {"dropping_points": points}
194
+
195
+ @app.post("/bookings", response_model=BookingResponse)
196
+ def create_booking_endpoint(booking: BookingCreate):
197
+ provider_exists = any(p["name"].lower() == booking.bus_provider.lower() for p in bus_data["bus_providers"])
198
+ if not provider_exists:
199
+ raise HTTPException(status_code=400, detail=f"Bus provider '{booking.bus_provider}' not found")
200
+ if not validate_route(booking.bus_provider, booking.from_district, booking.to_district):
201
+ raise HTTPException(status_code=400, detail=f"{booking.bus_provider} does not operate on this route")
202
+ fare = get_fare(booking.to_district, booking.dropping_point)
203
+ if fare == 0:
204
+ raise HTTPException(status_code=400, detail=f"Dropping point '{booking.dropping_point}' not found in {booking.to_district}")
205
+ new_booking_data = {
206
+ "booking_id": generate_booking_id(),
207
+ "name": booking.name,
208
+ "phone": booking.phone.strip(),
209
+ "bus_provider": booking.bus_provider,
210
+ "from_district": booking.from_district,
211
+ "to_district": booking.to_district,
212
+ "dropping_point": booking.dropping_point,
213
+ "travel_date": booking.travel_date,
214
+ "num_passengers": booking.num_passengers,
215
+ "fare": fare,
216
+ "total_amount": fare * booking.num_passengers,
217
+ "booking_date": datetime.now().isoformat(),
218
+ "status": "active"
219
+ }
220
+ saved_booking = create_booking(new_booking_data)
221
+ return saved_booking
222
+
223
+ @app.get("/bookings")
224
+ def list_all_bookings():
225
+ return {"bookings": get_all_bookings()}
226
+
227
+ @app.get("/bookings/phone/{phone}")
228
+ def bookings_by_phone(phone: str):
229
+ bookings = get_bookings_by_phone(phone.strip())
230
+ if not bookings:
231
+ raise HTTPException(status_code=404, detail="No bookings found for this phone number")
232
+ return {"bookings": bookings}
233
+
234
+ @app.get("/bookings/{booking_id}")
235
+ def booking_details(booking_id: str):
236
+ booking = get_booking_by_id(booking_id)
237
+ if not booking:
238
+ raise HTTPException(status_code=404, detail="Booking not found")
239
+ return booking
240
+
241
+ @app.delete("/bookings/{booking_id}")
242
+ def delete_booking_endpoint(booking_id: str, permanent: Optional[bool] = False):
243
+ if permanent:
244
+ success = delete_booking_permanently(booking_id)
245
+ if success:
246
+ return {"message": f"Booking {booking_id} deleted permanently."}
247
+ raise HTTPException(status_code=404, detail="Booking not found")
248
+ else:
249
+ success = cancel_booking(booking_id)
250
+ if success:
251
+ return {"message": f"Booking {booking_id} cancelled."}
252
+ raise HTTPException(status_code=404, detail="Booking not found")
253
+
254
+ @app.post("/query/smart")
255
+ def query_smart(request: QueryRequest):
256
+ session_id = request.session_id or str(uuid.uuid4())
257
+ if session_id not in sessions:
258
+ sessions[session_id] = {
259
+ "awaiting_booking_id": False,
260
+ "awaiting_phone_for_cancel": False,
261
+ "pending_bookings": [],
262
+ "phone": None
263
+ }
264
+ session = sessions[session_id]
265
+ query_text = request.query.strip()
266
+ query_lower = query_text.lower()
267
+
268
+ save_chat_message(session_id, "user", request.query, request.phone)
269
+
270
+ if session.get("awaiting_phone_for_cancel"):
271
+ phone = query_text
272
+ if phone.startswith("+88"):
273
+ phone = phone[3:]
274
+ session["phone"] = phone
275
+ session["awaiting_phone_for_cancel"] = False
276
+ bookings = get_bookings_by_phone(phone)
277
+ active_bookings = [b for b in bookings if b['status'] == 'active']
278
+ if not active_bookings:
279
+ message = f"No active bookings found for phone number {phone}"
280
+ save_chat_message(session_id, "assistant", message, phone)
281
+ return {"message": message, "session_id": session_id}
282
+ if len(active_bookings) == 1:
283
+ cancel_booking(active_bookings[0]['booking_id'])
284
+ message = f"Booking {active_bookings[0]['booking_id']} has been cancelled successfully."
285
+ save_chat_message(session_id, "assistant", message, phone)
286
+ return {"message": message, "session_id": session_id}
287
+ session["awaiting_booking_id"] = True
288
+ session["pending_bookings"] = active_bookings
289
+ message = "You have multiple active bookings:\n"
290
+ for b in active_bookings:
291
+ message += f"{b['from_district']} to {b['to_district']} on {b['travel_date']} (ID: {b['booking_id']})\n"
292
+ message += "\nPlease provide the Booking ID you want to cancel."
293
+ save_chat_message(session_id, "assistant", message, phone)
294
+ return {"message": message, "session_id": session_id}
295
+
296
+ if session["awaiting_booking_id"]:
297
+ booking_id = query_text
298
+ booking = next((b for b in session["pending_bookings"] if b["booking_id"] == booking_id), None)
299
+ if booking:
300
+ cancel_booking(booking_id)
301
+ session["awaiting_booking_id"] = False
302
+ session["pending_bookings"] = []
303
+ message = f"Booking {booking_id} has been cancelled successfully."
304
+ save_chat_message(session_id, "assistant", message, session.get("phone"))
305
+ return {"message": message, "session_id": session_id}
306
+ else:
307
+ message = f"Booking ID {booking_id} not found. Please check again."
308
+ save_chat_message(session_id, "assistant", message, session.get("phone"))
309
+ return {"message": message, "session_id": session_id}
310
+
311
+ if any(k in query_lower for k in ["cancel", "cancellation"]):
312
+ phone = (request.phone or session.get("phone") or "").strip()
313
+ if not phone:
314
+ session["awaiting_phone_for_cancel"] = True
315
+ message = "To cancel your booking, please provide your phone number."
316
+ save_chat_message(session_id, "assistant", message, None)
317
+ return {"message": message, "session_id": session_id}
318
+ if phone.startswith("+88"):
319
+ phone = phone[3:]
320
+ session["phone"] = phone
321
+ bookings = get_bookings_by_phone(phone)
322
+ active_bookings = [b for b in bookings if b['status'] == 'active']
323
+ if not active_bookings:
324
+ message = f"No active bookings found for phone number {phone}"
325
+ save_chat_message(session_id, "assistant", message, phone)
326
+ return {"message": message, "session_id": session_id}
327
+ if len(active_bookings) == 1:
328
+ cancel_booking(active_bookings[0]['booking_id'])
329
+ message = f"Booking {active_bookings[0]['booking_id']} has been cancelled successfully."
330
+ save_chat_message(session_id, "assistant", message, phone)
331
+ return {"message": message, "session_id": session_id}
332
+ session["awaiting_booking_id"] = True
333
+ session["pending_bookings"] = active_bookings
334
+ message = "You have multiple active bookings:\n"
335
+ for b in active_bookings:
336
+ message += f"{b['from_district']} to {b['to_district']} on {b['travel_date']} (ID: {b['booking_id']})\n"
337
+ message += "\nPlease provide the Booking ID you want to cancel."
338
+ save_chat_message(session_id, "assistant", message, phone)
339
+ return {"message": message, "session_id": session_id}
340
+
341
+ answer = get_answer(request.query)
342
+ save_chat_message(session_id, "assistant", answer, session.get("phone"))
343
+ return {"message": answer, "session_id": session_id}
344
+
345
+ @app.post("/query/detailed")
346
+ def query_rag_with_sources(request: QueryRequest):
347
+ return get_answer_with_sources(request.query)
348
+
349
+ @app.post("/chat/clear")
350
+ def clear_chat(session_id: str):
351
+ if session_id in sessions:
352
+ sessions[session_id] = {
353
+ "awaiting_booking_id": False,
354
+ "awaiting_phone_for_cancel": False,
355
+ "pending_bookings": [],
356
+ "phone": None
357
+ }
358
+ return {"message": "Chat cleared"}
359
+
360
+ @app.get("/stats")
361
+ def stats():
362
+ return get_booking_statistics()
363
+
364
+ if __name__ == "__main__":
365
+ import uvicorn
366
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/models.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+
4
+ class Booking(BaseModel):
5
+ name: str
6
+ phone: str
7
+ source: str
8
+ destination: str
9
+ provider: str
10
+ date: str
11
+
12
+ class SearchQuery(BaseModel):
13
+ source: str
14
+ destination: str
15
+ max_price: int = None
backend/rag_pipeline.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_community.embeddings import HuggingFaceEmbeddings
2
+ from langchain_chroma import Chroma
3
+ from langchain_google_genai import ChatGoogleGenerativeAI
4
+ from langchain.prompts import PromptTemplate
5
+ from langchain_core.output_parsers import StrOutputParser
6
+ from langchain_core.runnables import RunnablePassthrough
7
+ from .data_loader import all_chunks
8
+ import os
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ # --- API Key ---
14
+ os.environ["GOOGLE_API_KEY"] = os.environ.get("GOOGLE_API_KEY", "")
15
+
16
+ # --- Embeddings ---
17
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
18
+
19
+ # --- Vector DB ---
20
+ vectordb = Chroma(
21
+ collection_name="bus_data",
22
+ embedding_function=embedding_model,
23
+ persist_directory="vectorstore"
24
+ )
25
+
26
+ # Convert list metadata β†’ strings
27
+ def clean_metadata(metadata):
28
+ cleaned = {}
29
+ for key, value in metadata.items():
30
+ if isinstance(value, list):
31
+ cleaned[key] = ", ".join(str(v) for v in value)
32
+ else:
33
+ cleaned[key] = value
34
+ return cleaned
35
+
36
+ # Load chunks if empty
37
+ if len(vectordb.get()["ids"]) == 0:
38
+ print("Adding chunks to vector DB...")
39
+ for chunk in all_chunks:
40
+ metadata = chunk["metadata"].copy()
41
+ if "provider" in metadata and metadata["provider"]:
42
+ metadata["provider"] = metadata["provider"].strip().lower()
43
+ vectordb.add_texts([chunk["content"]], metadatas=[clean_metadata(metadata)])
44
+ print(f"βœ… Added {len(all_chunks)} chunks.")
45
+ else:
46
+ print(f"ℹ️ Vector database already contains {len(vectordb.get()['ids'])} chunks.")
47
+
48
+ # --- Gemini LLM ---
49
+ gemini_llm = ChatGoogleGenerativeAI(
50
+ temperature=0.3,
51
+ model="gemini-2.5-flash",
52
+ google_api_key=os.environ["GOOGLE_API_KEY"]
53
+ )
54
+
55
+ # --- Enhanced Prompt ---
56
+ prompt_template = """You are a friendly and helpful bus service assistant for Bangladesh bus services.
57
+
58
+ CRITICAL INSTRUCTIONS - READ CAREFULLY:
59
+ 1. If the user asks about a SPECIFIC bus provider (like Hanif, Ena, Desh Travel, etc.), ONLY use information from that provider's context.
60
+ 2. NEVER mix contact information, policies, or details between different providers.
61
+ 3. When answering about contact information, address, or policy, make absolutely sure you're looking at the correct provider's data.
62
+ 4. If you're not certain which provider the information belongs to, say you don't know.
63
+
64
+ GENERAL INSTRUCTIONS:
65
+ - Answer ONLY from the context provided below
66
+ - Be conversational, friendly, and concise
67
+ - Always mention prices in "Taka"
68
+ - Use bullet points for lists
69
+ - If information is missing, say: "I don't have that information. Please contact the bus service directly."
70
+
71
+ Context Information:
72
+ {context}
73
+
74
+ User Question: {question}
75
+
76
+ Helpful Answer:"""
77
+
78
+ PROMPT = PromptTemplate(
79
+ template=prompt_template,
80
+ input_variables=["context", "question"]
81
+ )
82
+
83
+ # ======================================================
84
+ # Detect provider from query
85
+ # ======================================================
86
+ def detect_provider_from_query(query: str):
87
+ query_lower = query.lower()
88
+ providers = ["hanif", "ena", "desh travel", "green line", "soudia", "shyamoli"]
89
+ for provider in providers:
90
+ if provider in query_lower:
91
+ return provider
92
+ return None
93
+
94
+ # ======================================================
95
+ # Format retrieved docs into a string
96
+ # ======================================================
97
+ def format_docs(docs):
98
+ return "\n\n".join(doc.page_content for doc in docs)
99
+
100
+ # ======================================================
101
+ # Build RAG chain
102
+ # ======================================================
103
+ def get_rag_chain(provider: str = None):
104
+ if provider:
105
+ retriever = vectordb.as_retriever(
106
+ search_type="similarity",
107
+ search_kwargs={"k": 10, "filter": {"provider": provider.strip().lower()}}
108
+ )
109
+ else:
110
+ retriever = vectordb.as_retriever(
111
+ search_type="similarity",
112
+ search_kwargs={"k": 20}
113
+ )
114
+
115
+ chain = (
116
+ {
117
+ "context": retriever | format_docs,
118
+ "question": RunnablePassthrough()
119
+ }
120
+ | PROMPT
121
+ | gemini_llm
122
+ | StrOutputParser()
123
+ )
124
+
125
+ return chain, retriever
126
+
127
+ # ======================================================
128
+ # Get answer
129
+ # ======================================================
130
+ def get_answer(query: str, provider: str = None):
131
+ provider = provider or detect_provider_from_query(query)
132
+ chain, _ = get_rag_chain(provider)
133
+ return chain.invoke(query)
134
+
135
+ def get_answer_with_sources(query: str, provider: str = None):
136
+ provider = provider or detect_provider_from_query(query)
137
+ chain, retriever = get_rag_chain(provider)
138
+
139
+ docs = retriever.invoke(query)
140
+ answer = chain.invoke(query)
141
+
142
+ return {
143
+ "answer": answer,
144
+ "source_documents": docs
145
+ }
data.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "districts": [
3
+ {
4
+ "name": "Dhaka",
5
+ "dropping_points": [
6
+ {"name": "Gabtoli", "price": 500},
7
+ {"name": "Mohakhali", "price": 550},
8
+ {"name": "Sayedabad", "price": 520}
9
+ ]
10
+ },
11
+ {
12
+ "name": "Chattogram",
13
+ "dropping_points": [
14
+ {"name": "Muradpur", "price": 600},
15
+ {"name": "Agrabad", "price": 650},
16
+ {"name": "Kaptai", "price": 620}
17
+ ]
18
+ },
19
+ {
20
+ "name": "Khulna",
21
+ "dropping_points": [
22
+ {"name": "Daulatpur", "price": 400},
23
+ {"name": "Khalishpur", "price": 420}
24
+ ]
25
+ },
26
+ {
27
+ "name": "Rajshahi",
28
+ "dropping_points": [
29
+ {"name": "Shah Makhdum", "price": 480},
30
+ {"name": "Bagha", "price": 500}
31
+ ]
32
+ },
33
+ {
34
+ "name": "Sylhet",
35
+ "dropping_points": [
36
+ {"name": "Zindabazar", "price": 700},
37
+ {"name": "Bimanbandar", "price": 720}
38
+ ]
39
+ },
40
+ {
41
+ "name": "Barishal",
42
+ "dropping_points": [
43
+ {"name": "Khanjahan", "price": 450},
44
+ {"name": "Bakerganj", "price": 470}
45
+ ]
46
+ },
47
+ {
48
+ "name": "Rangpur",
49
+ "dropping_points": [
50
+ {"name": "Kachari", "price": 480},
51
+ {"name": "Cantonment", "price": 500}
52
+ ]
53
+ },
54
+ {
55
+ "name": "Mymensingh",
56
+ "dropping_points": [
57
+ {"name": "Shahbazpur", "price": 460},
58
+ {"name": "Kewatkhali", "price": 480}
59
+ ]
60
+ },
61
+ {
62
+ "name": "Comilla",
63
+ "dropping_points": [
64
+ {"name": "Kotbari", "price": 530},
65
+ {"name": "Nangalkot", "price": 550}
66
+ ]
67
+ },
68
+ {
69
+ "name": "Bogra",
70
+ "dropping_points": [
71
+ {"name": "Shahjahanpur", "price": 470},
72
+ {"name": "Sariakandi", "price": 490}
73
+ ]
74
+ }
75
+ ],
76
+ "bus_providers": [
77
+ {
78
+ "name": "Desh Travel",
79
+ "coverage_districts": ["Dhaka", "Chattogram", "Sylhet", "Rangpur"]
80
+ },
81
+ {
82
+ "name": "Hanif",
83
+ "coverage_districts": ["Dhaka", "Khulna", "Mymensingh", "Comilla"]
84
+ },
85
+ {
86
+ "name": "Ena",
87
+ "coverage_districts": ["Chattogram", "Sylhet", "Barishal", "Bogra"]
88
+ },
89
+ {
90
+ "name": "Green Line",
91
+ "coverage_districts": ["Khulna", "Rajshahi", "Mymensingh", "Rangpur"]
92
+ },
93
+ {
94
+ "name": "Soudia",
95
+ "coverage_districts": ["Dhaka", "Rajshahi", "Barishal", "Comilla"]
96
+ },
97
+ {
98
+ "name": "Shyamoli",
99
+ "coverage_districts": ["Chattogram", "Khulna", "Sylhet", "Bogra"]
100
+ }
101
+ ]
102
+ }
103
+
docker-compose.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ container_name: busgo_app
7
+ ports:
8
+ - "8000:8000"
9
+ volumes:
10
+ # Persist the SQLite database
11
+ - ./bus_bookings.db:/app/bus_bookings.db
12
+ # Persist the vector store
13
+ - ./vectorstore:/app/vectorstore
14
+ env_file:
15
+ - .env
16
+ restart: unless-stopped
dokerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ .git/
9
+ .gitignore
10
+ .env
11
+ *.md
12
+ chroma_db/
13
+ .dockerignore
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ requests
5
+ streamlit==1.40.0
6
+ sqlite-utils==3.35.1
7
+ python-dotenv
8
+ langchain==0.3.27
9
+ langchain-community==0.3.29
10
+ langchain-text-splitters==0.3.11
11
+ langchain-huggingface==0.3.1
12
+ huggingface-hub>=0.33.4
13
+ sentence-transformers==3.0.0
14
+ langchain-chroma==0.2.6
15
+ chromadb==1.5.1
16
+ langchain-google-genai==2.0.10
17
+ google-generativeai==0.8.3
18
+ jinja2
19
+ aiofiles
20
+ python-multipart
static/css/style.css ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== CSS Variables ===== */
2
+ :root {
3
+ --bg: #0d0f14;
4
+ --surface: #161a24;
5
+ --surface2: #1e2435;
6
+ --border: #2a3045;
7
+ --accent: #f5a623;
8
+ --accent2: #ff6b35;
9
+ --text: #e8eaf0;
10
+ --text-muted: #7a8299;
11
+ --success: #2ecc71;
12
+ --danger: #e74c3c;
13
+ --radius: 12px;
14
+ --radius-sm: 8px;
15
+ --shadow: 0 4px 24px rgba(0,0,0,0.4);
16
+ --font-head: 'Syne', sans-serif;
17
+ --font-body: 'DM Sans', sans-serif;
18
+ }
19
+
20
+ /* ===== Reset ===== */
21
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
22
+
23
+ body {
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ font-family: var(--font-body);
27
+ font-size: 15px;
28
+ line-height: 1.6;
29
+ min-height: 100vh;
30
+ }
31
+
32
+ /* ===== Navbar ===== */
33
+ .navbar {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 1.5rem;
37
+ padding: 0 2rem;
38
+ height: 64px;
39
+ background: var(--surface);
40
+ border-bottom: 1px solid var(--border);
41
+ position: sticky;
42
+ top: 0;
43
+ z-index: 100;
44
+ }
45
+
46
+ .logo {
47
+ font-family: var(--font-head);
48
+ font-size: 1.3rem;
49
+ font-weight: 800;
50
+ color: var(--accent);
51
+ text-decoration: none;
52
+ letter-spacing: -0.5px;
53
+ margin-right: auto;
54
+ }
55
+
56
+ .nav-links { display: flex; gap: 0.25rem; }
57
+
58
+ .nav-link {
59
+ text-decoration: none;
60
+ color: var(--text-muted);
61
+ font-size: 0.875rem;
62
+ font-weight: 500;
63
+ padding: 0.45rem 0.9rem;
64
+ border-radius: var(--radius-sm);
65
+ transition: all 0.2s;
66
+ }
67
+
68
+ .nav-link:hover { color: var(--text); background: var(--surface2); }
69
+ .nav-link.active { color: var(--accent); background: rgba(245,166,35,0.1); }
70
+
71
+ .nav-status {
72
+ font-size: 1.2rem;
73
+ color: var(--text-muted);
74
+ transition: color 0.3s;
75
+ }
76
+ .nav-status.online { color: var(--success); }
77
+ .nav-status.offline { color: var(--danger); }
78
+
79
+ /* ===== Main ===== */
80
+ .main-content {
81
+ max-width: 1100px;
82
+ margin: 0 auto;
83
+ padding: 2.5rem 1.5rem 4rem;
84
+ }
85
+
86
+ /* ===== Page Header ===== */
87
+ .page-header { margin-bottom: 2.5rem; }
88
+
89
+ .page-title {
90
+ font-family: var(--font-head);
91
+ font-size: 2.4rem;
92
+ font-weight: 800;
93
+ letter-spacing: -1px;
94
+ line-height: 1.1;
95
+ }
96
+
97
+ .accent { color: var(--accent); }
98
+
99
+ .page-sub {
100
+ color: var(--text-muted);
101
+ margin-top: 0.5rem;
102
+ font-size: 1rem;
103
+ }
104
+
105
+ /* ===== Cards ===== */
106
+ .card {
107
+ background: var(--surface);
108
+ border: 1px solid var(--border);
109
+ border-radius: var(--radius);
110
+ padding: 1.75rem;
111
+ margin-bottom: 1.5rem;
112
+ }
113
+
114
+ .step-label {
115
+ font-family: var(--font-head);
116
+ font-size: 0.75rem;
117
+ font-weight: 700;
118
+ letter-spacing: 1.5px;
119
+ text-transform: uppercase;
120
+ color: var(--accent);
121
+ margin-bottom: 1.25rem;
122
+ }
123
+
124
+ /* ===== Form ===== */
125
+ .form-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
128
+ gap: 1.25rem;
129
+ }
130
+
131
+ .form-group { display: flex; flex-direction: column; gap: 0.4rem; }
132
+
133
+ .form-label {
134
+ font-size: 0.8rem;
135
+ font-weight: 500;
136
+ color: var(--text-muted);
137
+ letter-spacing: 0.3px;
138
+ }
139
+
140
+ .form-input, .form-select {
141
+ background: var(--surface2);
142
+ border: 1px solid var(--border);
143
+ border-radius: var(--radius-sm);
144
+ padding: 0.65rem 0.9rem;
145
+ color: var(--text);
146
+ font-family: var(--font-body);
147
+ font-size: 0.9rem;
148
+ transition: border-color 0.2s;
149
+ outline: none;
150
+ width: 100%;
151
+ }
152
+
153
+ .form-input:focus, .form-select:focus { border-color: var(--accent); }
154
+ .form-select:disabled { opacity: 0.4; cursor: not-allowed; }
155
+ .form-select option { background: var(--surface2); }
156
+
157
+ /* ===== Route Summary ===== */
158
+ .route-summary {
159
+ margin-top: 1.25rem;
160
+ padding: 0.85rem 1.1rem;
161
+ background: rgba(245,166,35,0.08);
162
+ border: 1px solid rgba(245,166,35,0.25);
163
+ border-radius: var(--radius-sm);
164
+ color: var(--accent);
165
+ font-size: 0.9rem;
166
+ font-weight: 500;
167
+ }
168
+
169
+ .total-box {
170
+ margin-top: 1rem;
171
+ padding: 0.85rem 1.1rem;
172
+ background: rgba(46,204,113,0.08);
173
+ border: 1px solid rgba(46,204,113,0.25);
174
+ border-radius: var(--radius-sm);
175
+ color: var(--success);
176
+ font-size: 0.95rem;
177
+ margin-bottom: 1.25rem;
178
+ }
179
+
180
+ /* ===== Buttons ===== */
181
+ .btn {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ gap: 0.4rem;
186
+ padding: 0.65rem 1.4rem;
187
+ border-radius: var(--radius-sm);
188
+ font-family: var(--font-body);
189
+ font-size: 0.9rem;
190
+ font-weight: 500;
191
+ cursor: pointer;
192
+ border: none;
193
+ transition: all 0.2s;
194
+ text-decoration: none;
195
+ }
196
+
197
+ .btn-primary {
198
+ background: var(--accent);
199
+ color: #0d0f14;
200
+ font-weight: 700;
201
+ }
202
+ .btn-primary:hover { background: #ffb94a; transform: translateY(-1px); }
203
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
204
+
205
+ .btn-outline {
206
+ background: transparent;
207
+ color: var(--text-muted);
208
+ border: 1px solid var(--border);
209
+ }
210
+ .btn-outline:hover { border-color: var(--accent); color: var(--accent); }
211
+
212
+ .btn-danger {
213
+ background: rgba(231,76,60,0.15);
214
+ color: var(--danger);
215
+ border: 1px solid rgba(231,76,60,0.3);
216
+ }
217
+ .btn-danger:hover { background: rgba(231,76,60,0.25); }
218
+
219
+ .btn-full { width: 100%; }
220
+
221
+ /* ===== Search card ===== */
222
+ .search-card .search-row { display: flex; gap: 1rem; align-items: center; }
223
+
224
+ /* ===== Bookings ===== */
225
+ .result-count {
226
+ font-size: 0.85rem;
227
+ color: var(--text-muted);
228
+ margin-bottom: 1rem;
229
+ }
230
+
231
+ .booking-item {
232
+ background: var(--surface);
233
+ border: 1px solid var(--border);
234
+ border-radius: var(--radius);
235
+ padding: 1.5rem;
236
+ margin-bottom: 1rem;
237
+ transition: border-color 0.2s;
238
+ }
239
+ .booking-item.active { border-left: 3px solid var(--success); }
240
+ .booking-item.cancelled { border-left: 3px solid var(--danger); opacity: 0.7; }
241
+
242
+ .booking-header {
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: center;
246
+ margin-bottom: 1rem;
247
+ }
248
+
249
+ .booking-id {
250
+ font-family: var(--font-head);
251
+ font-weight: 700;
252
+ font-size: 1rem;
253
+ color: var(--accent);
254
+ }
255
+
256
+ .booking-status {
257
+ font-size: 0.7rem;
258
+ font-weight: 700;
259
+ letter-spacing: 1px;
260
+ padding: 0.25rem 0.6rem;
261
+ border-radius: 4px;
262
+ }
263
+ .status-active { background: rgba(46,204,113,0.15); color: var(--success); }
264
+ .status-cancelled { background: rgba(231,76,60,0.15); color: var(--danger); }
265
+
266
+ .booking-body {
267
+ display: grid;
268
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
269
+ gap: 1rem;
270
+ }
271
+
272
+ .booking-field { margin-bottom: 0.5rem; }
273
+ .booking-field span { display: block; font-size: 0.75rem; color: var(--text-muted); }
274
+ .booking-field strong { font-size: 0.9rem; }
275
+ .total-field strong { color: var(--accent); font-size: 1rem; }
276
+
277
+ .booking-footer { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
278
+
279
+ /* ===== Providers ===== */
280
+ .providers-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
283
+ gap: 1.25rem;
284
+ }
285
+
286
+ .provider-card {
287
+ background: var(--surface);
288
+ border: 1px solid var(--border);
289
+ border-radius: var(--radius);
290
+ padding: 1.5rem;
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 1rem;
294
+ }
295
+
296
+ .provider-header { display: flex; align-items: center; gap: 1rem; }
297
+ .provider-icon { font-size: 2rem; }
298
+ .provider-name { font-family: var(--font-head); font-weight: 700; font-size: 1.1rem; }
299
+ .provider-meta { font-size: 0.8rem; color: var(--text-muted); }
300
+
301
+ .coverage-details summary {
302
+ font-size: 0.85rem;
303
+ color: var(--text-muted);
304
+ cursor: pointer;
305
+ padding: 0.4rem 0;
306
+ }
307
+ .coverage-details summary:hover { color: var(--text); }
308
+
309
+ .district-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem; }
310
+ .district-tag {
311
+ background: var(--surface2);
312
+ border: 1px solid var(--border);
313
+ border-radius: 4px;
314
+ padding: 0.2rem 0.5rem;
315
+ font-size: 0.75rem;
316
+ color: var(--text-muted);
317
+ }
318
+
319
+ .policy-box {
320
+ background: var(--surface2);
321
+ border: 1px solid var(--border);
322
+ border-radius: var(--radius-sm);
323
+ padding: 1rem;
324
+ font-size: 0.82rem;
325
+ color: var(--text-muted);
326
+ white-space: pre-wrap;
327
+ max-height: 280px;
328
+ overflow-y: auto;
329
+ line-height: 1.7;
330
+ }
331
+
332
+ /* ===== Districts / Routes ===== */
333
+ .districts-grid {
334
+ display: grid;
335
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
336
+ gap: 1.25rem;
337
+ }
338
+
339
+ .district-card {
340
+ background: var(--surface);
341
+ border: 1px solid var(--border);
342
+ border-radius: var(--radius);
343
+ padding: 1.25rem;
344
+ }
345
+
346
+ .district-header {
347
+ display: flex;
348
+ justify-content: space-between;
349
+ align-items: center;
350
+ margin-bottom: 0.75rem;
351
+ }
352
+
353
+ .district-name { font-family: var(--font-head); font-weight: 700; font-size: 1rem; }
354
+ .district-count { font-size: 0.75rem; color: var(--text-muted); background: var(--surface2); padding: 0.2rem 0.5rem; border-radius: 4px; }
355
+
356
+ .price-range {
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 0.4rem;
360
+ margin-bottom: 0.75rem;
361
+ font-size: 0.85rem;
362
+ }
363
+ .price-label { color: var(--text-muted); }
364
+ .price-val { color: var(--accent); font-weight: 600; }
365
+
366
+ .stops-details summary {
367
+ font-size: 0.82rem;
368
+ color: var(--text-muted);
369
+ cursor: pointer;
370
+ padding: 0.3rem 0;
371
+ }
372
+ .stops-list { margin-top: 0.5rem; }
373
+ .stop-row {
374
+ display: flex;
375
+ justify-content: space-between;
376
+ align-items: center;
377
+ padding: 0.35rem 0;
378
+ border-bottom: 1px solid var(--border);
379
+ font-size: 0.82rem;
380
+ }
381
+ .stop-row:last-child { border-bottom: none; }
382
+ .stop-price { color: var(--accent); font-weight: 600; }
383
+
384
+ /* ===== AI Chat ===== */
385
+ .chat-layout {
386
+ display: grid;
387
+ grid-template-columns: 260px 1fr;
388
+ gap: 1.5rem;
389
+ height: calc(100vh - 280px);
390
+ min-height: 500px;
391
+ }
392
+
393
+ .chat-sidebar {
394
+ background: var(--surface);
395
+ border: 1px solid var(--border);
396
+ border-radius: var(--radius);
397
+ padding: 1.25rem;
398
+ display: flex;
399
+ flex-direction: column;
400
+ gap: 1.25rem;
401
+ overflow-y: auto;
402
+ }
403
+
404
+ .sidebar-title {
405
+ font-size: 0.75rem;
406
+ font-weight: 700;
407
+ letter-spacing: 0.5px;
408
+ text-transform: uppercase;
409
+ color: var(--text-muted);
410
+ margin-bottom: 0.6rem;
411
+ }
412
+
413
+ .sidebar-hint { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.4rem; }
414
+
415
+ .suggestions { display: flex; flex-direction: column; gap: 0.4rem; }
416
+ .suggestion-btn {
417
+ background: var(--surface2);
418
+ border: 1px solid var(--border);
419
+ border-radius: var(--radius-sm);
420
+ padding: 0.5rem 0.75rem;
421
+ color: var(--text-muted);
422
+ font-size: 0.8rem;
423
+ cursor: pointer;
424
+ text-align: left;
425
+ transition: all 0.2s;
426
+ font-family: var(--font-body);
427
+ }
428
+ .suggestion-btn:hover { border-color: var(--accent); color: var(--text); }
429
+
430
+ .chat-main {
431
+ background: var(--surface);
432
+ border: 1px solid var(--border);
433
+ border-radius: var(--radius);
434
+ display: flex;
435
+ flex-direction: column;
436
+ overflow: hidden;
437
+ }
438
+
439
+ .chat-messages {
440
+ flex: 1;
441
+ overflow-y: auto;
442
+ padding: 1.5rem;
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 1rem;
446
+ }
447
+
448
+ .chat-message { display: flex; }
449
+ .chat-message.user { justify-content: flex-end; }
450
+
451
+ .msg-bubble {
452
+ max-width: 70%;
453
+ padding: 0.75rem 1rem;
454
+ border-radius: var(--radius);
455
+ font-size: 0.9rem;
456
+ line-height: 1.6;
457
+ }
458
+
459
+ .chat-message.user .msg-bubble {
460
+ background: var(--accent);
461
+ color: #0d0f14;
462
+ font-weight: 500;
463
+ border-bottom-right-radius: 4px;
464
+ }
465
+
466
+ .chat-message.assistant .msg-bubble {
467
+ background: var(--surface2);
468
+ border: 1px solid var(--border);
469
+ color: var(--text);
470
+ border-bottom-left-radius: 4px;
471
+ }
472
+
473
+ /* Typing animation */
474
+ .typing { display: flex; align-items: center; gap: 5px; padding: 1rem; }
475
+ .typing span {
476
+ width: 7px; height: 7px;
477
+ background: var(--text-muted);
478
+ border-radius: 50%;
479
+ animation: bounce 1.2s infinite;
480
+ }
481
+ .typing span:nth-child(2) { animation-delay: 0.2s; }
482
+ .typing span:nth-child(3) { animation-delay: 0.4s; }
483
+ @keyframes bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-8px)} }
484
+
485
+ .chat-input-area {
486
+ display: flex;
487
+ gap: 0.75rem;
488
+ padding: 1rem 1.25rem;
489
+ border-top: 1px solid var(--border);
490
+ }
491
+
492
+ .chat-input {
493
+ flex: 1;
494
+ background: var(--surface2);
495
+ border: 1px solid var(--border);
496
+ border-radius: var(--radius-sm);
497
+ padding: 0.65rem 1rem;
498
+ color: var(--text);
499
+ font-family: var(--font-body);
500
+ font-size: 0.9rem;
501
+ outline: none;
502
+ }
503
+ .chat-input:focus { border-color: var(--accent); }
504
+
505
+ /* ===== Modal ===== */
506
+ .modal {
507
+ position: fixed;
508
+ inset: 0;
509
+ background: rgba(0,0,0,0.7);
510
+ display: flex;
511
+ align-items: center;
512
+ justify-content: center;
513
+ z-index: 200;
514
+ backdrop-filter: blur(4px);
515
+ }
516
+
517
+ .modal-box {
518
+ background: var(--surface);
519
+ border: 1px solid var(--border);
520
+ border-radius: var(--radius);
521
+ padding: 2rem;
522
+ max-width: 480px;
523
+ width: 90%;
524
+ text-align: center;
525
+ box-shadow: var(--shadow);
526
+ }
527
+
528
+ .modal-icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
529
+ .modal-box h2 { font-family: var(--font-head); font-size: 1.5rem; margin-bottom: 1.25rem; }
530
+
531
+ .booking-result { text-align: left; margin-bottom: 1.5rem; }
532
+ .result-row {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ padding: 0.5rem 0;
536
+ border-bottom: 1px solid var(--border);
537
+ font-size: 0.875rem;
538
+ }
539
+ .result-row span { color: var(--text-muted); }
540
+ .result-row:last-child { border-bottom: none; }
541
+ .total-row strong { color: var(--accent); font-size: 1rem; }
542
+
543
+ /* ===== Toast ===== */
544
+ .toast {
545
+ position: fixed;
546
+ bottom: 2rem;
547
+ right: 2rem;
548
+ background: var(--surface2);
549
+ border: 1px solid var(--border);
550
+ border-radius: var(--radius-sm);
551
+ padding: 0.75rem 1.25rem;
552
+ font-size: 0.875rem;
553
+ z-index: 300;
554
+ animation: slideIn 0.3s ease;
555
+ box-shadow: var(--shadow);
556
+ }
557
+ .toast.success { border-left: 3px solid var(--success); }
558
+ .toast.error { border-left: 3px solid var(--danger); }
559
+ @keyframes slideIn { from { transform: translateX(100px); opacity: 0; } to { transform: none; opacity: 1; } }
560
+
561
+ /* ===== Utilities ===== */
562
+ .hidden { display: none !important; }
563
+ .loading { padding: 2rem; text-align: center; color: var(--text-muted); }
564
+ .empty-state {
565
+ text-align: center;
566
+ padding: 3rem;
567
+ color: var(--text-muted);
568
+ background: var(--surface);
569
+ border: 1px solid var(--border);
570
+ border-radius: var(--radius);
571
+ font-size: 1rem;
572
+ }
573
+
574
+ /* ===== Scrollbar ===== */
575
+ ::-webkit-scrollbar { width: 6px; }
576
+ ::-webkit-scrollbar-track { background: transparent; }
577
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
578
+
579
+ /* ===== Responsive ===== */
580
+ @media (max-width: 768px) {
581
+ .navbar { padding: 0 1rem; }
582
+ .nav-links { display: none; }
583
+ .main-content { padding: 1.5rem 1rem; }
584
+ .page-title { font-size: 1.8rem; }
585
+ .form-grid { grid-template-columns: 1fr; }
586
+ .chat-layout { grid-template-columns: 1fr; height: auto; }
587
+ .chat-sidebar { display: none; }
588
+ .booking-body { grid-template-columns: 1fr; }
589
+ .search-card .search-row { flex-direction: column; }
590
+ }
static/js/main.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== Backend Status Check =====
2
+ async function checkStatus() {
3
+ const dot = document.getElementById('statusDot');
4
+ if (!dot) return;
5
+ try {
6
+ const res = await fetch('/stats', { signal: AbortSignal.timeout(2000) });
7
+ if (res.ok) {
8
+ dot.classList.add('online');
9
+ dot.classList.remove('offline');
10
+ dot.title = 'Backend connected';
11
+ } else {
12
+ throw new Error();
13
+ }
14
+ } catch {
15
+ dot.classList.add('offline');
16
+ dot.classList.remove('online');
17
+ dot.title = 'Backend offline';
18
+ }
19
+ }
20
+ checkStatus();
21
+
22
+ // ===== Toast Notifications =====
23
+ function showToast(message, type = 'success') {
24
+ const existing = document.querySelector('.toast');
25
+ if (existing) existing.remove();
26
+
27
+ const toast = document.createElement('div');
28
+ toast.className = `toast ${type}`;
29
+ toast.textContent = message;
30
+ document.body.appendChild(toast);
31
+
32
+ setTimeout(() => {
33
+ toast.style.transition = 'opacity 0.3s';
34
+ toast.style.opacity = '0';
35
+ setTimeout(() => toast.remove(), 300);
36
+ }, 3000);
37
+ }
templates/assistant.html ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}AI Assistant β€” BusGo{% endblock %}
3
+ {% block content %}
4
+
5
+ <div class="page-header">
6
+ <h1 class="page-title">AI <span class="accent">Assistant</span></h1>
7
+ <p class="page-sub">Ask anything about routes, fares, providers, or your bookings</p>
8
+ </div>
9
+
10
+ <div class="chat-layout">
11
+ <div class="chat-sidebar">
12
+ <div class="sidebar-section">
13
+ <div class="sidebar-title">πŸ“± Your Phone (optional)</div>
14
+ <input type="text" id="userPhone" class="form-input" placeholder="01XXXXXXXXX">
15
+ <p class="sidebar-hint">Required only for booking-related queries like cancellations.</p>
16
+ </div>
17
+ <div class="sidebar-section">
18
+ <div class="sidebar-title">πŸ’‘ Try asking:</div>
19
+ <div class="suggestions">
20
+ <button class="suggestion-btn" onclick="fillSuggestion(this)">Buses from Dhaka to Rajshahi under 500 taka?</button>
21
+ <button class="suggestion-btn" onclick="fillSuggestion(this)">What are contact details of Hanif Bus?</button>
22
+ <button class="suggestion-btn" onclick="fillSuggestion(this)">Which bus is cheapest to Cox's Bazar?</button>
23
+ <button class="suggestion-btn" onclick="fillSuggestion(this)">Cancel my booking</button>
24
+ <button class="suggestion-btn" onclick="fillSuggestion(this)">What is Ena Paribahan's policy?</button>
25
+ </div>
26
+ </div>
27
+ <button class="btn btn-outline btn-full" onclick="clearChat()" style="margin-top:auto">πŸ—‘οΈ Clear Chat</button>
28
+ </div>
29
+
30
+ <div class="chat-main">
31
+ <div class="chat-messages" id="chatMessages">
32
+ <div class="chat-message assistant">
33
+ <div class="msg-bubble">
34
+ πŸ‘‹ Hello! I'm your BusGo AI assistant. Ask me about bus routes, fares, provider policies, or your bookings. If you need help with a booking cancellation, enter your phone number on the left first.
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="chat-input-area">
40
+ <input type="text" id="chatInput" class="chat-input" placeholder="Type your question..." onkeydown="if(event.key==='Enter') sendMessage()">
41
+ <button class="btn btn-primary chat-send" onclick="sendMessage()">Send ➀</button>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ {% endblock %}
47
+
48
+ {% block scripts %}
49
+ <script>
50
+ const sessionId = 'session-' + Math.random().toString(36).slice(2);
51
+
52
+ function fillSuggestion(btn) {
53
+ document.getElementById('chatInput').value = btn.textContent;
54
+ document.getElementById('chatInput').focus();
55
+ }
56
+
57
+ async function sendMessage() {
58
+ const input = document.getElementById('chatInput');
59
+ const query = input.value.trim();
60
+ if (!query) return;
61
+
62
+ appendMessage('user', query);
63
+ input.value = '';
64
+
65
+ let phone = document.getElementById('userPhone').value.trim();
66
+ if (phone.startsWith('+88')) phone = phone.slice(3);
67
+
68
+ const typingId = appendTyping();
69
+
70
+ try {
71
+ const payload = { query, session_id: sessionId };
72
+ if (phone) payload.phone = phone;
73
+
74
+ const res = await fetch('/query/smart', {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(payload)
78
+ });
79
+ const data = await res.json();
80
+ removeTyping(typingId);
81
+ appendMessage('assistant', data.message || 'Sorry, I could not process that.');
82
+ } catch(e) {
83
+ removeTyping(typingId);
84
+ appendMessage('assistant', '⚠️ Connection error. Please check if the backend is running.');
85
+ }
86
+ }
87
+
88
+ function appendMessage(role, text) {
89
+ const container = document.getElementById('chatMessages');
90
+ const div = document.createElement('div');
91
+ div.className = `chat-message ${role}`;
92
+ div.innerHTML = `<div class="msg-bubble">${text.replace(/\n/g, '<br>')}</div>`;
93
+ container.appendChild(div);
94
+ container.scrollTop = container.scrollHeight;
95
+ }
96
+
97
+ function appendTyping() {
98
+ const container = document.getElementById('chatMessages');
99
+ const id = 'typing-' + Date.now();
100
+ const div = document.createElement('div');
101
+ div.className = 'chat-message assistant';
102
+ div.id = id;
103
+ div.innerHTML = `<div class="msg-bubble typing"><span></span><span></span><span></span></div>`;
104
+ container.appendChild(div);
105
+ container.scrollTop = container.scrollHeight;
106
+ return id;
107
+ }
108
+
109
+ function removeTyping(id) {
110
+ const el = document.getElementById(id);
111
+ if (el) el.remove();
112
+ }
113
+
114
+ async function clearChat() {
115
+ document.getElementById('chatMessages').innerHTML = `
116
+ <div class="chat-message assistant">
117
+ <div class="msg-bubble">Chat cleared! How can I help you?</div>
118
+ </div>`;
119
+ try {
120
+ await fetch(`/chat/clear?session_id=${sessionId}`, { method: 'POST' });
121
+ } catch(e) {}
122
+ }
123
+ </script>
124
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}BusGo β€” Ticket Booking{% endblock %}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="/static/css/style.css">
10
+ </head>
11
+ <body>
12
+
13
+ <nav class="navbar">
14
+ <a href="/" class="logo">🚌 BusGo</a>
15
+ <div class="nav-links">
16
+ <a href="/" class="nav-link {% if active == 'book' %}active{% endif %}">Book Ticket</a>
17
+ <a href="/bookings-page" class="nav-link {% if active == 'bookings' %}active{% endif %}">My Bookings</a>
18
+ <a href="/providers-page" class="nav-link {% if active == 'providers' %}active{% endif %}">Providers</a>
19
+ <a href="/routes-page" class="nav-link {% if active == 'routes' %}active{% endif %}">Routes & Fares</a>
20
+ <a href="/assistant-page" class="nav-link {% if active == 'assistant' %}active{% endif %}">AI Assistant</a>
21
+ </div>
22
+ <div class="nav-status" id="statusDot" title="Checking backend...">●</div>
23
+ </nav>
24
+
25
+ <main class="main-content">
26
+ {% block content %}{% endblock %}
27
+ </main>
28
+
29
+ <script src="/static/js/main.js"></script>
30
+ {% block scripts %}{% endblock %}
31
+ </body>
32
+ </html>
templates/bookings.html ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}My Bookings β€” BusGo{% endblock %}
3
+ {% block content %}
4
+
5
+ <div class="page-header">
6
+ <h1 class="page-title">My <span class="accent">Bookings</span></h1>
7
+ <p class="page-sub">View and manage your ticket reservations</p>
8
+ </div>
9
+
10
+ <div class="card search-card">
11
+ <div class="search-row">
12
+ <input type="text" id="phoneInput" class="form-input" placeholder="Enter phone number β€” 01XXXXXXXXX" style="flex:1">
13
+ <button class="btn btn-primary" onclick="searchBookings()">πŸ” Search</button>
14
+ </div>
15
+ </div>
16
+
17
+ <div id="bookingsResult"></div>
18
+
19
+ {% endblock %}
20
+
21
+ {% block scripts %}
22
+ <script>
23
+ async function searchBookings() {
24
+ let phone = document.getElementById('phoneInput').value.trim();
25
+ if (phone.startsWith('+88')) phone = phone.slice(3);
26
+ if (!phone) return showToast('Please enter your phone number.', 'error');
27
+
28
+ const result = document.getElementById('bookingsResult');
29
+ result.innerHTML = '<div class="loading">Searching...</div>';
30
+
31
+ try {
32
+ const res = await fetch(`/bookings/phone/${encodeURIComponent(phone)}`);
33
+ const data = await res.json();
34
+
35
+ if (!res.ok) {
36
+ result.innerHTML = `<div class="empty-state">πŸ“­ No bookings found for this number.</div>`;
37
+ return;
38
+ }
39
+
40
+ const bookings = data.bookings || [];
41
+ if (!bookings.length) {
42
+ result.innerHTML = `<div class="empty-state">πŸ“­ No bookings found.</div>`;
43
+ return;
44
+ }
45
+
46
+ result.innerHTML = `<div class="result-count">Found ${bookings.length} booking(s)</div>` +
47
+ bookings.map(b => `
48
+ <div class="booking-item ${b.status}" id="booking-${b.booking_id}">
49
+ <div class="booking-header">
50
+ <div class="booking-id">${b.booking_id}</div>
51
+ <div class="booking-status status-${b.status}">${b.status.toUpperCase()}</div>
52
+ </div>
53
+ <div class="booking-body">
54
+ <div class="booking-col">
55
+ <div class="booking-field"><span>Passenger</span><strong>${b.name}</strong></div>
56
+ <div class="booking-field"><span>Phone</span><strong>${b.phone}</strong></div>
57
+ <div class="booking-field"><span>Passengers</span><strong>${b.num_passengers}</strong></div>
58
+ </div>
59
+ <div class="booking-col">
60
+ <div class="booking-field"><span>Provider</span><strong>${b.bus_provider}</strong></div>
61
+ <div class="booking-field"><span>Route</span><strong>${b.from_district} β†’ ${b.to_district}</strong></div>
62
+ <div class="booking-field"><span>Dropping Point</span><strong>${b.dropping_point}</strong></div>
63
+ <div class="booking-field"><span>Travel Date</span><strong>${b.travel_date}</strong></div>
64
+ </div>
65
+ <div class="booking-col">
66
+ <div class="booking-field"><span>Fare/person</span><strong>${b.fare} Taka</strong></div>
67
+ <div class="booking-field total-field"><span>Total</span><strong>${b.total_amount} Taka</strong></div>
68
+ <div class="booking-field"><span>Booked On</span><strong>${b.booking_date.slice(0,10)}</strong></div>
69
+ </div>
70
+ </div>
71
+ ${b.status === 'active' ? `
72
+ <div class="booking-footer">
73
+ <button class="btn btn-danger" onclick="cancelBooking('${b.booking_id}')">❌ Cancel Booking</button>
74
+ </div>` : ''}
75
+ </div>`).join('');
76
+ } catch(e) {
77
+ result.innerHTML = `<div class="empty-state">⚠️ Connection error. Is the backend running?</div>`;
78
+ }
79
+ }
80
+
81
+ async function cancelBooking(bookingId) {
82
+ if (!confirm('Are you sure you want to cancel this booking?')) return;
83
+ try {
84
+ const res = await fetch(`/bookings/${bookingId}?permanent=false`, { method: 'DELETE' });
85
+ if (res.ok) {
86
+ const item = document.getElementById(`booking-${bookingId}`);
87
+ item.querySelector('.booking-status').textContent = 'CANCELLED';
88
+ item.querySelector('.booking-status').className = 'booking-status status-cancelled';
89
+ item.querySelector('.booking-footer').remove();
90
+ item.classList.remove('active');
91
+ item.classList.add('cancelled');
92
+ showToast('Booking cancelled successfully.', 'success');
93
+ } else {
94
+ const d = await res.json();
95
+ showToast(d.detail || 'Failed to cancel.', 'error');
96
+ }
97
+ } catch(e) { showToast('Connection error.', 'error'); }
98
+ }
99
+
100
+ // Allow Enter key to search
101
+ document.getElementById('phoneInput').addEventListener('keydown', e => {
102
+ if (e.key === 'Enter') searchBookings();
103
+ });
104
+ </script>
105
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Book Ticket β€” BusGo{% endblock %}
3
+ {% block content %}
4
+
5
+ <div class="page-header">
6
+ <h1 class="page-title">Book Your <span class="accent">Ticket</span></h1>
7
+ <p class="page-sub">Fast, simple bus booking across Bangladesh</p>
8
+ </div>
9
+
10
+ <div class="card booking-card">
11
+ <div class="step-label">Step 1 β€” Route Details</div>
12
+
13
+ <div class="form-grid">
14
+ <div class="form-group">
15
+ <label class="form-label">Bus Provider</label>
16
+ <select id="providerSelect" class="form-select" onchange="onProviderChange()">
17
+ <option value="">Select provider...</option>
18
+ {% for p in providers %}
19
+ <option value="{{ p.name }}" data-districts="{{ p.coverage_districts | join(',') }}">{{ p.name }}</option>
20
+ {% endfor %}
21
+ </select>
22
+ </div>
23
+
24
+ <div class="form-group">
25
+ <label class="form-label">From District</label>
26
+ <select id="fromDistrict" class="form-select" onchange="onFromChange()" disabled>
27
+ <option value="">Select provider first...</option>
28
+ </select>
29
+ </div>
30
+
31
+ <div class="form-group">
32
+ <label class="form-label">To District</label>
33
+ <select id="toDistrict" class="form-select" onchange="onToChange()" disabled>
34
+ <option value="">Select from district...</option>
35
+ </select>
36
+ </div>
37
+
38
+ <div class="form-group">
39
+ <label class="form-label">Dropping Point</label>
40
+ <select id="droppingPoint" class="form-select" onchange="onDropChange()" disabled>
41
+ <option value="">Select destination...</option>
42
+ </select>
43
+ </div>
44
+ </div>
45
+
46
+ <div id="routeSummary" class="route-summary hidden">
47
+ <span id="summaryText"></span>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="card booking-card">
52
+ <div class="step-label">Step 2 β€” Passenger Details</div>
53
+
54
+ <div class="form-grid">
55
+ <div class="form-group">
56
+ <label class="form-label">Full Name</label>
57
+ <input type="text" id="passengerName" class="form-input" placeholder="Enter your full name">
58
+ </div>
59
+
60
+ <div class="form-group">
61
+ <label class="form-label">Phone Number</label>
62
+ <input type="text" id="passengerPhone" class="form-input" placeholder="01XXXXXXXXX">
63
+ </div>
64
+
65
+ <div class="form-group">
66
+ <label class="form-label">Number of Passengers</label>
67
+ <input type="number" id="numPassengers" class="form-input" value="1" min="1" max="10" onchange="updateTotal()">
68
+ </div>
69
+
70
+ <div class="form-group">
71
+ <label class="form-label">Travel Date</label>
72
+ <input type="date" id="travelDate" class="form-input">
73
+ </div>
74
+ </div>
75
+
76
+ <div id="totalBox" class="total-box hidden">
77
+ πŸ’° Total: <strong id="totalAmount">0</strong> Taka
78
+ </div>
79
+
80
+ <button class="btn btn-primary btn-full" onclick="submitBooking()">
81
+ 🎫 Confirm Booking
82
+ </button>
83
+ </div>
84
+
85
+ <!-- Success Modal -->
86
+ <div id="successModal" class="modal hidden">
87
+ <div class="modal-box">
88
+ <div class="modal-icon">βœ…</div>
89
+ <h2>Booking Confirmed!</h2>
90
+ <div id="modalContent" class="modal-content"></div>
91
+ <button class="btn btn-primary" onclick="closeModal()">Done</button>
92
+ </div>
93
+ </div>
94
+
95
+ {% endblock %}
96
+
97
+ {% block scripts %}
98
+ <script>
99
+ // Set min date to tomorrow
100
+ const tomorrow = new Date();
101
+ tomorrow.setDate(tomorrow.getDate() + 1);
102
+ document.getElementById('travelDate').min = tomorrow.toISOString().split('T')[0];
103
+ document.getElementById('travelDate').value = tomorrow.toISOString().split('T')[0];
104
+
105
+ function onProviderChange() {
106
+ const sel = document.getElementById('providerSelect');
107
+ const opt = sel.options[sel.selectedIndex];
108
+ const from = document.getElementById('fromDistrict');
109
+ const to = document.getElementById('toDistrict');
110
+ const dp = document.getElementById('droppingPoint');
111
+
112
+ from.innerHTML = '<option value="">Select from district...</option>';
113
+ to.innerHTML = '<option value="">Select to district...</option>';
114
+ dp.innerHTML = '<option value="">Select destination first...</option>';
115
+ to.disabled = true;
116
+ dp.disabled = true;
117
+ hideRouteSummary();
118
+
119
+ if (!opt.value) { from.disabled = true; return; }
120
+
121
+ const districts = opt.dataset.districts.split(',');
122
+ districts.forEach(d => {
123
+ const o = document.createElement('option');
124
+ o.value = d.trim(); o.textContent = d.trim();
125
+ from.appendChild(o);
126
+ });
127
+ from.disabled = false;
128
+ }
129
+
130
+ function onFromChange() {
131
+ const from = document.getElementById('fromDistrict').value;
132
+ const provSel = document.getElementById('providerSelect');
133
+ const opt = provSel.options[provSel.selectedIndex];
134
+ const to = document.getElementById('toDistrict');
135
+ const dp = document.getElementById('droppingPoint');
136
+
137
+ to.innerHTML = '<option value="">Select to district...</option>';
138
+ dp.innerHTML = '<option value="">Select destination first...</option>';
139
+ dp.disabled = true;
140
+ hideRouteSummary();
141
+
142
+ if (!from) { to.disabled = true; return; }
143
+
144
+ const districts = opt.dataset.districts.split(',').map(d => d.trim()).filter(d => d !== from);
145
+ districts.forEach(d => {
146
+ const o = document.createElement('option');
147
+ o.value = d; o.textContent = d;
148
+ to.appendChild(o);
149
+ });
150
+ to.disabled = false;
151
+ }
152
+
153
+ async function onToChange() {
154
+ const to = document.getElementById('toDistrict').value;
155
+ const dp = document.getElementById('droppingPoint');
156
+ dp.innerHTML = '<option value="">Loading...</option>';
157
+ dp.disabled = true;
158
+ hideRouteSummary();
159
+
160
+ if (!to) return;
161
+
162
+ const res = await fetch(`/dropping-points/${encodeURIComponent(to)}`);
163
+ const data = await res.json();
164
+ dp.innerHTML = '<option value="">Select dropping point...</option>';
165
+ (data.dropping_points || []).forEach(pt => {
166
+ const o = document.createElement('option');
167
+ o.value = pt.name;
168
+ o.textContent = `${pt.name} β€” ${pt.price} Taka`;
169
+ o.dataset.price = pt.price;
170
+ dp.appendChild(o);
171
+ });
172
+ dp.disabled = false;
173
+ }
174
+
175
+ function onDropChange() {
176
+ updateTotal();
177
+ const from = document.getElementById('fromDistrict').value;
178
+ const to = document.getElementById('toDistrict').value;
179
+ const dp = document.getElementById('droppingPoint');
180
+ const opt = dp.options[dp.selectedIndex];
181
+ if (opt.value) {
182
+ document.getElementById('summaryText').textContent =
183
+ `βœ… ${from} β†’ ${to} | ${opt.value} | ${opt.dataset.price} Taka/person`;
184
+ document.getElementById('routeSummary').classList.remove('hidden');
185
+ } else {
186
+ hideRouteSummary();
187
+ }
188
+ }
189
+
190
+ function updateTotal() {
191
+ const dp = document.getElementById('droppingPoint');
192
+ const opt = dp.options[dp.selectedIndex];
193
+ const n = parseInt(document.getElementById('numPassengers').value) || 1;
194
+ if (opt && opt.dataset.price) {
195
+ const total = parseInt(opt.dataset.price) * n;
196
+ document.getElementById('totalAmount').textContent = total;
197
+ document.getElementById('totalBox').classList.remove('hidden');
198
+ }
199
+ }
200
+
201
+ function hideRouteSummary() {
202
+ document.getElementById('routeSummary').classList.add('hidden');
203
+ document.getElementById('totalBox').classList.add('hidden');
204
+ }
205
+
206
+ async function submitBooking() {
207
+ const provider = document.getElementById('providerSelect').value;
208
+ const from = document.getElementById('fromDistrict').value;
209
+ const to = document.getElementById('toDistrict').value;
210
+ const dp = document.getElementById('droppingPoint').value;
211
+ const name = document.getElementById('passengerName').value.trim();
212
+ let phone = document.getElementById('passengerPhone').value.trim();
213
+ const n = parseInt(document.getElementById('numPassengers').value);
214
+ const date = document.getElementById('travelDate').value;
215
+
216
+ if (!provider || !from || !to || !dp) return showToast('Please complete route selection.', 'error');
217
+ if (!name) return showToast('Please enter your name.', 'error');
218
+ if (phone.startsWith('+88')) phone = phone.slice(3);
219
+ if (phone.length < 11 || !/^\d+$/.test(phone)) return showToast('Enter a valid 11-digit phone number.', 'error');
220
+
221
+ const btn = document.querySelector('.btn-primary');
222
+ btn.disabled = true; btn.textContent = 'Booking...';
223
+
224
+ try {
225
+ const res = await fetch('/bookings', {
226
+ method: 'POST',
227
+ headers: {'Content-Type': 'application/json'},
228
+ body: JSON.stringify({
229
+ name, phone, bus_provider: provider,
230
+ from_district: from, to_district: to,
231
+ dropping_point: dp, travel_date: date, num_passengers: n
232
+ })
233
+ });
234
+ const data = await res.json();
235
+ if (res.ok) {
236
+ document.getElementById('modalContent').innerHTML = `
237
+ <div class="booking-result">
238
+ <div class="result-row"><span>Booking ID</span><strong>${data.booking_id}</strong></div>
239
+ <div class="result-row"><span>Name</span><strong>${data.name}</strong></div>
240
+ <div class="result-row"><span>Route</span><strong>${data.from_district} β†’ ${data.to_district}</strong></div>
241
+ <div class="result-row"><span>Dropping Point</span><strong>${data.dropping_point}</strong></div>
242
+ <div class="result-row"><span>Provider</span><strong>${data.bus_provider}</strong></div>
243
+ <div class="result-row"><span>Date</span><strong>${data.travel_date}</strong></div>
244
+ <div class="result-row"><span>Passengers</span><strong>${data.num_passengers}</strong></div>
245
+ <div class="result-row total-row"><span>Total</span><strong>${data.total_amount} Taka</strong></div>
246
+ </div>`;
247
+ document.getElementById('successModal').classList.remove('hidden');
248
+ } else {
249
+ showToast(data.detail || 'Booking failed.', 'error');
250
+ }
251
+ } catch(e) { showToast('Connection error.', 'error'); }
252
+ btn.disabled = false; btn.textContent = '🎫 Confirm Booking';
253
+ }
254
+
255
+ function closeModal() {
256
+ document.getElementById('successModal').classList.add('hidden');
257
+ location.reload();
258
+ }
259
+ </script>
260
+ {% endblock %}
templates/providers.html ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Providers β€” BusGo{% endblock %}
3
+ {% block content %}
4
+
5
+ <div class="page-header">
6
+ <h1 class="page-title">Bus <span class="accent">Providers</span></h1>
7
+ <p class="page-sub">Explore all available bus services</p>
8
+ </div>
9
+
10
+ <div class="providers-grid">
11
+ {% for provider in providers %}
12
+ <div class="provider-card">
13
+ <div class="provider-header">
14
+ <div class="provider-icon">🚌</div>
15
+ <div>
16
+ <h3 class="provider-name">{{ provider.name }}</h3>
17
+ <div class="provider-meta">{{ provider.coverage_districts | length }} districts covered</div>
18
+ </div>
19
+ </div>
20
+
21
+ <details class="coverage-details">
22
+ <summary>View Coverage Districts</summary>
23
+ <div class="district-tags">
24
+ {% for d in provider.coverage_districts %}
25
+ <span class="district-tag">{{ d }}</span>
26
+ {% endfor %}
27
+ </div>
28
+ </details>
29
+
30
+ <button class="btn btn-outline btn-full" onclick="loadPolicy('{{ provider.name }}', this)">
31
+ πŸ“„ View Policy
32
+ </button>
33
+
34
+ <div id="policy-{{ loop.index }}" class="policy-box hidden" data-index="{{ loop.index }}"></div>
35
+ </div>
36
+ {% endfor %}
37
+ </div>
38
+
39
+ {% endblock %}
40
+
41
+ {% block scripts %}
42
+ <script>
43
+ async function loadPolicy(providerName, btn) {
44
+ const idx = btn.closest('.provider-card').querySelector('.policy-box').dataset.index;
45
+ const box = document.getElementById(`policy-${idx}`);
46
+
47
+ if (!box.classList.contains('hidden')) {
48
+ box.classList.add('hidden');
49
+ btn.textContent = 'πŸ“„ View Policy';
50
+ return;
51
+ }
52
+
53
+ btn.textContent = 'Loading...';
54
+ btn.disabled = true;
55
+
56
+ try {
57
+ const res = await fetch(`/providers/${encodeURIComponent(providerName)}/policy`);
58
+ const data = await res.json();
59
+ if (res.ok) {
60
+ box.textContent = data.policy;
61
+ box.classList.remove('hidden');
62
+ btn.textContent = 'πŸ™ˆ Hide Policy';
63
+ } else {
64
+ showToast('Policy not available for this provider.', 'error');
65
+ btn.textContent = 'πŸ“„ View Policy';
66
+ }
67
+ } catch(e) {
68
+ showToast('Connection error.', 'error');
69
+ btn.textContent = 'πŸ“„ View Policy';
70
+ }
71
+ btn.disabled = false;
72
+ }
73
+ </script>
74
+ {% endblock %}
templates/routes.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Routes & Fares β€” BusGo{% endblock %}
3
+ {% block content %}
4
+
5
+ <div class="page-header">
6
+ <h1 class="page-title">Routes & <span class="accent">Fares</span></h1>
7
+ <p class="page-sub">Browse all destinations and ticket prices</p>
8
+ </div>
9
+
10
+ <div class="card" style="margin-bottom: 2rem;">
11
+ <input type="text" id="searchInput" class="form-input" placeholder="πŸ” Search district or dropping point..." oninput="filterDistricts()">
12
+ </div>
13
+
14
+ <div class="districts-grid" id="districtsGrid">
15
+ {% for district in districts %}
16
+ <div class="district-card" data-name="{{ district.name | lower }}" data-points="{{ district.dropping_points | map(attribute='name') | join(',') | lower }}">
17
+ <div class="district-header">
18
+ <h3 class="district-name">πŸ“ {{ district.name }}</h3>
19
+ <div class="district-count">{{ district.dropping_points | length }} stops</div>
20
+ </div>
21
+
22
+ {% if district.dropping_points %}
23
+ <div class="price-range">
24
+ <span class="price-label">From</span>
25
+ <span class="price-val">{{ district.dropping_points | map(attribute='price') | min }} Taka</span>
26
+ <span class="price-label">to</span>
27
+ <span class="price-val">{{ district.dropping_points | map(attribute='price') | max }} Taka</span>
28
+ </div>
29
+
30
+ <details class="stops-details">
31
+ <summary>View Dropping Points</summary>
32
+ <div class="stops-list">
33
+ {% for dp in district.dropping_points %}
34
+ <div class="stop-row">
35
+ <span class="stop-name">🚏 {{ dp.name }}</span>
36
+ <span class="stop-price">{{ dp.price }} Taka</span>
37
+ </div>
38
+ {% endfor %}
39
+ </div>
40
+ </details>
41
+ {% endif %}
42
+ </div>
43
+ {% endfor %}
44
+ </div>
45
+
46
+ {% endblock %}
47
+
48
+ {% block scripts %}
49
+ <script>
50
+ function filterDistricts() {
51
+ const q = document.getElementById('searchInput').value.toLowerCase();
52
+ document.querySelectorAll('.district-card').forEach(card => {
53
+ const name = card.dataset.name;
54
+ const points = card.dataset.points;
55
+ card.style.display = (name.includes(q) || points.includes(q)) ? '' : 'none';
56
+ });
57
+ }
58
+ </script>
59
+ {% endblock %}