mirror of
https://github.com/mblanke/Gov_Travel_App.git
synced 2026-03-01 14:10:22 -05:00
feat: add scripts for database inspection and migration
- Implemented multiple scripts to check and inspect meal plans, scraped data, and accommodations for Munich and Riga. - Added a migration script to convert scraped data into the application's database format. - Introduced new database service methods for querying and updating travel rates. - Enhanced server configuration for serving static files in production. - Updated PostCSS configuration for consistency.
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const sqlite3 = require("sqlite3").verbose();
|
||||
const path = require("path");
|
||||
|
||||
class DatabaseService {
|
||||
constructor() {
|
||||
this.dbPath = path.join(__dirname, '..', 'database', 'travel_rates.db');
|
||||
this.db = null;
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
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 = `
|
||||
/**
|
||||
* 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(?)
|
||||
@@ -42,54 +42,59 @@ class DatabaseService {
|
||||
LIMIT 10
|
||||
`;
|
||||
|
||||
const term = `%${searchTerm}%`;
|
||||
const exactTerm = searchTerm.toLowerCase();
|
||||
const likeTerm = `${searchTerm.toLowerCase()}%`;
|
||||
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)) : []);
|
||||
});
|
||||
});
|
||||
}
|
||||
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`;
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
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;
|
||||
/**
|
||||
* Get accommodation rate for a specific month
|
||||
*/
|
||||
async getMonthlyRate(cityKey, month) {
|
||||
const rate = await this.getAccommodationRate(cityKey);
|
||||
|
||||
const monthIndex = month - 1; // 0-based index
|
||||
return {
|
||||
city: rate.name,
|
||||
month: month,
|
||||
rate: rate.monthlyRates[monthIndex],
|
||||
currency: rate.currency
|
||||
};
|
||||
}
|
||||
if (!rate) return null;
|
||||
|
||||
/**
|
||||
* Full-text search across all cities
|
||||
*/
|
||||
async fullTextSearch(searchTerm) {
|
||||
const query = `
|
||||
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
|
||||
@@ -103,147 +108,157 @@ class DatabaseService {
|
||||
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)));
|
||||
});
|
||||
});
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 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 = `
|
||||
/**
|
||||
* 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)));
|
||||
});
|
||||
});
|
||||
}
|
||||
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 = `
|
||||
/**
|
||||
* 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)));
|
||||
});
|
||||
});
|
||||
}
|
||||
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`;
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
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`;
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
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 = `
|
||||
/**
|
||||
* 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
|
||||
@@ -255,22 +270,22 @@ class DatabaseService {
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
const term = `${prefix}%`;
|
||||
const exactTerm = `${prefix}`;
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
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 = `
|
||||
/**
|
||||
* Get all per-diem rates
|
||||
*/
|
||||
async getAllPerDiemRates() {
|
||||
const query = `
|
||||
SELECT country, city_name as city, breakfast, lunch, dinner,
|
||||
incidentals, currency
|
||||
FROM travel_rates
|
||||
@@ -279,19 +294,19 @@ class DatabaseService {
|
||||
ORDER BY country, city_name
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(query, [], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
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 = `
|
||||
/**
|
||||
* 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'
|
||||
@@ -299,42 +314,42 @@ class DatabaseService {
|
||||
ORDER BY province, city_name
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(query, [], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
close() {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
console.log("✅ Database connection closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DatabaseService();
|
||||
|
||||
Reference in New Issue
Block a user