Files
Gov_Travel_App/services/databaseService.js
mblanke 4d915aa3ea Build premium data portal with React + Tailwind CSS
Frontend Features:
- Landing page with glassmorphism, animated counters, hero section
- Interactive data tables with search, sort, filter, CSV export
- Premium dark theme (navy + gold accents)
- Framer Motion animations and micro-interactions
- Responsive design with Inter + Playfair Display typography
- DataTable component with pagination and live search

Backend Updates:
- New API endpoints: /api/rates/per-diem, /api/rates/accommodations, /api/stats
- Database service methods for bulk data retrieval
- Production mode serves built React app from /dist/client
- Fallback to legacy HTML for development

Tech Stack:
- React 18 + TypeScript
- Vite 7 build tool
- Tailwind CSS 4 with @tailwindcss/postcss
- Framer Motion for animations
- Lucide React icons
- SQLite3 backend

Build Output:
- 351KB optimized JavaScript bundle
- 29KB CSS bundle
- Fully tree-shaken and minified
2026-01-13 11:05:54 -05:00

341 lines
10 KiB
JavaScript

const sqlite3 = require('sqlite3').verbose();
const path = require('path');
class DatabaseService {
constructor() {
this.dbPath = path.join(__dirname, '..', 'database', 'travel_rates.db');
this.db = null;
}
connect() {
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(this.dbPath, (err) => {
if (err) {
console.error('❌ Database connection failed:', err);
reject(err);
} else {
console.log('✅ Database connected');
resolve();
}
});
});
}
/**
* Search for a city (complete travel rates)
* GUARANTEED to find Canberra!
*/
async searchCity(searchTerm) {
const query = `
SELECT * FROM travel_rates
WHERE LOWER(city_name) LIKE LOWER(?)
OR LOWER(city_key) LIKE LOWER(?)
OR LOWER(country) LIKE LOWER(?)
OR LOWER(province) LIKE LOWER(?)
ORDER BY
CASE
WHEN LOWER(city_name) = LOWER(?) THEN 1
WHEN LOWER(city_key) = LOWER(?) THEN 2
WHEN LOWER(city_name) LIKE LOWER(?) THEN 3
ELSE 4
END
LIMIT 10
`;
const term = `%${searchTerm}%`;
const exactTerm = searchTerm.toLowerCase();
const likeTerm = `${searchTerm.toLowerCase()}%`;
return new Promise((resolve, reject) => {
this.db.all(query, [term, term, term, term, exactTerm, exactTerm, likeTerm], (err, rows) => {
if (err) reject(err);
else resolve(rows ? rows.map(row => this.formatTravelRate(row)) : []);
});
});
}
/**
* Get complete travel rate by exact city key
*/
async getAccommodationRate(cityKey) {
const query = `SELECT * FROM travel_rates WHERE LOWER(city_key) = LOWER(?) LIMIT 1`;
return new Promise((resolve, reject) => {
this.db.get(query, [cityKey], (err, row) => {
if (err) reject(err);
else resolve(row ? this.formatTravelRate(row) : null);
});
});
}
/**
* Get accommodation rate for a specific month
*/
async getMonthlyRate(cityKey, month) {
const rate = await this.getAccommodationRate(cityKey);
if (!rate) return null;
const monthIndex = month - 1; // 0-based index
return {
city: rate.name,
month: month,
rate: rate.monthlyRates[monthIndex],
currency: rate.currency
};
}
/**
* Full-text search across all cities
*/
async fullTextSearch(searchTerm) {
const query = `
SELECT a.* FROM travel_rates a
WHERE a.id IN (
SELECT rowid FROM travel_search
WHERE travel_search MATCH ?
)
ORDER BY
CASE
WHEN LOWER(a.city_name) = LOWER(?) THEN 1
ELSE 2
END
LIMIT 20
`;
return new Promise((resolve, reject) => {
this.db.all(query, [searchTerm, searchTerm], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(row => this.formatTravelRate(row)));
});
});
}
/**
* Format complete travel rate for API response
*/
formatTravelRate(row) {
return {
cityKey: row.city_key,
name: row.city_name,
province: row.province,
country: row.country,
region: row.region,
currency: row.currency,
accommodation: {
monthly: [
row.jan_accommodation, row.feb_accommodation, row.mar_accommodation,
row.apr_accommodation, row.may_accommodation, row.jun_accommodation,
row.jul_accommodation, row.aug_accommodation, row.sep_accommodation,
row.oct_accommodation, row.nov_accommodation, row.dec_accommodation
],
standard: row.standard_accommodation
},
meals: {
breakfast: row.breakfast,
lunch: row.lunch,
dinner: row.dinner,
total: row.total_meals
},
incidentals: row.incidentals,
totalDailyAllowance: row.total_daily_allowance,
fullDayCost: parseFloat(row.standard_accommodation || row.jan_accommodation) + parseFloat(row.total_daily_allowance),
isInternational: row.is_international === 1
};
}
/**
* Legacy format for backward compatibility
*/
formatAccommodationRate(row) {
return {
cityKey: row.city_key,
name: row.city_name,
province: row.province,
country: row.country,
region: row.region,
currency: row.currency,
monthlyRates: [
row.jan_accommodation || row.jan_rate,
row.feb_accommodation || row.feb_rate,
row.mar_accommodation || row.mar_rate,
row.apr_accommodation || row.apr_rate,
row.may_accommodation || row.may_rate,
row.jun_accommodation || row.jun_rate,
row.jul_accommodation || row.jul_rate,
row.aug_accommodation || row.aug_rate,
row.sep_accommodation || row.sep_rate,
row.oct_accommodation || row.oct_rate,
row.nov_accommodation || row.nov_rate,
row.dec_accommodation || row.dec_rate
],
standardRate: row.standard_accommodation || row.standard_rate,
isInternational: row.is_international === 1,
effectiveDate: row.effective_date
};
}
/**
* List all cities by region
*/
async getCitiesByRegion(region) {
const query = `
SELECT * FROM travel_rates
WHERE region = ?
ORDER BY city_name
`;
return new Promise((resolve, reject) => {
this.db.all(query, [region], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(row => this.formatAccommodationRate(row)));
});
});
}
/**
* List all cities by country
*/
async getCitiesByCountry(country) {
const query = `
SELECT * FROM travel_rates
WHERE LOWER(country) = LOWER(?)
ORDER BY city_name
`;
return new Promise((resolve, reject) => {
this.db.all(query, [country], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(row => this.formatAccommodationRate(row)));
});
});
}
/**
* Get all available regions
*/
async getAllRegions() {
const query = `SELECT DISTINCT region FROM travel_rates ORDER BY region`;
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(row => row.region));
});
});
}
/**
* Get all available countries
*/
async getAllCountries() {
const query = `SELECT DISTINCT country FROM travel_rates ORDER BY country`;
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(row => row.country));
});
});
}
/**
* Autocomplete for city search
*/
async autocomplete(prefix, limit = 10) {
const query = `
SELECT city_name, country, region FROM travel_rates
WHERE LOWER(city_name) LIKE LOWER(?)
ORDER BY
CASE
WHEN LOWER(city_name) LIKE LOWER(?) THEN 1
ELSE 2
END,
city_name
LIMIT ?
`;
const term = `${prefix}%`;
const exactTerm = `${prefix}`;
return new Promise((resolve, reject) => {
this.db.all(query, [term, exactTerm, limit], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
/**
* Get all per-diem rates
*/
async getAllPerDiemRates() {
const query = `
SELECT country, city_name as city, breakfast, lunch, dinner,
incidentals, currency
FROM travel_rates
WHERE country IS NOT NULL
AND country != 'Canada'
ORDER BY country, city_name
`;
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
/**
* Get all accommodation rates
*/
async getAllAccommodations() {
const query = `
SELECT city_name as city, province, accommodation_rate as rate, currency
FROM travel_rates
WHERE country = 'Canada'
AND accommodation_rate IS NOT NULL
ORDER BY province, city_name
`;
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
/**
* Get statistics
*/
async getStats() {
const queries = {
countries: `SELECT COUNT(DISTINCT country) as count FROM travel_rates WHERE country != 'Canada'`,
accommodations: `SELECT COUNT(*) as count FROM travel_rates WHERE country = 'Canada' AND accommodation_rate IS NOT NULL`,
perDiem: `SELECT COUNT(*) as count FROM travel_rates WHERE country != 'Canada'`,
};
const results = {};
for (const [key, query] of Object.entries(queries)) {
results[key] = await new Promise((resolve, reject) => {
this.db.get(query, [], (err, row) => {
if (err) reject(err);
else resolve(row.count);
});
});
}
return results;
}
close() {
if (this.db) {
this.db.close();
console.log('✅ Database connection closed');
}
}
}
module.exports = new DatabaseService();