Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added BOOKING_INTEGRATION.md
Empty file.
290 changes: 290 additions & 0 deletions src/app/api/ragbot/booking-suggestions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// src/app/api/ragbot/booking-suggestions/route.ts
import { NextRequest, NextResponse } from 'next/server';
import connectDB from '@/lib/db';
import Department from '@/lib/models/departmentSchema';

interface ServiceSchema {
id: string;
name: string;
description?: string;
category?: string;
isActive?: boolean;
processingTime?: string;
fee?: number;
requirements?: string[];
}
interface DepartmentData {
id: string;
departmentId: string;
name: string;
shortName: string;
description: string;
services?: ServiceData[];
}

interface ServiceData {
id: string;
name: string;
description?: string;
category?: string;
processingTime?: string;
fee?: number;
requirements?: string[];
departmentId?: string;
departmentName?: string;
}

interface BookingSuggestion {
type: 'department' | 'service';
id: string;
name: string;
description?: string;
departmentId?: string;
departmentName?: string;
category?: string;
fee?: number;
relevanceScore: number;
}

/**
* Server-side booking suggestions for the RAG bot
*/
export async function POST(request: NextRequest) {
try {
// Add proper error handling for JSON parsing
let body;
try {
body = await request.json();
} catch (jsonError) {
console.error('❌ Invalid JSON in request body:', jsonError);
return NextResponse.json(
{
error: 'Invalid JSON in request body',
suggestions: [],
details: jsonError instanceof Error ? jsonError.message : 'Unknown JSON parsing error'
},
{ status: 400 }
);
}

const { query } = body;

if (!query) {
return NextResponse.json(
{
error: 'Query is required',
suggestions: []
},
{ status: 400 }
);
}

console.log('🔍 Booking suggestions API called with query:', query);

await connectDB();

// Fetch departments with services
const departments = await Department.find(
{
$or: [
{ status: 'ACTIVE' },
{ status: 'active' }
]
},
{
_id: 1,
departmentId: 1,
name: 1,
shortName: 1,
description: 1,
services: 1
}
).lean();

console.log('🏛️ Raw departments from DB:', departments.length);
console.log('📊 First department services:', departments[0]?.services?.length || 0);

// Transform data to match booking helper format
const departmentData: DepartmentData[] = departments.map(dept => ({
id: dept._id ? String(dept._id) : '',
departmentId: dept.departmentId || '',
name: dept.name || '',
shortName: dept.shortName || '',
description: dept.description || '',
services: (dept.services || [])
.filter((service: ServiceSchema) => service.isActive)
.map((service: ServiceSchema) => ({
id: service.id || '',
name: service.name || '',
description: service.description || '',
category: service.category || '',
processingTime: service.processingTime || '',
fee: service.fee || 0,
requirements: service.requirements || [],
departmentId: dept.departmentId || '',
departmentName: dept.name || ''
}))
}));
Comment on lines +116 to +127
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Service IDs may be empty; include _id fallback when mapping services

Many department.service items hold only _id (not id). Mapping only service.id risks empty IDs, breaking selection and URL pre-fill.

Apply this diff to include an _id fallback:

-        .map((service: ServiceSchema) => ({
-          id: service.id || '',
+        .map((service: ServiceSchema) => ({
+          id: (service as any).id || ((service as any)._id ? String((service as any)._id) : ''),
           name: service.name || '',
           description: service.description || '',
           category: service.category || '',
           processingTime: service.processingTime || '',
           fee: service.fee || 0,
           requirements: service.requirements || [],
           departmentId: dept.departmentId || '',
           departmentName: dept.name || ''
         }))

Optionally extend ServiceSchema to reflect _id:

 interface ServiceSchema {
+  _id?: string;
   id: string;
   name: string;
   description?: string;
   category?: string;
   isActive?: boolean;
   processingTime?: string;
   fee?: number;
   requirements?: string[];
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/api/ragbot/booking-suggestions/route.ts around lines 116 to 127 the
mapping uses service.id which can be empty when the object only contains _id;
update the mapping to use service.id || service._id || '' for the id field (and
any other fields that may live under an _ prefixed key if applicable), and
optionally extend the ServiceSchema/type to include _id?: string so the compiler
knows the fallback exists; keep all other fields the same and ensure downstream
uses expect the resolved id.


// Flatten all services
const services: ServiceData[] = departmentData.flatMap(dept => dept.services || []);

// Generate suggestions using server-side logic
const suggestions = generateBookingSuggestions(query, departmentData, services);

console.log('📊 Generated suggestions:', suggestions.length);
console.log('🎯 Suggestions:', suggestions);
console.log('🏛️ Departments found:', departmentData.length);
console.log('📋 Services found:', services.length);

return NextResponse.json({
success: true,
suggestions,
departments: departmentData,
services
});

} catch (error) {
console.error('Error generating booking suggestions:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

return NextResponse.json(
{
error: 'Internal server error',
details: errorMessage
},
{ status: 500 }
);
}
}

/**
* Server-side suggestion logic (similar to client-side but optimized for server)
*/
function generateBookingSuggestions(
query: string,
departments: DepartmentData[],
services: ServiceData[]
): BookingSuggestion[] {
const normalizedQuery = query.toLowerCase();
const suggestions: BookingSuggestion[] = [];

// Keywords mapping for better matching
const serviceKeywords: Record<string, string[]> = {
passport: ['passport', 'travel', 'immigration', 'visa', 'abroad', 'international'],
license: ['license', 'permit', 'driving', 'vehicle', 'motorcycle', 'car'],
certificate: ['certificate', 'birth', 'death', 'marriage', 'divorce', 'citizenship'],
registration: ['register', 'registration', 'business', 'company', 'organization'],
tax: ['tax', 'income', 'vat', 'customs', 'duty', 'revenue'],
education: ['education', 'school', 'university', 'degree', 'scholarship', 'student'],
health: ['health', 'medical', 'hospital', 'medicine', 'doctor', 'treatment'],
property: ['property', 'land', 'house', 'building', 'real estate', 'title'],
insurance: ['insurance', 'social security', 'pension', 'retirement', 'benefits'],
employment: ['employment', 'job', 'work', 'labor', 'salary', 'employee']
};

// Department keywords mapping
const departmentKeywords: Record<string, string[]> = {
immigration: ['immigration', 'passport', 'visa', 'travel', 'foreign'],
transport: ['transport', 'vehicle', 'driving', 'license', 'road'],
registrar: ['registrar', 'birth', 'death', 'marriage', 'certificate'],
revenue: ['revenue', 'tax', 'customs', 'duty', 'vat'],
education: ['education', 'ministry of education', 'school', 'university'],
health: ['health', 'ministry of health', 'medical', 'hospital'],
lands: ['lands', 'property', 'title', 'survey', 'real estate']
};

// Score services based on keyword matching
services.forEach(service => {
let relevanceScore = 0;

// Direct name matching
if (service.name.toLowerCase().includes(normalizedQuery)) {
relevanceScore += 10;
}

// Description matching
if (service.description && service.description.toLowerCase().includes(normalizedQuery)) {
relevanceScore += 7;
}

// Category matching
if (service.category && service.category.toLowerCase().includes(normalizedQuery)) {
relevanceScore += 5;
}

// Keyword matching
Object.entries(serviceKeywords).forEach(([category, keywords]) => {
keywords.forEach(keyword => {
if (normalizedQuery.includes(keyword)) {
if (service.name.toLowerCase().includes(category)) {
relevanceScore += 8;
} else if (service.description?.toLowerCase().includes(category)) {
relevanceScore += 6;
} else if (service.category?.toLowerCase().includes(category)) {
relevanceScore += 4;
}
}
});
});

if (relevanceScore > 0) {
suggestions.push({
type: 'service',
id: service.id,
name: service.name,
description: service.description,
departmentId: service.departmentId,
departmentName: service.departmentName,
category: service.category,
fee: service.fee,
relevanceScore
});
}
});

// Score departments based on keyword matching
departments.forEach(dept => {
let relevanceScore = 0;

// Direct name matching
if (dept.name.toLowerCase().includes(normalizedQuery) ||
dept.shortName.toLowerCase().includes(normalizedQuery)) {
relevanceScore += 8;
}

// Description matching
if (dept.description && dept.description.toLowerCase().includes(normalizedQuery)) {
relevanceScore += 6;
}

// Keyword matching for departments
Object.entries(departmentKeywords).forEach(([category, keywords]) => {
keywords.forEach(keyword => {
if (normalizedQuery.includes(keyword)) {
if (dept.name.toLowerCase().includes(category) ||
dept.shortName.toLowerCase().includes(category)) {
relevanceScore += 7;
} else if (dept.description?.toLowerCase().includes(category)) {
relevanceScore += 5;
}
}
});
});

if (relevanceScore > 0) {
suggestions.push({
type: 'department',
id: dept.departmentId,
name: dept.name,
description: dept.description,
relevanceScore
});
}
});

// Sort by relevance score and return top 5
return suggestions
.sort((a, b) => b.relevanceScore - a.relevanceScore)
.slice(0, 5);
}
47 changes: 47 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,51 @@
border: 2px solid #00ff00 !important;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
}

.markdown-content a {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin: 0 0.125rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background: linear-gradient(135deg,
rgba(255, 199, 44, 0.1) 0%,
rgba(255, 87, 34, 0.1) 100%);
border: 1px solid rgba(255, 199, 44, 0.3);
color: #FFC72C;
font-weight: 500;
font-size: 0.875rem;
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(4px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.markdown-content a:hover {
color: #FF5722;
border-color: rgba(255, 87, 34, 0.5);
background: linear-gradient(135deg,
rgba(255, 199, 44, 0.15) 0%,
rgba(255, 87, 34, 0.15) 100%);
box-shadow: 0 4px 12px rgba(255, 199, 44, 0.2);
transform: translateY(-1px);
}

/* Dark mode adjustments */
:root.dark .markdown-content a {
background: linear-gradient(135deg,
rgba(255, 199, 44, 0.08) 0%,
rgba(255, 87, 34, 0.08) 100%);
border-color: rgba(255, 199, 44, 0.25);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

:root.dark .markdown-content a:hover {
background: linear-gradient(135deg,
rgba(255, 199, 44, 0.12) 0%,
rgba(255, 87, 34, 0.12) 100%);
box-shadow: 0 4px 12px rgba(255, 199, 44, 0.15);
}
Loading
Loading