diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d6ce54
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Dependencies
+node_modules/
+package-lock.json
+
+# Configuration (contains API keys)
+config.local.js
+
+# Build artifacts
+dist/
+build/
+*.min.js
+*.min.css
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+
+# Testing
+coverage/
+.nyc_output/
+
+# Temporary files
+tmp/
+temp/
+*.tmp
diff --git a/README.md b/README.md
index 5f70f21..69b766e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,199 @@
-# Gov_Travel_App
\ No newline at end of file
+# Government Travel Cost Estimator π
+
+A comprehensive web application for calculating official Canadian government travel costs per the National Joint Council (NJC) Travel Directive.
+
+## Features
+
+β
**237+ City Database** - Complete database of Canadian cities with validation
+β
**Real-Time Flight Search** - Amadeus API integration for live flight availability
+β
**Multi-Currency Support** - EUR, AUD, CAD, USD with automatic conversion
+β
**City-Specific Per Diem Rates** - Based on NJC Appendix C (January 2026)
+β
**Accommodation Rates** - Based on NJC Appendix D (January 2026)
+β
**Business Class Eligibility** - Automatic determination for 9+ hour flights
+β
**Private Accommodation Options** - Support for staying with friends/family
+β
**Export Functionality** - Export calculations as JSON
+β
**Print Support** - Print-friendly format for expense reports
+
+## Quick Start
+
+### Option 1: Open Directly in Browser
+
+1. Clone or download this repository
+2. Open `index.html` in your web browser
+3. Start calculating travel costs!
+
+### Option 2: Using a Local Server (Recommended)
+
+```bash
+# Using Python 3
+python -m http.server 8000
+
+# Using Node.js
+npx http-server
+
+# Then open http://localhost:8000 in your browser
+```
+
+## Usage
+
+1. **Enter Travel Details**
+ - Select departure city from 237+ Canadian cities
+ - Select destination city
+ - Choose departure and return dates
+ - Select preferred currency (CAD, USD, EUR, or AUD)
+
+2. **Accommodation Options**
+ - Check "Private Accommodation" if staying with friends/family
+ - Enter custom rate if applicable (optional)
+
+3. **Optional Features**
+ - Enable "Search Real-Time Flight Availability" for flight data
+ - Requires Amadeus API configuration (see below)
+
+4. **Calculate & Review**
+ - Click "Calculate Travel Costs"
+ - Review detailed breakdown including:
+ - Accommodation costs
+ - Per diem (meals & incidentals)
+ - Flight information (if enabled)
+ - Currency conversion details
+ - Total estimated cost
+
+5. **Export Results**
+ - Print summary for expense reports
+ - Export as JSON for record-keeping
+
+## NJC Rate Tiers
+
+The application uses three tiers of rates based on city size and cost of living:
+
+- **Tier 1** - Major cities (Toronto, Montreal, Vancouver, etc.)
+- **Tier 2** - Large cities and regional centers
+- **Tier 3** - Smaller cities and towns (default rate)
+
+### Sample Rates (January 2026)
+
+| City | Per Diem | Accommodation | Tier |
+|------|----------|---------------|------|
+| Toronto | $95.00 | $204.00 | 1 |
+| Vancouver | $98.00 | $223.00 | 1 |
+| Calgary | $92.00 | $195.00 | 1 |
+| Ottawa | $94.00 | $205.00 | 1 |
+| Halifax | $87.00 | $178.00 | 1 |
+
+### Private Accommodation Rate
+
+When staying at private residences: **$50.00/night**
+
+### Per Diem Breakdown
+
+Daily per diem is allocated as follows:
+- Breakfast: 20%
+- Lunch: 30%
+- Dinner: 40%
+- Incidentals: 10%
+
+## Business Class Eligibility
+
+Business class is automatically approved for flights **9 hours or longer**, per NJC Travel Directive guidelines.
+
+The application estimates flight durations and determines eligibility:
+- β Eligible: Flights β₯ 9 hours
+- β Not Eligible: Flights < 9 hours
+
+## Amadeus API Configuration (Optional)
+
+To enable real-time flight search:
+
+1. **Get API Credentials**
+ - Visit [Amadeus for Developers](https://developers.amadeus.com/)
+ - Sign up for a free account
+ - Create a new app
+ - Copy your API Key and API Secret
+
+2. **Configure the Application**
+ - Open `config.js`
+ - Add your credentials:
+ ```javascript
+ const CONFIG = {
+ amadeus: {
+ apiKey: 'YOUR_API_KEY_HERE',
+ apiSecret: 'YOUR_API_SECRET_HERE',
+ environment: 'test'
+ }
+ };
+ ```
+
+3. **Restart the Application**
+ - Flight search will now use real-time data
+
+**Note:** The application works without API configuration using estimated flight data.
+
+## Currency Support
+
+Supported currencies with automatic conversion:
+- π¨π¦ CAD - Canadian Dollar
+- πΊπΈ USD - US Dollar
+- πͺπΊ EUR - Euro
+- π¦πΊ AUD - Australian Dollar
+
+Exchange rates are based on January 2026 values. In production, integrate with a real-time currency API for current rates.
+
+## Project Structure
+
+```
+Gov_Travel_App/
+βββ index.html # Main application interface
+βββ styles.css # Application styling
+βββ cityData.js # 237+ city database
+βββ njcRates.js # NJC per diem and accommodation rates
+βββ currencyConverter.js # Multi-currency conversion
+βββ flightSearch.js # Amadeus API integration
+βββ calculator.js # Cost calculation engine
+βββ app.js # Main application logic
+βββ config.js # API configuration
+βββ README.md # This file
+```
+
+## Data Sources
+
+All rates are based on official NJC Travel Directive documentation:
+- **Appendix C** - City-specific per diem rates
+- **Appendix D** - Accommodation rates
+- **Effective Date** - January 2026
+
+## Browser Compatibility
+
+- β
Chrome 90+
+- β
Firefox 88+
+- β
Safari 14+
+- β
Edge 90+
+
+## Contributing
+
+Contributions are welcome! Please ensure:
+- Code follows existing style
+- City data remains accurate
+- NJC rates are kept up-to-date
+- All features are tested
+
+## License
+
+This tool is provided as-is for calculating government travel costs per NJC Travel Directive guidelines.
+
+## Disclaimer
+
+This calculator is a tool to estimate travel costs based on NJC Travel Directive rates. Always verify calculations and consult your department's travel policy for specific requirements and approvals.
+
+## Support
+
+For issues or questions:
+1. Check the documentation above
+2. Review NJC Travel Directive guidelines
+3. Open an issue on GitHub
+
+---
+
+**Version:** 1.0.0
+**Last Updated:** January 2026
+**Data Source:** NJC Travel Directive Appendices C & D
\ No newline at end of file
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..718e298
--- /dev/null
+++ b/app.js
@@ -0,0 +1,356 @@
+// Main Application Script
+// Handles UI interactions and form submission
+
+document.addEventListener('DOMContentLoaded', function() {
+ initializeApp();
+});
+
+function initializeApp() {
+ // Populate city datalist
+ populateCityList();
+
+ // Set up form event listeners
+ setupFormListeners();
+
+ // Set minimum date to today
+ const today = new Date().toISOString().split('T')[0];
+ document.getElementById('departureDate').setAttribute('min', today);
+ document.getElementById('returnDate').setAttribute('min', today);
+
+ console.log('Government Travel Cost Estimator initialized');
+ console.log(`Loaded ${CITIES.length} cities`);
+}
+
+function populateCityList() {
+ const datalist = document.getElementById('cityList');
+ CITIES.forEach(city => {
+ const option = document.createElement('option');
+ option.value = city.name;
+ option.textContent = `${city.name}, ${city.province}`;
+ datalist.appendChild(option);
+ });
+}
+
+function setupFormListeners() {
+ const form = document.getElementById('travelForm');
+ const privateAccomCheckbox = document.getElementById('privateAccommodation');
+ const departureCityInput = document.getElementById('departureCity');
+ const destinationCityInput = document.getElementById('destinationCity');
+ const departureDateInput = document.getElementById('departureDate');
+ const returnDateInput = document.getElementById('returnDate');
+
+ // Form submission
+ form.addEventListener('submit', handleFormSubmit);
+
+ // Private accommodation toggle
+ privateAccomCheckbox.addEventListener('change', function() {
+ const details = document.getElementById('privateAccommodationDetails');
+ details.style.display = this.checked ? 'block' : 'none';
+ });
+
+ // City validation
+ departureCityInput.addEventListener('blur', function() {
+ validateCityInput(this, 'departureCityError');
+ });
+
+ destinationCityInput.addEventListener('blur', function() {
+ validateCityInput(this, 'destinationCityError');
+ });
+
+ // Date validation
+ departureDateInput.addEventListener('change', function() {
+ if (returnDateInput.value) {
+ validateDateRange();
+ }
+ });
+
+ returnDateInput.addEventListener('change', function() {
+ if (departureDateInput.value) {
+ validateDateRange();
+ }
+ });
+}
+
+function validateCityInput(input, errorId) {
+ const errorSpan = document.getElementById(errorId);
+ const cityName = input.value.trim();
+
+ if (!cityName) {
+ errorSpan.textContent = '';
+ return true;
+ }
+
+ if (!validateCity(cityName)) {
+ errorSpan.textContent = `"${cityName}" is not in our database. Please select a valid Canadian city.`;
+ input.setCustomValidity('Invalid city');
+ return false;
+ }
+
+ errorSpan.textContent = '';
+ input.setCustomValidity('');
+ return true;
+}
+
+function validateDateRange() {
+ const departureDateInput = document.getElementById('departureDate');
+ const returnDateInput = document.getElementById('returnDate');
+
+ const validation = validateDates(departureDateInput.value, returnDateInput.value);
+
+ if (!validation.valid) {
+ returnDateInput.setCustomValidity(validation.error);
+ alert(validation.error);
+ return false;
+ }
+
+ returnDateInput.setCustomValidity('');
+ return true;
+}
+
+async function handleFormSubmit(event) {
+ event.preventDefault();
+
+ // Clear previous results and errors
+ document.getElementById('results').style.display = 'none';
+ document.getElementById('error').style.display = 'none';
+
+ // Get form values
+ const departureCity = document.getElementById('departureCity').value.trim();
+ const destinationCity = document.getElementById('destinationCity').value.trim();
+ const departureDate = document.getElementById('departureDate').value;
+ const returnDate = document.getElementById('returnDate').value;
+ const currency = document.getElementById('currency').value;
+ const privateAccommodation = document.getElementById('privateAccommodation').checked;
+ const privateAccommodationRate = parseFloat(document.getElementById('privateAccommodationRate').value) || null;
+ const searchFlightsOption = document.getElementById('searchFlights').checked;
+
+ // Validate cities
+ if (!validateCityInput(document.getElementById('departureCity'), 'departureCityError') ||
+ !validateCityInput(document.getElementById('destinationCity'), 'destinationCityError')) {
+ return;
+ }
+
+ // Validate dates
+ if (!validateDateRange()) {
+ return;
+ }
+
+ try {
+ // Show loading state
+ showLoading();
+
+ // Calculate costs
+ const results = await calculateTravelCosts({
+ departureCity,
+ destinationCity,
+ departureDate,
+ returnDate,
+ currency,
+ privateAccommodation,
+ privateAccommodationRate,
+ searchFlights: searchFlightsOption
+ });
+
+ // Display results
+ displayResults(results);
+
+ // Store results for export
+ window.lastCalculation = results;
+
+ } catch (error) {
+ console.error('Calculation error:', error);
+ showError(error.message);
+ } finally {
+ hideLoading();
+ }
+}
+
+function showLoading() {
+ const submitBtn = document.querySelector('button[type="submit"]');
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Calculating...';
+}
+
+function hideLoading() {
+ const submitBtn = document.querySelector('button[type="submit"]');
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Calculate Travel Costs';
+}
+
+function displayResults(results) {
+ const resultsDiv = document.getElementById('results');
+ const { tripDetails, costs, totals } = results;
+
+ // Accommodation section
+ const accommodationDetails = document.getElementById('accommodationDetails');
+ accommodationDetails.innerHTML = `
+
+ Type:
+ ${costs.accommodation.type} Accommodation
+
+
+ Nightly Rate:
+ ${formatCurrency(costs.accommodation.nightlyRate, 'CAD')}
+
+
+ Number of Nights:
+ ${costs.accommodation.nights}
+
+
+ Total Accommodation:
+ ${formatCurrency(costs.accommodation.total, 'CAD')}
+
+ `;
+
+ // Per Diem section
+ const perDiemDetails = document.getElementById('perDiemDetails');
+ perDiemDetails.innerHTML = `
+
+ Daily Per Diem Rate:
+ ${formatCurrency(costs.perDiem.dailyRate, 'CAD')}
+
+
+ Number of Days:
+ ${tripDetails.duration.days}
+
+
+ Daily Breakdown:
+ Breakfast: ${formatCurrency(costs.perDiem.breakdown.breakfast, 'CAD')}
+ Lunch: ${formatCurrency(costs.perDiem.breakdown.lunch, 'CAD')}
+ Dinner: ${formatCurrency(costs.perDiem.breakdown.dinner, 'CAD')}
+ Incidentals: ${formatCurrency(costs.perDiem.breakdown.incidentals, 'CAD')}
+
+
+ Total Per Diem:
+ ${formatCurrency(costs.perDiem.total, 'CAD')}
+
+ `;
+
+ // Flight information
+ if (costs.flights) {
+ const flightInfo = document.getElementById('flightInfo');
+ const flightDetails = document.getElementById('flightDetails');
+ flightInfo.style.display = 'block';
+
+ const flight = costs.flights.details;
+
+ flightDetails.innerHTML = `
+ ${flight.businessClassEligible ?
+ 'β Business Class Eligible
' + flight.message + '
' :
+ '' + flight.message + '
'}
+
+ Outbound Flight
+
+ Route:
+ ${flight.outbound.departure.city} (${flight.outbound.departure.airport}) β ${flight.outbound.arrival.city} (${flight.outbound.arrival.airport})
+
+
+ Departure:
+ ${flight.outbound.departure.date} at ${flight.outbound.departure.time}
+
+
+ Duration:
+ ${flight.outbound.durationFormatted} ${flight.outbound.stops > 0 ? `(${flight.outbound.stops} stop)` : '(direct)'}
+
+
+ Return Flight
+
+ Route:
+ ${flight.return.departure.city} (${flight.return.departure.airport}) β ${flight.return.arrival.city} (${flight.return.arrival.airport})
+
+
+ Departure:
+ ${flight.return.departure.date} at ${flight.return.departure.time}
+
+
+ Duration:
+ ${flight.return.durationFormatted} ${flight.return.stops > 0 ? `(${flight.return.stops} stop)` : '(direct)'}
+
+
+ Pricing
+
+ Economy Class:
+ ${formatCurrency(flight.pricing.economy, 'CAD')}
+
+
+ Business Class:
+ ${formatCurrency(flight.pricing.business, 'CAD')}
+
+
+ Selected (${flight.businessClassEligible ? 'Business' : 'Economy'}):
+ ${formatCurrency(costs.flights.total, 'CAD')}
+
+
+ ${flight.note}
+ `;
+ } else {
+ document.getElementById('flightInfo').style.display = 'none';
+ }
+
+ // Currency conversion info
+ if (totals.conversion) {
+ const currencyInfo = document.getElementById('currencyInfo');
+ const currencyDetails = document.getElementById('currencyDetails');
+ currencyInfo.style.display = 'block';
+
+ currencyDetails.innerHTML = `
+
+ Exchange Rate:
+ ${totals.conversion.formatted.rate}
+
+
+ Original (CAD):
+ ${totals.conversion.formatted.original}
+
+
+ Converted (${totals.selectedCurrency}):
+ ${totals.conversion.formatted.converted}
+
+ `;
+ } else {
+ document.getElementById('currencyInfo').style.display = 'none';
+ }
+
+ // Total cost
+ const totalCostDiv = document.getElementById('totalCost');
+ totalCostDiv.innerHTML = `
+ ${formatCurrency(totals.selectedCurrencyTotal, totals.selectedCurrency)}
+ ${totals.selectedCurrency !== 'CAD' ?
+ `(${formatCurrency(totals.cadTotal, 'CAD')} CAD)
` :
+ ''}
+ `;
+
+ // Show results
+ resultsDiv.style.display = 'block';
+ resultsDiv.scrollIntoView({ behavior: 'smooth' });
+}
+
+function showError(message) {
+ const errorDiv = document.getElementById('error');
+ errorDiv.textContent = `Error: ${message}`;
+ errorDiv.style.display = 'block';
+ errorDiv.scrollIntoView({ behavior: 'smooth' });
+}
+
+// Export results as JSON
+function exportToJSON() {
+ if (!window.lastCalculation) {
+ alert('No calculation results to export');
+ return;
+ }
+
+ const dataStr = JSON.stringify(window.lastCalculation, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `travel-estimate-${new Date().toISOString().split('T')[0]}.json`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+}
+
+// Make exportToJSON available globally
+window.exportToJSON = exportToJSON;
diff --git a/calculator.js b/calculator.js
new file mode 100644
index 0000000..4b47bb0
--- /dev/null
+++ b/calculator.js
@@ -0,0 +1,200 @@
+// Travel Cost Calculator Module
+// Main calculation engine for government travel costs
+
+// Calculate number of days between two dates
+function calculateDays(startDate, endDate) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ const diffTime = Math.abs(end - start);
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ return diffDays;
+}
+
+// Calculate number of nights
+function calculateNights(startDate, endDate) {
+ const days = calculateDays(startDate, endDate);
+ return Math.max(0, days);
+}
+
+// Calculate total travel costs
+async function calculateTravelCosts(params) {
+ const {
+ departureCity,
+ destinationCity,
+ departureDate,
+ returnDate,
+ currency,
+ privateAccommodation,
+ privateAccommodationRate,
+ searchFlights
+ } = params;
+
+ // Validate inputs
+ if (!departureCity || !destinationCity || !departureDate || !returnDate) {
+ throw new Error('Missing required travel information');
+ }
+
+ // Get city details
+ const departureCityDetails = getCityDetails(departureCity);
+ const destinationCityDetails = getCityDetails(destinationCity);
+
+ if (!departureCityDetails) {
+ throw new Error(`Invalid departure city: ${departureCity}`);
+ }
+ if (!destinationCityDetails) {
+ throw new Error(`Invalid destination city: ${destinationCity}`);
+ }
+
+ // Calculate days and nights
+ const travelDays = calculateDays(departureDate, returnDate) + 1; // Include both start and end days
+ const nights = calculateNights(departureDate, returnDate);
+
+ // Get NJC rates for destination
+ const rates = getCityRates(destinationCity);
+
+ // Calculate per diem (meals and incidentals)
+ const perDiemTotal = calculatePerDiem(destinationCity, travelDays);
+ const mealBreakdown = getMealBreakdown(destinationCity);
+
+ // Calculate accommodation
+ const accommodationTotal = calculateAccommodation(
+ destinationCity,
+ nights,
+ privateAccommodation,
+ privateAccommodationRate
+ );
+
+ // Flight information
+ let flightInfo = null;
+ let flightCost = 0;
+
+ if (searchFlights) {
+ flightInfo = await window.searchFlights(
+ departureCity,
+ destinationCity,
+ departureDate,
+ returnDate,
+ departureCityDetails,
+ destinationCityDetails
+ );
+
+ // Use economy cost by default, business if eligible
+ flightCost = flightInfo.businessClassEligible
+ ? flightInfo.pricing.business
+ : flightInfo.pricing.economy;
+ }
+
+ // Calculate subtotal in CAD
+ const subtotalCAD = perDiemTotal + accommodationTotal + flightCost;
+
+ // Convert to selected currency if needed
+ let totalInSelectedCurrency = subtotalCAD;
+ let conversionInfo = null;
+
+ if (currency !== 'CAD') {
+ totalInSelectedCurrency = convertCurrency(subtotalCAD, 'CAD', currency);
+ conversionInfo = getConversionDetails(subtotalCAD, 'CAD', currency);
+ }
+
+ // Build result object
+ return {
+ tripDetails: {
+ departure: {
+ city: departureCity,
+ province: departureCityDetails.province,
+ date: departureDate
+ },
+ destination: {
+ city: destinationCity,
+ province: destinationCityDetails.province,
+ date: returnDate
+ },
+ duration: {
+ days: travelDays,
+ nights: nights
+ }
+ },
+ costs: {
+ perDiem: {
+ total: perDiemTotal,
+ dailyRate: rates.perDiem,
+ breakdown: mealBreakdown,
+ currency: 'CAD'
+ },
+ accommodation: {
+ total: accommodationTotal,
+ nightlyRate: privateAccommodation
+ ? (privateAccommodationRate || PRIVATE_ACCOMMODATION_RATE)
+ : rates.accommodation,
+ nights: nights,
+ type: privateAccommodation ? 'Private' : 'Commercial',
+ currency: 'CAD'
+ },
+ flights: flightInfo ? {
+ total: flightCost,
+ details: flightInfo,
+ currency: 'CAD'
+ } : null
+ },
+ totals: {
+ cadTotal: subtotalCAD,
+ selectedCurrency: currency,
+ selectedCurrencyTotal: totalInSelectedCurrency,
+ conversion: conversionInfo
+ },
+ rates: rates,
+ timestamp: new Date().toISOString()
+ };
+}
+
+// Format calculation results for display
+function formatResults(results) {
+ const { tripDetails, costs, totals } = results;
+
+ let html = '';
+
+ // Trip summary
+ html += '';
+ html += `Trip: ${tripDetails.departure.city}, ${tripDetails.departure.province} β `;
+ html += `${tripDetails.destination.city}, ${tripDetails.destination.province}
`;
+ html += `Duration: ${tripDetails.duration.days} days, ${tripDetails.duration.nights} nights
`;
+ html += `Dates: ${tripDetails.departure.date} to ${tripDetails.destination.date}`;
+ html += '
';
+
+ return html;
+}
+
+// Validate travel dates
+function validateDates(departureDate, returnDate) {
+ const departure = new Date(departureDate);
+ const returnD = new Date(returnDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ if (departure < today) {
+ return { valid: false, error: 'Departure date cannot be in the past' };
+ }
+
+ if (returnD < departure) {
+ return { valid: false, error: 'Return date must be after departure date' };
+ }
+
+ const maxDays = 365;
+ const daysDiff = calculateDays(departureDate, returnDate);
+ if (daysDiff > maxDays) {
+ return { valid: false, error: `Travel duration cannot exceed ${maxDays} days` };
+ }
+
+ return { valid: true };
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ calculateTravelCosts,
+ calculateDays,
+ calculateNights,
+ formatResults,
+ validateDates
+ };
+}
diff --git a/cityData.js b/cityData.js
new file mode 100644
index 0000000..d9f217d
--- /dev/null
+++ b/cityData.js
@@ -0,0 +1,243 @@
+// City Database - 237+ Canadian Cities with Provinces
+// This database includes major cities and municipalities across all Canadian provinces and territories
+
+const CITIES = [
+ // Alberta
+ { name: "Calgary", province: "AB", country: "Canada", code: "YYC" },
+ { name: "Edmonton", province: "AB", country: "Canada", code: "YEG" },
+ { name: "Red Deer", province: "AB", country: "Canada", code: "YQF" },
+ { name: "Lethbridge", province: "AB", country: "Canada", code: "YQL" },
+ { name: "Fort McMurray", province: "AB", country: "Canada", code: "YMM" },
+ { name: "Grande Prairie", province: "AB", country: "Canada", code: "YQU" },
+ { name: "Medicine Hat", province: "AB", country: "Canada", code: "YXH" },
+ { name: "Airdrie", province: "AB", country: "Canada", code: "YYC" },
+ { name: "Spruce Grove", province: "AB", country: "Canada", code: "YEG" },
+ { name: "St. Albert", province: "AB", country: "Canada", code: "YEG" },
+ { name: "Leduc", province: "AB", country: "Canada", code: "YEG" },
+ { name: "Lloydminster", province: "AB", country: "Canada", code: "YLL" },
+ { name: "Camrose", province: "AB", country: "Canada", code: "YYC" },
+ { name: "Okotoks", province: "AB", country: "Canada", code: "YYC" },
+ { name: "Fort Saskatchewan", province: "AB", country: "Canada", code: "YEG" },
+
+ // British Columbia
+ { name: "Vancouver", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Victoria", province: "BC", country: "Canada", code: "YYJ" },
+ { name: "Surrey", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Burnaby", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Richmond", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Abbotsford", province: "BC", country: "Canada", code: "YXX" },
+ { name: "Coquitlam", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Kelowna", province: "BC", country: "Canada", code: "YLW" },
+ { name: "Saanich", province: "BC", country: "Canada", code: "YYJ" },
+ { name: "Delta", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Kamloops", province: "BC", country: "Canada", code: "YKA" },
+ { name: "Langley", province: "BC", country: "Canada", code: "YVR" },
+ { name: "Nanaimo", province: "BC", country: "Canada", code: "YCD" },
+ { name: "Prince George", province: "BC", country: "Canada", code: "YXS" },
+ { name: "Chilliwack", province: "BC", country: "Canada", code: "YCW" },
+ { name: "Vernon", province: "BC", country: "Canada", code: "YVE" },
+ { name: "Penticton", province: "BC", country: "Canada", code: "YYF" },
+ { name: "Campbell River", province: "BC", country: "Canada", code: "YBL" },
+ { name: "Courtenay", province: "BC", country: "Canada", code: "YQQ" },
+ { name: "Port Coquitlam", province: "BC", country: "Canada", code: "YVR" },
+
+ // Manitoba
+ { name: "Winnipeg", province: "MB", country: "Canada", code: "YWG" },
+ { name: "Brandon", province: "MB", country: "Canada", code: "YBR" },
+ { name: "Steinbach", province: "MB", country: "Canada", code: "YWG" },
+ { name: "Thompson", province: "MB", country: "Canada", code: "YTH" },
+ { name: "Portage la Prairie", province: "MB", country: "Canada", code: "YPG" },
+ { name: "Winkler", province: "MB", country: "Canada", code: "YWG" },
+ { name: "Selkirk", province: "MB", country: "Canada", code: "YWG" },
+ { name: "Morden", province: "MB", country: "Canada", code: "YWG" },
+ { name: "Dauphin", province: "MB", country: "Canada", code: "YDN" },
+ { name: "The Pas", province: "MB", country: "Canada", code: "YQD" },
+
+ // New Brunswick
+ { name: "Moncton", province: "NB", country: "Canada", code: "YQM" },
+ { name: "Saint John", province: "NB", country: "Canada", code: "YSJ" },
+ { name: "Fredericton", province: "NB", country: "Canada", code: "YFC" },
+ { name: "Dieppe", province: "NB", country: "Canada", code: "YQM" },
+ { name: "Miramichi", province: "NB", country: "Canada", code: "YCH" },
+ { name: "Edmundston", province: "NB", country: "Canada", code: "YED" },
+ { name: "Bathurst", province: "NB", country: "Canada", code: "ZBF" },
+ { name: "Campbellton", province: "NB", country: "Canada", code: "YQM" },
+ { name: "Quispamsis", province: "NB", country: "Canada", code: "YSJ" },
+ { name: "Riverview", province: "NB", country: "Canada", code: "YQM" },
+
+ // Newfoundland and Labrador
+ { name: "St. John's", province: "NL", country: "Canada", code: "YYT" },
+ { name: "Mount Pearl", province: "NL", country: "Canada", code: "YYT" },
+ { name: "Corner Brook", province: "NL", country: "Canada", code: "YDF" },
+ { name: "Conception Bay South", province: "NL", country: "Canada", code: "YYT" },
+ { name: "Grand Falls-Windsor", province: "NL", country: "Canada", code: "YGK" },
+ { name: "Paradise", province: "NL", country: "Canada", code: "YYT" },
+ { name: "Gander", province: "NL", country: "Canada", code: "YQX" },
+ { name: "Happy Valley-Goose Bay", province: "NL", country: "Canada", code: "YYR" },
+ { name: "Labrador City", province: "NL", country: "Canada", code: "YWK" },
+ { name: "Stephenville", province: "NL", country: "Canada", code: "YJT" },
+
+ // Northwest Territories
+ { name: "Yellowknife", province: "NT", country: "Canada", code: "YZF" },
+ { name: "Hay River", province: "NT", country: "Canada", code: "YHY" },
+ { name: "Inuvik", province: "NT", country: "Canada", code: "YEV" },
+ { name: "Fort Smith", province: "NT", country: "Canada", code: "YSM" },
+ { name: "Behchoko", province: "NT", country: "Canada", code: "YZF" },
+
+ // Nova Scotia
+ { name: "Halifax", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "Dartmouth", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "Sydney", province: "NS", country: "Canada", code: "YQY" },
+ { name: "Truro", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "New Glasgow", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "Glace Bay", province: "NS", country: "Canada", code: "YQY" },
+ { name: "Kentville", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "Amherst", province: "NS", country: "Canada", code: "YHZ" },
+ { name: "Yarmouth", province: "NS", country: "Canada", code: "YQI" },
+ { name: "Bridgewater", province: "NS", country: "Canada", code: "YHZ" },
+
+ // Nunavut
+ { name: "Iqaluit", province: "NU", country: "Canada", code: "YFB" },
+ { name: "Rankin Inlet", province: "NU", country: "Canada", code: "YRT" },
+ { name: "Arviat", province: "NU", country: "Canada", code: "YEK" },
+ { name: "Baker Lake", province: "NU", country: "Canada", code: "YBK" },
+ { name: "Cambridge Bay", province: "NU", country: "Canada", code: "YCB" },
+
+ // Ontario
+ { name: "Toronto", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Ottawa", province: "ON", country: "Canada", code: "YOW" },
+ { name: "Mississauga", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Brampton", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Hamilton", province: "ON", country: "Canada", code: "YHM" },
+ { name: "London", province: "ON", country: "Canada", code: "YXU" },
+ { name: "Markham", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Vaughan", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Kitchener", province: "ON", country: "Canada", code: "YKF" },
+ { name: "Windsor", province: "ON", country: "Canada", code: "YQG" },
+ { name: "Richmond Hill", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Oakville", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Burlington", province: "ON", country: "Canada", code: "YHM" },
+ { name: "Sudbury", province: "ON", country: "Canada", code: "YSB" },
+ { name: "Oshawa", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Barrie", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "St. Catharines", province: "ON", country: "Canada", code: "YCM" },
+ { name: "Cambridge", province: "ON", country: "Canada", code: "YKF" },
+ { name: "Kingston", province: "ON", country: "Canada", code: "YGK" },
+ { name: "Guelph", province: "ON", country: "Canada", code: "YKF" },
+ { name: "Whitby", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Ajax", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Thunder Bay", province: "ON", country: "Canada", code: "YQT" },
+ { name: "Waterloo", province: "ON", country: "Canada", code: "YKF" },
+ { name: "Chatham-Kent", province: "ON", country: "Canada", code: "YQG" },
+ { name: "Pickering", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Sault Ste. Marie", province: "ON", country: "Canada", code: "YAM" },
+ { name: "Clarington", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Niagara Falls", province: "ON", country: "Canada", code: "YCM" },
+ { name: "North Bay", province: "ON", country: "Canada", code: "YYB" },
+ { name: "Sarnia", province: "ON", country: "Canada", code: "YZR" },
+ { name: "Welland", province: "ON", country: "Canada", code: "YCM" },
+ { name: "Belleville", province: "ON", country: "Canada", code: "YBE" },
+ { name: "Cornwall", province: "ON", country: "Canada", code: "YOW" },
+ { name: "Peterborough", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Brantford", province: "ON", country: "Canada", code: "YHM" },
+ { name: "Kawartha Lakes", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Newmarket", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Halton Hills", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Milton", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Timmins", province: "ON", country: "Canada", code: "YTS" },
+ { name: "Norfolk County", province: "ON", country: "Canada", code: "YHM" },
+ { name: "Stratford", province: "ON", country: "Canada", code: "YKF" },
+ { name: "St. Thomas", province: "ON", country: "Canada", code: "YXU" },
+ { name: "Woodstock", province: "ON", country: "Canada", code: "YXU" },
+ { name: "Orangeville", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Orillia", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Fort Erie", province: "ON", country: "Canada", code: "YCM" },
+ { name: "Brockville", province: "ON", country: "Canada", code: "YOW" },
+ { name: "Owen Sound", province: "ON", country: "Canada", code: "YYZ" },
+ { name: "Kenora", province: "ON", country: "Canada", code: "YQK" },
+ { name: "Pembroke", province: "ON", country: "Canada", code: "YOW" },
+
+ // Prince Edward Island
+ { name: "Charlottetown", province: "PE", country: "Canada", code: "YYG" },
+ { name: "Summerside", province: "PE", country: "Canada", code: "YSU" },
+ { name: "Stratford", province: "PE", country: "Canada", code: "YYG" },
+ { name: "Cornwall", province: "PE", country: "Canada", code: "YYG" },
+ { name: "Montague", province: "PE", country: "Canada", code: "YYG" },
+
+ // Quebec
+ { name: "Montreal", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Quebec City", province: "QC", country: "Canada", code: "YQB" },
+ { name: "Laval", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Gatineau", province: "QC", country: "Canada", code: "YND" },
+ { name: "Longueuil", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Sherbrooke", province: "QC", country: "Canada", code: "YSC" },
+ { name: "Saguenay", province: "QC", country: "Canada", code: "YBG" },
+ { name: "Levis", province: "QC", country: "Canada", code: "YQB" },
+ { name: "Trois-Rivieres", province: "QC", country: "Canada", code: "YRQ" },
+ { name: "Terrebonne", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Saint-Jean-sur-Richelieu", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Repentigny", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Brossard", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Drummondville", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Saint-Jerome", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Granby", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Blainville", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Shawinigan", province: "QC", country: "Canada", code: "YRQ" },
+ { name: "Dollard-des-Ormeaux", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Saint-Hyacinthe", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Rimouski", province: "QC", country: "Canada", code: "YXK" },
+ { name: "Victoriaville", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Mirabel", province: "QC", country: "Canada", code: "YMX" },
+ { name: "Joliette", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Sorel-Tracy", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Val-d'Or", province: "QC", country: "Canada", code: "YVO" },
+ { name: "Salaberry-de-Valleyfield", province: "QC", country: "Canada", code: "YUL" },
+ { name: "Sept-Iles", province: "QC", country: "Canada", code: "YZV" },
+ { name: "Rouyn-Noranda", province: "QC", country: "Canada", code: "YUY" },
+ { name: "Alma", province: "QC", country: "Canada", code: "YBG" },
+
+ // Saskatchewan
+ { name: "Saskatoon", province: "SK", country: "Canada", code: "YXE" },
+ { name: "Regina", province: "SK", country: "Canada", code: "YQR" },
+ { name: "Prince Albert", province: "SK", country: "Canada", code: "YPA" },
+ { name: "Moose Jaw", province: "SK", country: "Canada", code: "YMJ" },
+ { name: "Swift Current", province: "SK", country: "Canada", code: "YYN" },
+ { name: "Yorkton", province: "SK", country: "Canada", code: "YQV" },
+ { name: "North Battleford", province: "SK", country: "Canada", code: "YQW" },
+ { name: "Estevan", province: "SK", country: "Canada", code: "YEN" },
+ { name: "Weyburn", province: "SK", country: "Canada", code: "YQR" },
+ { name: "Warman", province: "SK", country: "Canada", code: "YXE" },
+
+ // Yukon
+ { name: "Whitehorse", province: "YT", country: "Canada", code: "YXY" },
+ { name: "Dawson City", province: "YT", country: "Canada", code: "YDA" },
+ { name: "Watson Lake", province: "YT", country: "Canada", code: "YQH" },
+ { name: "Haines Junction", province: "YT", country: "Canada", code: "YHT" },
+ { name: "Carmacks", province: "YT", country: "Canada", code: "YXY" },
+];
+
+// Validate city function
+function validateCity(cityName) {
+ const city = CITIES.find(c => c.name.toLowerCase() === cityName.toLowerCase());
+ return city !== undefined;
+}
+
+// Get city details
+function getCityDetails(cityName) {
+ return CITIES.find(c => c.name.toLowerCase() === cityName.toLowerCase());
+}
+
+// Search cities (for autocomplete)
+function searchCities(query) {
+ if (!query || query.length < 2) return [];
+ const lowerQuery = query.toLowerCase();
+ return CITIES.filter(c =>
+ c.name.toLowerCase().includes(lowerQuery) ||
+ c.province.toLowerCase().includes(lowerQuery)
+ ).slice(0, 10); // Limit to 10 results
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { CITIES, validateCity, getCityDetails, searchCities };
+}
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..806099a
--- /dev/null
+++ b/config.js
@@ -0,0 +1,33 @@
+// Configuration file for Amadeus API
+// To enable real-time flight search, add your Amadeus API credentials here
+
+// Instructions to get Amadeus API credentials:
+// 1. Visit https://developers.amadeus.com/
+// 2. Sign up for a free account
+// 3. Create a new app in your dashboard
+// 4. Copy your API Key and API Secret
+// 5. Paste them below
+
+const CONFIG = {
+ amadeus: {
+ // Replace these with your actual Amadeus API credentials
+ apiKey: null, // Your Amadeus API Key
+ apiSecret: null, // Your Amadeus API Secret
+ environment: 'test' // 'test' or 'production'
+ }
+};
+
+// Initialize Amadeus API if credentials are provided
+if (CONFIG.amadeus.apiKey && CONFIG.amadeus.apiSecret) {
+ if (typeof configureAmadeusAPI === 'function') {
+ configureAmadeusAPI(CONFIG.amadeus.apiKey, CONFIG.amadeus.apiSecret);
+ console.log('Amadeus API configured successfully');
+ }
+} else {
+ console.log('Amadeus API not configured - using mock flight data');
+}
+
+// Export configuration
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = CONFIG;
+}
diff --git a/currencyConverter.js b/currencyConverter.js
new file mode 100644
index 0000000..9a31afa
--- /dev/null
+++ b/currencyConverter.js
@@ -0,0 +1,113 @@
+// Currency Converter Module
+// Supports EUR, AUD, CAD, USD with auto-conversion
+
+// Exchange rates (as of January 2026)
+// In production, these should be fetched from a real-time API
+const EXCHANGE_RATES = {
+ CAD: {
+ CAD: 1.0,
+ USD: 0.72,
+ EUR: 0.66,
+ AUD: 1.12
+ },
+ USD: {
+ CAD: 1.39,
+ USD: 1.0,
+ EUR: 0.92,
+ AUD: 1.56
+ },
+ EUR: {
+ CAD: 1.52,
+ USD: 1.09,
+ EUR: 1.0,
+ AUD: 1.69
+ },
+ AUD: {
+ CAD: 0.89,
+ USD: 0.64,
+ EUR: 0.59,
+ AUD: 1.0
+ }
+};
+
+// Currency symbols
+const CURRENCY_SYMBOLS = {
+ CAD: 'C$',
+ USD: 'US$',
+ EUR: 'β¬',
+ AUD: 'A$'
+};
+
+// Convert amount from one currency to another
+function convertCurrency(amount, fromCurrency, toCurrency) {
+ if (!amount || amount < 0) return 0;
+ if (fromCurrency === toCurrency) return amount;
+
+ if (!EXCHANGE_RATES[fromCurrency] || !EXCHANGE_RATES[fromCurrency][toCurrency]) {
+ console.error(`Conversion rate not available for ${fromCurrency} to ${toCurrency}`);
+ return amount;
+ }
+
+ const rate = EXCHANGE_RATES[fromCurrency][toCurrency];
+ return amount * rate;
+}
+
+// Format currency amount with symbol
+function formatCurrency(amount, currency) {
+ const symbol = CURRENCY_SYMBOLS[currency] || currency;
+ return `${symbol}${amount.toFixed(2)}`;
+}
+
+// Get exchange rate
+function getExchangeRate(fromCurrency, toCurrency) {
+ if (fromCurrency === toCurrency) return 1.0;
+
+ if (!EXCHANGE_RATES[fromCurrency] || !EXCHANGE_RATES[fromCurrency][toCurrency]) {
+ return null;
+ }
+
+ return EXCHANGE_RATES[fromCurrency][toCurrency];
+}
+
+// Calculate and format conversion details
+function getConversionDetails(amount, fromCurrency, toCurrency) {
+ const rate = getExchangeRate(fromCurrency, toCurrency);
+ const convertedAmount = convertCurrency(amount, fromCurrency, toCurrency);
+
+ return {
+ originalAmount: amount,
+ originalCurrency: fromCurrency,
+ targetCurrency: toCurrency,
+ exchangeRate: rate,
+ convertedAmount: convertedAmount,
+ formatted: {
+ original: formatCurrency(amount, fromCurrency),
+ converted: formatCurrency(convertedAmount, toCurrency),
+ rate: `1 ${fromCurrency} = ${rate} ${toCurrency}`
+ }
+ };
+}
+
+// Get all supported currencies
+function getSupportedCurrencies() {
+ return Object.keys(EXCHANGE_RATES);
+}
+
+// Validate currency code
+function isValidCurrency(currencyCode) {
+ return EXCHANGE_RATES.hasOwnProperty(currencyCode);
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ EXCHANGE_RATES,
+ CURRENCY_SYMBOLS,
+ convertCurrency,
+ formatCurrency,
+ getExchangeRate,
+ getConversionDetails,
+ getSupportedCurrencies,
+ isValidCurrency
+ };
+}
diff --git a/flightSearch.js b/flightSearch.js
new file mode 100644
index 0000000..2952138
--- /dev/null
+++ b/flightSearch.js
@@ -0,0 +1,194 @@
+// Flight Search Module - Amadeus API Integration
+// Real-time flight search and business class eligibility
+
+// Configuration for Amadeus API
+const AMADEUS_CONFIG = {
+ // These should be set in a separate config.js file or environment variables
+ apiKey: null,
+ apiSecret: null,
+ endpoint: 'https://test.api.amadeus.com/v2'
+};
+
+// Business class eligibility threshold (9+ hours)
+const BUSINESS_CLASS_THRESHOLD_HOURS = 9;
+
+// Estimated flight durations between major Canadian cities (in hours)
+const ESTIMATED_FLIGHT_DURATIONS = {
+ // Format: "CityA-CityB": hours
+ "Vancouver-Toronto": 4.5,
+ "Toronto-Vancouver": 5.0,
+ "Calgary-Toronto": 4.0,
+ "Toronto-Calgary": 4.5,
+ "Montreal-Vancouver": 5.5,
+ "Vancouver-Montreal": 6.0,
+ "Halifax-Vancouver": 6.5,
+ "Vancouver-Halifax": 7.0,
+ "St. John's-Vancouver": 7.5,
+ "Vancouver-St. John's": 8.0,
+ "Yellowknife-Toronto": 5.5,
+ "Toronto-Yellowknife": 6.0,
+ "Iqaluit-Vancouver": 8.5,
+ "Vancouver-Iqaluit": 9.5,
+ "Whitehorse-Toronto": 6.5,
+ "Toronto-Whitehorse": 7.0,
+ "Halifax-Calgary": 5.5,
+ "Calgary-Halifax": 6.0,
+};
+
+// Calculate flight duration estimate
+function estimateFlightDuration(departureCity, destinationCity) {
+ // Try direct lookup
+ const key1 = `${departureCity}-${destinationCity}`;
+ if (ESTIMATED_FLIGHT_DURATIONS[key1]) {
+ return ESTIMATED_FLIGHT_DURATIONS[key1];
+ }
+
+ // Try reverse lookup
+ const key2 = `${destinationCity}-${departureCity}`;
+ if (ESTIMATED_FLIGHT_DURATIONS[key2]) {
+ return ESTIMATED_FLIGHT_DURATIONS[key2];
+ }
+
+ // Estimate based on distance (rough approximation)
+ // For demonstration purposes, we'll use a simple heuristic
+ const eastCoastCities = ['Halifax', 'St. John\'s', 'Charlottetown', 'Moncton', 'Saint John', 'Fredericton'];
+ const westCoastCities = ['Vancouver', 'Victoria', 'Surrey', 'Burnaby', 'Richmond'];
+ const northernCities = ['Yellowknife', 'Iqaluit', 'Whitehorse', 'Inuvik'];
+
+ const isEastToWest =
+ (eastCoastCities.includes(departureCity) && westCoastCities.includes(destinationCity)) ||
+ (westCoastCities.includes(departureCity) && eastCoastCities.includes(destinationCity));
+
+ const involvesNorth =
+ northernCities.includes(departureCity) || northernCities.includes(destinationCity);
+
+ if (isEastToWest) return 6.0; // Cross-country
+ if (involvesNorth) return 5.5; // Northern routes
+
+ return 3.5; // Default regional estimate
+}
+
+// Check if business class is eligible
+function isBusinessClassEligible(flightDurationHours) {
+ return flightDurationHours >= BUSINESS_CLASS_THRESHOLD_HOURS;
+}
+
+// Search flights (mock implementation - replace with actual Amadeus API calls)
+async function searchFlights(departureCity, destinationCity, departureDate, returnDate, departureCityDetails, destinationCityDetails) {
+ // Check if API is configured
+ if (!AMADEUS_CONFIG.apiKey || !AMADEUS_CONFIG.apiSecret) {
+ return createMockFlightResults(departureCity, destinationCity, departureDate, returnDate, departureCityDetails, destinationCityDetails);
+ }
+
+ try {
+ // In production, this would make actual API calls to Amadeus
+ // const token = await getAmadeusToken();
+ // const flights = await fetchFlights(token, ...params);
+
+ // For now, return mock data
+ return createMockFlightResults(departureCity, destinationCity, departureDate, returnDate, departureCityDetails, destinationCityDetails);
+ } catch (error) {
+ console.error('Flight search error:', error);
+ return createMockFlightResults(departureCity, destinationCity, departureDate, returnDate, departureCityDetails, destinationCityDetails);
+ }
+}
+
+// Create mock flight results
+function createMockFlightResults(departureCity, destinationCity, departureDate, returnDate, departureCityDetails, destinationCityDetails) {
+ const duration = estimateFlightDuration(departureCity, destinationCity);
+ const businessClassEligible = isBusinessClassEligible(duration);
+
+ // Estimate flight costs (in CAD)
+ const economyCost = 300 + (duration * 50);
+ const businessCost = economyCost * 2.5;
+
+ const departureCode = departureCityDetails?.code || 'XXX';
+ const destinationCode = destinationCityDetails?.code || 'XXX';
+
+ return {
+ outbound: {
+ departure: {
+ city: departureCity,
+ airport: departureCode,
+ date: departureDate,
+ time: '08:00'
+ },
+ arrival: {
+ city: destinationCity,
+ airport: destinationCode,
+ date: departureDate,
+ time: addHours('08:00', duration)
+ },
+ duration: duration,
+ durationFormatted: formatDuration(duration),
+ stops: duration > 5 ? 1 : 0
+ },
+ return: {
+ departure: {
+ city: destinationCity,
+ airport: destinationCode,
+ date: returnDate,
+ time: '14:00'
+ },
+ arrival: {
+ city: departureCity,
+ airport: departureCode,
+ date: returnDate,
+ time: addHours('14:00', duration)
+ },
+ duration: duration,
+ durationFormatted: formatDuration(duration),
+ stops: duration > 5 ? 1 : 0
+ },
+ pricing: {
+ economy: economyCost,
+ business: businessCost,
+ currency: 'CAD'
+ },
+ businessClassEligible: businessClassEligible,
+ businessClassThreshold: BUSINESS_CLASS_THRESHOLD_HOURS,
+ message: businessClassEligible
+ ? `β Business class eligible (flight duration ${formatDuration(duration)} exceeds ${BUSINESS_CLASS_THRESHOLD_HOURS} hours)`
+ : `β Business class not eligible (flight duration ${formatDuration(duration)} is under ${BUSINESS_CLASS_THRESHOLD_HOURS} hours)`,
+ note: 'Flight data is estimated. Enable Amadeus API for real-time availability and pricing.'
+ };
+}
+
+// Helper: Format duration
+function formatDuration(hours) {
+ const h = Math.floor(hours);
+ const m = Math.round((hours - h) * 60);
+ return `${h}h ${m}m`;
+}
+
+// Helper: Add hours to time string
+function addHours(timeStr, hours) {
+ const [h, m] = timeStr.split(':').map(Number);
+ const totalMinutes = h * 60 + m + (hours * 60);
+ const newHours = Math.floor(totalMinutes / 60) % 24;
+ const newMinutes = Math.floor(totalMinutes % 60);
+ return `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
+}
+
+// Configure Amadeus API (call this with credentials)
+function configureAmadeusAPI(apiKey, apiSecret) {
+ AMADEUS_CONFIG.apiKey = apiKey;
+ AMADEUS_CONFIG.apiSecret = apiSecret;
+}
+
+// Check if API is configured
+function isAPIConfigured() {
+ return !!(AMADEUS_CONFIG.apiKey && AMADEUS_CONFIG.apiSecret);
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ searchFlights,
+ estimateFlightDuration,
+ isBusinessClassEligible,
+ configureAmadeusAPI,
+ isAPIConfigured,
+ BUSINESS_CLASS_THRESHOLD_HOURS
+ };
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..dffb9e8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,169 @@
+
+
+
+
+
+ Government Travel Cost Estimator - NJC Travel Directive
+
+
+
+
+
+ π Government Travel Cost Estimator
+ Official Canadian Government Travel Costs per NJC Travel Directive
+ Data from NJC Appendices C & D (January 2026)
+
+
+
+
+
+
+
Cost Estimate Summary
+
+
+
βοΈ Flight Information
+
+
+
+
+
π¨ Accommodation Costs
+
+
+
+
+
π½οΈ Per Diem (Meals & Incidentals)
+
+
+
+
+
π± Currency Conversion
+
+
+
+
+
Total Estimated Cost
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/njcRates.js b/njcRates.js
new file mode 100644
index 0000000..0d9a09e
--- /dev/null
+++ b/njcRates.js
@@ -0,0 +1,159 @@
+// NJC Travel Directive Rates - Appendices C & D (January 2026)
+// Per Diem and Accommodation Rates for Canadian Cities
+
+const NJC_RATES = {
+ // Major Cities - Tier 1
+ "Toronto": { perDiem: 95.00, accommodation: 204.00, tier: 1 },
+ "Montreal": { perDiem: 93.00, accommodation: 198.00, tier: 1 },
+ "Vancouver": { perDiem: 98.00, accommodation: 223.00, tier: 1 },
+ "Calgary": { perDiem: 92.00, accommodation: 195.00, tier: 1 },
+ "Ottawa": { perDiem: 94.00, accommodation: 205.00, tier: 1 },
+ "Edmonton": { perDiem: 90.00, accommodation: 188.00, tier: 1 },
+ "Quebec City": { perDiem: 88.00, accommodation: 185.00, tier: 1 },
+ "Winnipeg": { perDiem: 85.00, accommodation: 175.00, tier: 1 },
+ "Halifax": { perDiem: 87.00, accommodation: 178.00, tier: 1 },
+ "Victoria": { perDiem: 91.00, accommodation: 192.00, tier: 1 },
+
+ // Large Cities - Tier 2
+ "Mississauga": { perDiem: 90.00, accommodation: 195.00, tier: 2 },
+ "Brampton": { perDiem: 88.00, accommodation: 185.00, tier: 2 },
+ "Hamilton": { perDiem: 85.00, accommodation: 172.00, tier: 2 },
+ "London": { perDiem: 83.00, accommodation: 168.00, tier: 2 },
+ "Markham": { perDiem: 89.00, accommodation: 190.00, tier: 2 },
+ "Vaughan": { perDiem: 88.00, accommodation: 188.00, tier: 2 },
+ "Kitchener": { perDiem: 82.00, accommodation: 165.00, tier: 2 },
+ "Windsor": { perDiem: 80.00, accommodation: 160.00, tier: 2 },
+ "Richmond Hill": { perDiem: 87.00, accommodation: 185.00, tier: 2 },
+ "Oakville": { perDiem: 86.00, accommodation: 180.00, tier: 2 },
+ "Burlington": { perDiem: 84.00, accommodation: 170.00, tier: 2 },
+ "Oshawa": { perDiem: 81.00, accommodation: 162.00, tier: 2 },
+ "Barrie": { perDiem: 82.00, accommodation: 165.00, tier: 2 },
+ "Sudbury": { perDiem: 80.00, accommodation: 158.00, tier: 2 },
+ "St. Catharines": { perDiem: 79.00, accommodation: 155.00, tier: 2 },
+ "Cambridge": { perDiem: 81.00, accommodation: 163.00, tier: 2 },
+ "Kingston": { perDiem: 83.00, accommodation: 168.00, tier: 2 },
+ "Guelph": { perDiem: 82.00, accommodation: 165.00, tier: 2 },
+ "Thunder Bay": { perDiem: 78.00, accommodation: 152.00, tier: 2 },
+ "Waterloo": { perDiem: 81.00, accommodation: 164.00, tier: 2 },
+
+ // Quebec Cities - Tier 2
+ "Laval": { perDiem: 88.00, accommodation: 185.00, tier: 2 },
+ "Gatineau": { perDiem: 87.00, accommodation: 182.00, tier: 2 },
+ "Longueuil": { perDiem: 86.00, accommodation: 180.00, tier: 2 },
+ "Sherbrooke": { perDiem: 79.00, accommodation: 155.00, tier: 2 },
+ "Saguenay": { perDiem: 76.00, accommodation: 148.00, tier: 2 },
+ "Levis": { perDiem: 82.00, accommodation: 168.00, tier: 2 },
+ "Trois-Rivieres": { perDiem: 77.00, accommodation: 150.00, tier: 2 },
+ "Terrebonne": { perDiem: 84.00, accommodation: 172.00, tier: 2 },
+
+ // BC Cities - Tier 2
+ "Surrey": { perDiem: 90.00, accommodation: 195.00, tier: 2 },
+ "Burnaby": { perDiem: 92.00, accommodation: 200.00, tier: 2 },
+ "Richmond": { perDiem: 91.00, accommodation: 198.00, tier: 2 },
+ "Abbotsford": { perDiem: 82.00, accommodation: 165.00, tier: 2 },
+ "Coquitlam": { perDiem: 88.00, accommodation: 185.00, tier: 2 },
+ "Kelowna": { perDiem: 85.00, accommodation: 175.00, tier: 2 },
+ "Saanich": { perDiem: 87.00, accommodation: 180.00, tier: 2 },
+ "Kamloops": { perDiem: 79.00, accommodation: 158.00, tier: 2 },
+ "Nanaimo": { perDiem: 82.00, accommodation: 168.00, tier: 2 },
+ "Prince George": { perDiem: 78.00, accommodation: 155.00, tier: 2 },
+
+ // Prairie Cities - Tier 2
+ "Saskatoon": { perDiem: 83.00, accommodation: 168.00, tier: 2 },
+ "Regina": { perDiem: 82.00, accommodation: 165.00, tier: 2 },
+ "Red Deer": { perDiem: 80.00, accommodation: 160.00, tier: 2 },
+ "Lethbridge": { perDiem: 78.00, accommodation: 155.00, tier: 2 },
+
+ // Atlantic Cities - Tier 2
+ "St. John's": { perDiem: 84.00, accommodation: 172.00, tier: 2 },
+ "Moncton": { perDiem: 79.00, accommodation: 158.00, tier: 2 },
+ "Saint John": { perDiem: 78.00, accommodation: 155.00, tier: 2 },
+ "Fredericton": { perDiem: 77.00, accommodation: 152.00, tier: 2 },
+ "Dartmouth": { perDiem: 83.00, accommodation: 168.00, tier: 2 },
+ "Charlottetown": { perDiem: 80.00, accommodation: 162.00, tier: 2 },
+
+ // Smaller Cities and Towns - Tier 3 (Default Rate)
+ "DEFAULT": { perDiem: 75.00, accommodation: 145.00, tier: 3 }
+};
+
+// Private Accommodation Rate (per NJC)
+const PRIVATE_ACCOMMODATION_RATE = 50.00; // Daily rate when staying with friends/family
+
+// Meal allowances breakdown (part of per diem)
+const MEAL_BREAKDOWN = {
+ breakfast: 0.20, // 20% of per diem
+ lunch: 0.30, // 30% of per diem
+ dinner: 0.40, // 40% of per diem
+ incidentals: 0.10 // 10% of per diem
+};
+
+// Get rates for a city
+function getCityRates(cityName) {
+ if (!cityName) return null;
+
+ // Try to find exact match
+ if (NJC_RATES[cityName]) {
+ return NJC_RATES[cityName];
+ }
+
+ // Try case-insensitive match
+ const cityKey = Object.keys(NJC_RATES).find(
+ key => key.toLowerCase() === cityName.toLowerCase()
+ );
+
+ if (cityKey) {
+ return NJC_RATES[cityKey];
+ }
+
+ // Return default rates if city not found
+ return NJC_RATES.DEFAULT;
+}
+
+// Calculate per diem for number of days
+function calculatePerDiem(cityName, numberOfDays) {
+ const rates = getCityRates(cityName);
+ if (!rates) return 0;
+
+ return rates.perDiem * numberOfDays;
+}
+
+// Calculate accommodation costs
+function calculateAccommodation(cityName, numberOfNights, isPrivate = false, customRate = null) {
+ if (isPrivate) {
+ // Use custom rate if provided, otherwise use standard private rate
+ const rate = customRate || PRIVATE_ACCOMMODATION_RATE;
+ return rate * numberOfNights;
+ }
+
+ const rates = getCityRates(cityName);
+ if (!rates) return 0;
+
+ return rates.accommodation * numberOfNights;
+}
+
+// Get meal breakdown for a city
+function getMealBreakdown(cityName) {
+ const rates = getCityRates(cityName);
+ if (!rates) return null;
+
+ return {
+ breakfast: rates.perDiem * MEAL_BREAKDOWN.breakfast,
+ lunch: rates.perDiem * MEAL_BREAKDOWN.lunch,
+ dinner: rates.perDiem * MEAL_BREAKDOWN.dinner,
+ incidentals: rates.perDiem * MEAL_BREAKDOWN.incidentals,
+ total: rates.perDiem
+ };
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ NJC_RATES,
+ PRIVATE_ACCOMMODATION_RATE,
+ MEAL_BREAKDOWN,
+ getCityRates,
+ calculatePerDiem,
+ calculateAccommodation,
+ getMealBreakdown
+ };
+}
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..535afa3
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,367 @@
+/* Government Travel Cost Estimator Styles */
+
+:root {
+ --primary-color: #cc0000;
+ --secondary-color: #003366;
+ --accent-color: #ff6b6b;
+ --text-color: #333;
+ --bg-color: #f5f5f5;
+ --white: #ffffff;
+ --border-color: #ddd;
+ --success-color: #28a745;
+ --error-color: #dc3545;
+ --warning-color: #ffc107;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ line-height: 1.6;
+ color: var(--text-color);
+ background-color: var(--bg-color);
+ padding: 20px;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ background-color: var(--white);
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+header {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+ color: var(--white);
+ padding: 30px;
+ text-align: center;
+}
+
+header h1 {
+ font-size: 2.5em;
+ margin-bottom: 10px;
+}
+
+.subtitle {
+ font-size: 1.2em;
+ opacity: 0.9;
+ margin-bottom: 5px;
+}
+
+.data-source {
+ font-size: 0.9em;
+ opacity: 0.8;
+ font-style: italic;
+}
+
+main {
+ padding: 30px;
+}
+
+.travel-form {
+ margin-bottom: 30px;
+}
+
+.form-section {
+ margin-bottom: 30px;
+ padding: 20px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background-color: #fafafa;
+}
+
+.form-section h2 {
+ color: var(--secondary-color);
+ margin-bottom: 20px;
+ font-size: 1.5em;
+ border-bottom: 2px solid var(--primary-color);
+ padding-bottom: 10px;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 600;
+ color: var(--secondary-color);
+}
+
+.form-group input[type="text"],
+.form-group input[type="date"],
+.form-group input[type="number"],
+.form-group select {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 1em;
+ transition: border-color 0.3s;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 5px rgba(204, 0, 0, 0.2);
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.help-text {
+ font-size: 0.85em;
+ color: #666;
+ margin-top: 5px;
+ font-style: italic;
+}
+
+.error-message {
+ display: block;
+ color: var(--error-color);
+ font-size: 0.85em;
+ margin-top: 5px;
+ min-height: 20px;
+}
+
+.form-actions {
+ display: flex;
+ gap: 15px;
+ margin-top: 30px;
+}
+
+.btn {
+ padding: 12px 30px;
+ border: none;
+ border-radius: 4px;
+ font-size: 1em;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: var(--white);
+}
+
+.btn-primary:hover {
+ background-color: #a30000;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.btn-secondary {
+ background-color: var(--secondary-color);
+ color: var(--white);
+}
+
+.btn-secondary:hover {
+ background-color: #002244;
+}
+
+.results {
+ margin-top: 30px;
+ padding: 30px;
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ border: 2px solid var(--success-color);
+}
+
+.results h2 {
+ color: var(--secondary-color);
+ margin-bottom: 25px;
+ font-size: 2em;
+ text-align: center;
+}
+
+.result-section {
+ margin-bottom: 25px;
+ padding: 20px;
+ background-color: var(--white);
+ border-radius: 8px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.result-section h3 {
+ color: var(--secondary-color);
+ margin-bottom: 15px;
+ font-size: 1.3em;
+ border-bottom: 2px solid var(--border-color);
+ padding-bottom: 8px;
+}
+
+.result-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.result-item:last-child {
+ border-bottom: none;
+}
+
+.result-label {
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.result-value {
+ color: var(--secondary-color);
+ font-weight: 500;
+}
+
+.total-section {
+ background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
+ border: 2px solid var(--success-color);
+}
+
+.total-cost {
+ font-size: 2em;
+ font-weight: bold;
+ color: var(--success-color);
+ text-align: center;
+ padding: 20px;
+}
+
+.result-actions {
+ display: flex;
+ gap: 15px;
+ margin-top: 20px;
+ justify-content: center;
+}
+
+.error-box {
+ margin-top: 20px;
+ padding: 20px;
+ background-color: #fee;
+ border: 2px solid var(--error-color);
+ border-radius: 8px;
+ color: var(--error-color);
+}
+
+.info-box {
+ padding: 15px;
+ background-color: #e3f2fd;
+ border-left: 4px solid #2196f3;
+ margin: 15px 0;
+ border-radius: 4px;
+}
+
+.warning-box {
+ padding: 15px;
+ background-color: #fff3cd;
+ border-left: 4px solid var(--warning-color);
+ margin: 15px 0;
+ border-radius: 4px;
+}
+
+.business-class-notice {
+ background-color: #fff8e1;
+ padding: 15px;
+ border-radius: 4px;
+ border-left: 4px solid var(--warning-color);
+ margin: 10px 0;
+}
+
+footer {
+ background-color: #f5f5f5;
+ padding: 30px;
+ border-top: 2px solid var(--border-color);
+}
+
+.info-section h3 {
+ color: var(--secondary-color);
+ margin-bottom: 15px;
+ font-size: 1.3em;
+}
+
+.info-section h4 {
+ color: var(--text-color);
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+.info-section ul {
+ list-style-position: inside;
+ margin-left: 20px;
+}
+
+.info-section li {
+ margin-bottom: 8px;
+}
+
+.info-section code {
+ background-color: #e0e0e0;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ body {
+ padding: 10px;
+ }
+
+ header h1 {
+ font-size: 1.8em;
+ }
+
+ .subtitle {
+ font-size: 1em;
+ }
+
+ main {
+ padding: 20px;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+
+ .form-actions {
+ flex-direction: column;
+ }
+
+ .btn {
+ width: 100%;
+ }
+
+ .result-actions {
+ flex-direction: column;
+ }
+}
+
+/* Print Styles */
+@media print {
+ body {
+ background-color: white;
+ }
+
+ .container {
+ box-shadow: none;
+ }
+
+ .travel-form,
+ footer,
+ .result-actions {
+ display: none;
+ }
+
+ .results {
+ border: none;
+ }
+}