@per-diem-calculator/vanilla 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11034 -0
- package/dist/index.umd.cjs +1384 -0
- package/package.json +12 -2
- package/.prettierrc +0 -17
- package/eslint.config.js +0 -29
- package/index.html +0 -11
- package/src/css/_styles.css +0 -8
- package/src/css/colors.css +0 -45
- package/src/css/fonts.css +0 -9
- package/src/css/rows/_heights.css +0 -6
- package/src/css/rows/_index.css +0 -15
- package/src/css/rows/add.css +0 -18
- package/src/css/rows/animate-btns.css +0 -18
- package/src/css/rows/animate-row-close.css +0 -18
- package/src/css/rows/animate-row-open.css +0 -14
- package/src/css/rows/animate-row-other.css +0 -5
- package/src/css/rows/btn-add-row.css +0 -41
- package/src/css/rows/btn-delete.css +0 -22
- package/src/css/rows/btn-expenses-calculate.css +0 -22
- package/src/css/rows/btn-expenses-category.css +0 -22
- package/src/css/rows/delete.css +0 -10
- package/src/css/rows/details.css +0 -22
- package/src/css/rows/expense.css +0 -18
- package/src/css/rows/location.css +0 -34
- package/src/css/rows/summary.css +0 -22
- package/src/css/tom-select/defaults.css +0 -530
- package/src/css/tom-select/overrides.css +0 -55
- package/src/css/tw-shadow-props.css +0 -50
- package/src/index.ts +0 -1
- package/src/ts/components/Button/Button.ts +0 -50
- package/src/ts/components/Button/template.html +0 -34
- package/src/ts/components/ExpenseRow/ExpenseRow.ts +0 -397
- package/src/ts/components/ExpenseRow/template.html +0 -260
- package/src/ts/components/Label/Label.ts +0 -45
- package/src/ts/components/Label/template.html +0 -1
- package/src/ts/components/LocationCategory/LocationCategory.ts +0 -226
- package/src/ts/components/LocationCategory/template.html +0 -520
- package/src/ts/components/LocationDate/LocationDate.ts +0 -366
- package/src/ts/components/LocationDate/template.html +0 -27
- package/src/ts/components/LocationSelect/LocationSelect.ts +0 -299
- package/src/ts/components/LocationSelect/template.html +0 -45
- package/src/ts/components/index.ts +0 -6
- package/src/ts/controller.ts +0 -193
- package/src/ts/model.ts +0 -163
- package/src/ts/types/config.ts +0 -22
- package/src/ts/types/dates.ts +0 -82
- package/src/ts/types/expenses.ts +0 -73
- package/src/ts/types/locations.ts +0 -25
- package/src/ts/utils/config/configDefault.ts +0 -13
- package/src/ts/utils/config/index.ts +0 -12
- package/src/ts/utils/config/numbers.ts +0 -24
- package/src/ts/utils/config/sanitizeConfig.ts +0 -39
- package/src/ts/utils/dates/INPUT_DATE_MINMAX.ts +0 -5
- package/src/ts/utils/dates/YEAR_REGEX.ts +0 -4
- package/src/ts/utils/dates/getDateSlice.ts +0 -54
- package/src/ts/utils/dates/getValidAPIYear.ts +0 -17
- package/src/ts/utils/dates/index.ts +0 -19
- package/src/ts/utils/dates/isDateRaw.ts +0 -90
- package/src/ts/utils/dates/isShortMonth.ts +0 -24
- package/src/ts/utils/dates/isYYYY.ts +0 -10
- package/src/ts/utils/dates/offsetDateString.ts +0 -17
- package/src/ts/utils/expenses/INTL_MIE_RATES.ts +0 -2125
- package/src/ts/utils/expenses/createExpenseObjs.ts +0 -35
- package/src/ts/utils/expenses/getLodgingRateDomestic.ts +0 -73
- package/src/ts/utils/expenses/getLodgingRateIntl.ts +0 -119
- package/src/ts/utils/expenses/getMieRates.ts +0 -84
- package/src/ts/utils/expenses/index.ts +0 -5
- package/src/ts/utils/expenses/parseIntlLodgingRates.ts +0 -124
- package/src/ts/utils/expenses/returnValidStateExpense.ts +0 -46
- package/src/ts/utils/fetch/fetchJsonGSA.ts +0 -29
- package/src/ts/utils/fetch/fetchXmlDOD.ts +0 -38
- package/src/ts/utils/fetch/index.ts +0 -3
- package/src/ts/utils/fetch/memoize.ts +0 -46
- package/src/ts/utils/fetch/parseXml.ts +0 -19
- package/src/ts/utils/locations/getCitiesDomestic.ts +0 -48
- package/src/ts/utils/locations/getCitiesIntl.ts +0 -63
- package/src/ts/utils/locations/getCountriesDomestic.ts +0 -237
- package/src/ts/utils/locations/getCountriesIntl.ts +0 -34
- package/src/ts/utils/locations/index.ts +0 -6
- package/src/ts/utils/locations/keepUniqueLocations.ts +0 -12
- package/src/ts/utils/locations/locationKeys.ts +0 -10
- package/src/ts/utils/locations/returnValidStateLocation.ts +0 -13
- package/src/ts/utils/locations/sortLocations.ts +0 -19
- package/src/ts/utils/misc/USD.ts +0 -4
- package/src/ts/utils/misc/debounce.ts +0 -22
- package/src/ts/utils/misc/handlePointerDown.ts +0 -3
- package/src/ts/utils/misc/handlePointerUp.ts +0 -22
- package/src/ts/utils/misc/inPrimitiveType.ts +0 -4
- package/src/ts/utils/misc/index.ts +0 -6
- package/src/ts/utils/misc/wait.ts +0 -4
- package/src/ts/utils/styles/applyStyles.ts +0 -19
- package/src/ts/utils/styles/highlightInput.ts +0 -15
- package/src/ts/utils/styles/index.ts +0 -3
- package/src/ts/utils/styles/removeStyles.ts +0 -14
- package/src/ts/views/Expense/Expense.ts +0 -465
- package/src/ts/views/Expense/template.html +0 -176
- package/src/ts/views/Location/Location.ts +0 -763
- package/src/ts/views/Location/template-row.html +0 -146
- package/src/ts/views/Location/template.html +0 -130
- package/src/ts/views/index.ts +0 -2
- package/tsconfig.json +0 -27
- package/vite.config.ts +0 -12
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { StateLocationItemValid } from '../../types/locations';
|
|
3
|
-
import type { StateExpenseItem } from '../../types/expenses';
|
|
4
|
-
|
|
5
|
-
// Utils
|
|
6
|
-
import { isDateRawType, getDateRaw } from '../dates';
|
|
7
|
-
|
|
8
|
-
export const createExpenseObjs = (
|
|
9
|
-
location: StateLocationItemValid,
|
|
10
|
-
): StateExpenseItem[] => {
|
|
11
|
-
const { start, end, country, city } = location;
|
|
12
|
-
const expenses: StateExpenseItem[] = [];
|
|
13
|
-
const currentDate = new Date(start);
|
|
14
|
-
const lastDate = new Date(end);
|
|
15
|
-
while (currentDate <= lastDate) {
|
|
16
|
-
const currentDateRaw = getDateRaw(currentDate.toISOString());
|
|
17
|
-
if (!isDateRawType(currentDateRaw))
|
|
18
|
-
throw new Error('Failed to create valid date.');
|
|
19
|
-
expenses.push({
|
|
20
|
-
date: currentDateRaw,
|
|
21
|
-
country: country,
|
|
22
|
-
city: city,
|
|
23
|
-
deductions: {
|
|
24
|
-
FirstLastDay: false,
|
|
25
|
-
breakfastProvided: false,
|
|
26
|
-
lunchProvided: false,
|
|
27
|
-
dinnerProvided: false,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
|
|
31
|
-
}
|
|
32
|
-
expenses[0].deductions.FirstLastDay = true;
|
|
33
|
-
expenses[expenses.length - 1].deductions.FirstLastDay = true;
|
|
34
|
-
return expenses;
|
|
35
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { RateLodging, StateExpenseItem } from '../../types/expenses';
|
|
3
|
-
import type { ShortMonth } from '../../types/dates';
|
|
4
|
-
|
|
5
|
-
// Utils
|
|
6
|
-
import { fetchJsonGSA } from '../fetch';
|
|
7
|
-
import {
|
|
8
|
-
isDateRawType,
|
|
9
|
-
getValidAPIYear,
|
|
10
|
-
isShortMonth,
|
|
11
|
-
getShortMonth,
|
|
12
|
-
getYYYY,
|
|
13
|
-
getMM,
|
|
14
|
-
} from '../dates';
|
|
15
|
-
|
|
16
|
-
const getRate = (
|
|
17
|
-
expense: StateExpenseItem,
|
|
18
|
-
rates: RateLodging[],
|
|
19
|
-
): RateLodging => {
|
|
20
|
-
const lodgingRate = rates.find(
|
|
21
|
-
rate =>
|
|
22
|
-
rate.State !== null &&
|
|
23
|
-
rate.State === expense.country &&
|
|
24
|
-
rate.City === expense.city,
|
|
25
|
-
);
|
|
26
|
-
if (!lodgingRate)
|
|
27
|
-
throw new Error(
|
|
28
|
-
'Failed to find lodging rate in fetched data from GSA.',
|
|
29
|
-
);
|
|
30
|
-
return lodgingRate;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const createStateExpenseItem = (
|
|
34
|
-
expense: StateExpenseItem,
|
|
35
|
-
lodgingRate: RateLodging,
|
|
36
|
-
lodgingMonth: ShortMonth,
|
|
37
|
-
): StateExpenseItem => {
|
|
38
|
-
const effDate = `${getYYYY(expense.date)}-${getMM(expense.date)}-01`;
|
|
39
|
-
if (!isDateRawType(effDate))
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Failed to create valid effective date for rate: ${expense.date} - ${expense.city}`,
|
|
42
|
-
);
|
|
43
|
-
return {
|
|
44
|
-
...expense,
|
|
45
|
-
rates: {
|
|
46
|
-
maxLodging: +lodgingRate[lodgingMonth],
|
|
47
|
-
maxMie: +lodgingRate.Meals,
|
|
48
|
-
effDate,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const getLodgingRateDomestic = async (
|
|
54
|
-
expense: StateExpenseItem,
|
|
55
|
-
): Promise<StateExpenseItem> => {
|
|
56
|
-
try {
|
|
57
|
-
const date = new Date(expense.date);
|
|
58
|
-
const lodgingMonth = getShortMonth(date.toUTCString());
|
|
59
|
-
if (!isShortMonth(lodgingMonth))
|
|
60
|
-
throw new Error('Invalid month for fetching rates from GSA.');
|
|
61
|
-
|
|
62
|
-
return await fetchJsonGSA<RateLodging>(
|
|
63
|
-
getValidAPIYear(expense.date),
|
|
64
|
-
'lodging',
|
|
65
|
-
)
|
|
66
|
-
.then(rates => getRate(expense, rates))
|
|
67
|
-
.then(rate => createStateExpenseItem(expense, rate, lodgingMonth));
|
|
68
|
-
} catch (error) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`Failed to get lodging rate for ${expense.date} = ${expense.city} - ${error}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { StateExpenseItem } from '../../types/expenses';
|
|
3
|
-
import type { DateRaw } from '../../types/dates';
|
|
4
|
-
|
|
5
|
-
// Utils
|
|
6
|
-
import { fetchXmlDOD, parseXml } from '../fetch';
|
|
7
|
-
import { parseIntlLodgingRates } from './parseIntlLodgingRates';
|
|
8
|
-
import { getValidAPIYear, isDateRawType, getYYYY, getDateRaw } from '../dates';
|
|
9
|
-
|
|
10
|
-
const getRecords = async (expense: StateExpenseItem): Promise<Element[]> => {
|
|
11
|
-
const data = await fetchXmlDOD(getValidAPIYear(expense.date));
|
|
12
|
-
if (!data) throw new Error(`Error getting rates from XML`);
|
|
13
|
-
return parseXml(
|
|
14
|
-
data,
|
|
15
|
-
`//record[location_name[text()="${expense.city}"]]`,
|
|
16
|
-
) as Element[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const getRecord = (expense: StateExpenseItem, records: Element[]) => {
|
|
20
|
-
const { date, country, city } = expense;
|
|
21
|
-
const record = parseIntlLodgingRates(date, country, records);
|
|
22
|
-
if (!record)
|
|
23
|
-
throw new Error(
|
|
24
|
-
`Failed to pull intl rate from XML records for ${date}: ${country} - ${city}.`,
|
|
25
|
-
);
|
|
26
|
-
return record;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const getRecordEls = (record: Element) => {
|
|
30
|
-
const lodgingText = record.querySelector('lodging_rate')?.textContent;
|
|
31
|
-
const mieText = record.querySelector('local_meals')?.textContent;
|
|
32
|
-
const rateStartDateText = record.querySelector('start_date')?.textContent;
|
|
33
|
-
|
|
34
|
-
if (!(lodgingText && mieText && rateStartDateText))
|
|
35
|
-
throw new Error(
|
|
36
|
-
'Failed to pull lodging rate, mie rate, effective date from XML records.',
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
return { lodgingText, mieText, rateStartDateText };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const createRatesObj = (
|
|
43
|
-
dateRaw: DateRaw,
|
|
44
|
-
lodgingText: string,
|
|
45
|
-
mieText: string,
|
|
46
|
-
rateStartDateText: string,
|
|
47
|
-
) => {
|
|
48
|
-
const maxLodging = +lodgingText;
|
|
49
|
-
const maxMie = +mieText;
|
|
50
|
-
|
|
51
|
-
// OCONUS has different rates with the same effective date but different season start/end dates
|
|
52
|
-
// We'll create an effective date based on the rate's season start date, and reduce the year by 1 if it's newer than the trip date
|
|
53
|
-
const rateStartDate = new Date(
|
|
54
|
-
`${getYYYY(dateRaw)}-${rateStartDateText.replaceAll('/', '-')}`,
|
|
55
|
-
);
|
|
56
|
-
const tripDate = new Date(dateRaw);
|
|
57
|
-
if (tripDate < rateStartDate)
|
|
58
|
-
rateStartDate.setUTCFullYear(+getYYYY(dateRaw) - 1);
|
|
59
|
-
const effDate = getDateRaw(rateStartDate.toISOString());
|
|
60
|
-
|
|
61
|
-
if (!isDateRawType(effDate))
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Failed to create valid effective date for ${dateRaw} `,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
maxLodging,
|
|
68
|
-
maxMie,
|
|
69
|
-
effDate,
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const createStateExpenseItem = (
|
|
74
|
-
expense: StateExpenseItem,
|
|
75
|
-
maxLodging: number,
|
|
76
|
-
maxMie: number,
|
|
77
|
-
effDate: DateRaw,
|
|
78
|
-
): StateExpenseItem => {
|
|
79
|
-
return {
|
|
80
|
-
...expense,
|
|
81
|
-
rates: {
|
|
82
|
-
maxLodging,
|
|
83
|
-
maxMie,
|
|
84
|
-
effDate,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export const getLodgingRateIntl = async (
|
|
90
|
-
expense: StateExpenseItem,
|
|
91
|
-
): Promise<StateExpenseItem> => {
|
|
92
|
-
try {
|
|
93
|
-
return await getRecords(expense)
|
|
94
|
-
.then(records => getRecord(expense, records))
|
|
95
|
-
.then(record => getRecordEls(record))
|
|
96
|
-
.then(recordEls => {
|
|
97
|
-
const { lodgingText, mieText, rateStartDateText } = recordEls;
|
|
98
|
-
return createRatesObj(
|
|
99
|
-
expense.date,
|
|
100
|
-
lodgingText,
|
|
101
|
-
mieText,
|
|
102
|
-
rateStartDateText,
|
|
103
|
-
);
|
|
104
|
-
})
|
|
105
|
-
.then(ratesObj => {
|
|
106
|
-
const { maxLodging, maxMie, effDate } = ratesObj;
|
|
107
|
-
return createStateExpenseItem(
|
|
108
|
-
expense,
|
|
109
|
-
maxLodging,
|
|
110
|
-
maxMie,
|
|
111
|
-
effDate,
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
} catch (error) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`Failed to get lodging rate for ${expense.date} - ${expense.city} - ${error}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type {
|
|
3
|
-
RateMeals,
|
|
4
|
-
ExpenseRates,
|
|
5
|
-
StateExpenseItem,
|
|
6
|
-
StateExpenseItemInclRates,
|
|
7
|
-
} from '../../types/expenses';
|
|
8
|
-
|
|
9
|
-
// Utils
|
|
10
|
-
import { fetchJsonGSA } from '../fetch';
|
|
11
|
-
import { INTL_MIE_RATES } from './INTL_MIE_RATES';
|
|
12
|
-
import { getValidAPIYear } from '../dates';
|
|
13
|
-
import { US_STATE_LENGTH } from '../config';
|
|
14
|
-
|
|
15
|
-
const getMealsRate = async (expense: StateExpenseItem): Promise<RateMeals> => {
|
|
16
|
-
if (!expense.rates?.maxMie)
|
|
17
|
-
throw new Error(
|
|
18
|
-
`Failed to get M&IE rates for ${expense.date} - ${expense.city} due to missing rate/deduction information in the expense object.`,
|
|
19
|
-
);
|
|
20
|
-
const mealsRates =
|
|
21
|
-
expense.country.length === US_STATE_LENGTH ?
|
|
22
|
-
await fetchJsonGSA<RateMeals>(getValidAPIYear(expense.date), 'mie')
|
|
23
|
-
: INTL_MIE_RATES;
|
|
24
|
-
const rate = mealsRates.find(rate => rate.total === expense.rates?.maxMie);
|
|
25
|
-
if (!rate)
|
|
26
|
-
throw new Error('Failed to find meal rates that matched total MIE.');
|
|
27
|
-
return rate;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const createRatesObject = (
|
|
31
|
-
expense: StateExpenseItem,
|
|
32
|
-
mealsRate: RateMeals,
|
|
33
|
-
): Required<ExpenseRates> => {
|
|
34
|
-
if (!expense.rates?.maxMie)
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Failed to get M&IE rates for ${expense.date} - ${expense.city} due to missing rate/deduction information in the expense object.`,
|
|
37
|
-
);
|
|
38
|
-
return {
|
|
39
|
-
...expense.rates,
|
|
40
|
-
maxMieFirstLast: mealsRate.FirstLastDay,
|
|
41
|
-
deductionBreakfast: mealsRate.breakfast,
|
|
42
|
-
deductionLunch: mealsRate.lunch,
|
|
43
|
-
deductionDinner: mealsRate.dinner,
|
|
44
|
-
maxIncidental: mealsRate.incidental,
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const createStateExpenseItem = (
|
|
49
|
-
expense: StateExpenseItem,
|
|
50
|
-
rates: Required<ExpenseRates>,
|
|
51
|
-
expensesCategory: string,
|
|
52
|
-
): StateExpenseItemInclRates => {
|
|
53
|
-
const lodgingAmount = expensesCategory === 'mie' ? 0 : rates.maxLodging;
|
|
54
|
-
const mieAmount =
|
|
55
|
-
expensesCategory === 'lodging' ? 0
|
|
56
|
-
: expense.deductions.FirstLastDay ? rates.maxMieFirstLast
|
|
57
|
-
: rates.maxMie;
|
|
58
|
-
const totalAmount = lodgingAmount + mieAmount;
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
...expense,
|
|
62
|
-
rates,
|
|
63
|
-
lodgingAmount,
|
|
64
|
-
mieAmount,
|
|
65
|
-
totalAmount,
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const getMieRates = async (
|
|
70
|
-
expensesCategory: string,
|
|
71
|
-
expense: StateExpenseItem,
|
|
72
|
-
): Promise<StateExpenseItemInclRates> => {
|
|
73
|
-
try {
|
|
74
|
-
return await getMealsRate(expense)
|
|
75
|
-
.then(mealsRate => createRatesObject(expense, mealsRate))
|
|
76
|
-
.then(rates =>
|
|
77
|
-
createStateExpenseItem(expense, rates, expensesCategory),
|
|
78
|
-
);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Failed to get mie rates for ${expense.date} - ${expense.city} - ${error}`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { createExpenseObjs } from './createExpenseObjs';
|
|
2
|
-
export { getLodgingRateDomestic } from './getLodgingRateDomestic';
|
|
3
|
-
export { getLodgingRateIntl } from './getLodgingRateIntl';
|
|
4
|
-
export { getMieRates } from './getMieRates';
|
|
5
|
-
export { returnValidStateExpense } from './returnValidStateExpense';
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { DateRaw } from '../../types/dates';
|
|
3
|
-
|
|
4
|
-
// Utils
|
|
5
|
-
import { getYYYY } from '../dates';
|
|
6
|
-
|
|
7
|
-
const getRecordValues = (record: Element) => {
|
|
8
|
-
const countryText = record.querySelector('country_name')?.textContent;
|
|
9
|
-
const effDateText = record.querySelector('eff_date')?.textContent;
|
|
10
|
-
const expDateText = record.querySelector('exp_date')?.textContent;
|
|
11
|
-
const startDateText = record.querySelector('start_date')?.textContent;
|
|
12
|
-
const endDateText = record.querySelector('end_date')?.textContent;
|
|
13
|
-
if (
|
|
14
|
-
!(
|
|
15
|
-
countryText &&
|
|
16
|
-
effDateText &&
|
|
17
|
-
expDateText &&
|
|
18
|
-
startDateText &&
|
|
19
|
-
endDateText
|
|
20
|
-
)
|
|
21
|
-
)
|
|
22
|
-
throw new Error(
|
|
23
|
-
'Failed to pull country_name, eff_date, exp_date, start_date, end_date from XML records.',
|
|
24
|
-
);
|
|
25
|
-
return {
|
|
26
|
-
countryText,
|
|
27
|
-
effDateText,
|
|
28
|
-
expDateText,
|
|
29
|
-
startDateText,
|
|
30
|
-
endDateText,
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const getRecordDates = (
|
|
35
|
-
year: string,
|
|
36
|
-
dateRaw: DateRaw,
|
|
37
|
-
effDateText: string,
|
|
38
|
-
expDateText: string,
|
|
39
|
-
startDateText: string,
|
|
40
|
-
endDateText: string,
|
|
41
|
-
) => {
|
|
42
|
-
// Create dates from all record elements
|
|
43
|
-
// start_date and end_date don't have years, so we artificially add them
|
|
44
|
-
// e.g. 01/15 -> 2024-01-15
|
|
45
|
-
const date = new Date(dateRaw);
|
|
46
|
-
const effDate = new Date(
|
|
47
|
-
`${effDateText.split('/')[2]}-${effDateText.split('/')[0]}-${effDateText.split('/')[1]}`,
|
|
48
|
-
);
|
|
49
|
-
const expDate = new Date(
|
|
50
|
-
`${expDateText.split('/')[2]}-${expDateText.split('/')[0]}-${expDateText.split('/')[1]}`,
|
|
51
|
-
);
|
|
52
|
-
const startDate = new Date(`${year}-${startDateText?.replace('/', '-')}`);
|
|
53
|
-
const endDate = new Date(`${year}-${endDateText?.replace('/', '-')}`);
|
|
54
|
-
if (!(date && effDate && expDate && startDate && endDate))
|
|
55
|
-
throw new Error(
|
|
56
|
-
'Failed to create Date objects using date columns from XML records.',
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return { date, effDate, expDate, startDate, endDate };
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const parseIntlLodgingRates = (
|
|
63
|
-
dateRaw: DateRaw,
|
|
64
|
-
country: string,
|
|
65
|
-
records: Element[],
|
|
66
|
-
) => {
|
|
67
|
-
return records.find(record => {
|
|
68
|
-
const year = getYYYY(dateRaw);
|
|
69
|
-
|
|
70
|
-
// Get record elements
|
|
71
|
-
const {
|
|
72
|
-
countryText,
|
|
73
|
-
effDateText,
|
|
74
|
-
expDateText,
|
|
75
|
-
startDateText,
|
|
76
|
-
endDateText,
|
|
77
|
-
} = getRecordValues(record);
|
|
78
|
-
|
|
79
|
-
// If it's not the country, no need to proceed further
|
|
80
|
-
if (countryText !== country) return false;
|
|
81
|
-
|
|
82
|
-
// Get dates from elements
|
|
83
|
-
const { date, effDate, expDate, startDate, endDate } = getRecordDates(
|
|
84
|
-
year,
|
|
85
|
-
dateRaw,
|
|
86
|
-
effDateText,
|
|
87
|
-
expDateText,
|
|
88
|
-
startDateText,
|
|
89
|
-
endDateText,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
// If trip date not after eff_date or trip date not before exp_date, no need to proceed further
|
|
93
|
-
if (!(date >= effDate && date <= expDate)) return false;
|
|
94
|
-
|
|
95
|
-
if (endDate < startDate) {
|
|
96
|
-
// Some rates are effective from end of one year to start of next
|
|
97
|
-
// e.g. 09/01 to 04/30
|
|
98
|
-
// Previous step set start_date, end_date to the trip date year
|
|
99
|
-
// e.g. trip date 09/02/22, start_date 09/01/22, 04/30/22.
|
|
100
|
-
// This step checks two variations, one where start_date changed, another where end_date changed, and returns true if trip date falls within either one
|
|
101
|
-
// e.g. check against both 09/01/22 to 04/30/23, and 09/01/21 to 04/30/22
|
|
102
|
-
|
|
103
|
-
// date 09/02/22, start 09/01/22, end 04/30/23
|
|
104
|
-
endDate.setUTCFullYear(+year + 1);
|
|
105
|
-
if (date >= startDate && date <= endDate) return true;
|
|
106
|
-
|
|
107
|
-
// date 09/02/22, start 09/01/21, end 04/30/22
|
|
108
|
-
startDate.setUTCFullYear(+year - 1);
|
|
109
|
-
endDate.setUTCFullYear(+year);
|
|
110
|
-
if (date >= startDate && date <= endDate) return true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (date >= startDate && date <= endDate) return true;
|
|
114
|
-
|
|
115
|
-
return false;
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Logging to check how rates are being parsed
|
|
120
|
-
// console.log(`______Date: ${date.toISOString().slice(0, 10)}`);
|
|
121
|
-
// console.log(`start_Date: ${startDate.toISOString().slice(0, 10)}`);
|
|
122
|
-
// console.log(`end___Date: ${endDate.toISOString().slice(0, 10)}`);
|
|
123
|
-
// console.log(`Checking if date >= start_Date && date <= end_Date`);
|
|
124
|
-
// console.log(date >= startDate && date <= endDate);
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type {
|
|
3
|
-
StateExpenseItemInclRates,
|
|
4
|
-
StateExpenseItemValid,
|
|
5
|
-
} from '../../types/expenses';
|
|
6
|
-
import type { DateRaw } from '../../types/dates';
|
|
7
|
-
|
|
8
|
-
// Utils
|
|
9
|
-
import { getYYYY, getYY, getMM } from '../dates';
|
|
10
|
-
import { OCTOBER } from '../config';
|
|
11
|
-
|
|
12
|
-
const getGSAFiscalYear = (date: DateRaw) => {
|
|
13
|
-
return +getMM(date) < OCTOBER ? +getYYYY(date) : +getYYYY(date) + 1;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const getDODSourceDate = (date: DateRaw) => {
|
|
17
|
-
const todayDate = new Date();
|
|
18
|
-
const todayMonth = getMM(todayDate.toISOString());
|
|
19
|
-
const todayYear = getYYYY(todayDate.toISOString());
|
|
20
|
-
|
|
21
|
-
const tripMonth = getMM(date);
|
|
22
|
-
const tripYear = getYYYY(date);
|
|
23
|
-
|
|
24
|
-
const sourceYear =
|
|
25
|
-
+tripYear > +todayYear ? getYY(todayDate.toISOString()) : getYY(date);
|
|
26
|
-
const sourceMonth =
|
|
27
|
-
+tripYear > +todayYear ? todayMonth
|
|
28
|
-
: +tripMonth > +todayMonth ? todayMonth
|
|
29
|
-
: tripMonth;
|
|
30
|
-
|
|
31
|
-
return `${sourceMonth}-01-${sourceYear}`;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const returnValidStateExpense = (
|
|
35
|
-
expense: StateExpenseItemInclRates,
|
|
36
|
-
): StateExpenseItemValid => {
|
|
37
|
-
const { date, country, city } = expense;
|
|
38
|
-
let source = '';
|
|
39
|
-
if (expense.country.length === 2)
|
|
40
|
-
// Domestic rates are state abbr like 'NY' with length 2
|
|
41
|
-
source = `https://www.gsa.gov/travel/plan-book/per-diem-rates/per-diem-rates-results?action=perdiems_report&fiscal_year=${getGSAFiscalYear(date)}&state=${country}&city=${city}`;
|
|
42
|
-
else
|
|
43
|
-
source = `https://www.defensetravel.dod.mil/neorates/report/index.php?report=oconus&country=${country}&date=${getDODSourceDate(date)}&military=YES`;
|
|
44
|
-
|
|
45
|
-
return { ...expense, source };
|
|
46
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { YYYY } from '../../types/dates';
|
|
3
|
-
|
|
4
|
-
// Utils
|
|
5
|
-
import { memoize } from './memoize';
|
|
6
|
-
|
|
7
|
-
const PROXY_URL = import.meta.env.VITE_PROXY_URL;
|
|
8
|
-
const PROXY_KEY = import.meta.env.VITE_PROXY_KEY;
|
|
9
|
-
const GSA_API_URL = 'https://api.gsa.gov/travel/perdiem/v2/rates/conus';
|
|
10
|
-
|
|
11
|
-
const fetchJsonGSA = async <T>(
|
|
12
|
-
year: YYYY,
|
|
13
|
-
type: 'lodging' | 'mie',
|
|
14
|
-
): Promise<T[]> => {
|
|
15
|
-
const url = `${PROXY_URL}?url=${GSA_API_URL}/${type}/${year}`;
|
|
16
|
-
const res = await fetch(url, {
|
|
17
|
-
method: 'GET',
|
|
18
|
-
headers: {
|
|
19
|
-
'x-perdiem-key': PROXY_KEY,
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
if (!res.ok) throw new Error(`Failed to get API results from ${url}.`);
|
|
23
|
-
const result = await res.json();
|
|
24
|
-
return result;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const fetchJsonGSAMemo = memoize(fetchJsonGSA);
|
|
28
|
-
|
|
29
|
-
export { fetchJsonGSAMemo as fetchJsonGSA };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { YYYY } from '../../types/dates';
|
|
3
|
-
|
|
4
|
-
// Utils
|
|
5
|
-
import JSZip from 'jszip';
|
|
6
|
-
import { memoize } from './memoize';
|
|
7
|
-
import { getYY } from '../dates';
|
|
8
|
-
|
|
9
|
-
const PROXY_URL = import.meta.env.VITE_PROXY_URL;
|
|
10
|
-
const PROXY_KEY = import.meta.env.VITE_PROXY_KEY;
|
|
11
|
-
const DOD_XML_URL = `https://www.travel.dod.mil/Portals/119/Documents/Allowances/Per_Diem/OCONUS/REL/OCONUS-REL-API_YEAR_FROM_MODEL.zip`;
|
|
12
|
-
|
|
13
|
-
// Download relational zip file from US DOD (https://www.travel.dod.mil/Travel-Transportation-Rates/Per-Diem/Per-Diem-Rate-Lookup/) -> unzip the XML file with JSZip
|
|
14
|
-
const fetchXmlDOD = async (year: YYYY) => {
|
|
15
|
-
const url = `${PROXY_URL}?url=${DOD_XML_URL.replace('API_YEAR_FROM_MODEL', year)}`;
|
|
16
|
-
const res = await fetch(url, {
|
|
17
|
-
headers: {
|
|
18
|
-
'x-perdiem-key': PROXY_KEY,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
if (!res.ok) throw new Error(`Failed to download file from ${url}`);
|
|
22
|
-
|
|
23
|
-
const resFile = await res.arrayBuffer();
|
|
24
|
-
if (!resFile)
|
|
25
|
-
throw new Error(`Failed to write file from ${url} to arrayBuffer`);
|
|
26
|
-
|
|
27
|
-
const zip = new JSZip();
|
|
28
|
-
await zip.loadAsync(resFile);
|
|
29
|
-
const filename = `ocallhist-${getYY(year, 'YYYY')}.xml`;
|
|
30
|
-
const data = await zip.file(filename)?.async('string');
|
|
31
|
-
if (!data)
|
|
32
|
-
throw new Error(`Failed to extract XML file from zip from ${url}`);
|
|
33
|
-
return data;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const fetchXmlDODmemo = memoize(fetchXmlDOD);
|
|
37
|
-
|
|
38
|
-
export { fetchXmlDODmemo as fetchXmlDOD };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */
|
|
3
|
-
export function memoize<
|
|
4
|
-
F extends <T extends any>(...args: [T, ...any[]]) => ReturnType<F>,
|
|
5
|
-
>(fn: F): F;
|
|
6
|
-
export function memoize<F extends (...args: any[]) => ReturnType<F>>(fn: F): F;
|
|
7
|
-
export function memoize<
|
|
8
|
-
F extends <T extends any>(...args: [T, ...any[]]) => ReturnType<F>,
|
|
9
|
-
>(fn: F): F {
|
|
10
|
-
let lastArgs: unknown[] = [];
|
|
11
|
-
let lastResult: ReturnType<F>;
|
|
12
|
-
let calledOnce = false;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Compare arguments for shallow identity equality.
|
|
16
|
-
*/
|
|
17
|
-
const argsEqual = (a: unknown[], b: unknown[]): boolean => {
|
|
18
|
-
if (a.length !== b.length) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
for (let i = 0; i < a.length; i++) {
|
|
23
|
-
if (a[i] !== b[i]) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return true;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Wrap function in memoized function.
|
|
32
|
-
*/
|
|
33
|
-
const memoized = <U extends any>(
|
|
34
|
-
...newArgs: [U, ...any[]]
|
|
35
|
-
): ReturnType<F> => {
|
|
36
|
-
if (!calledOnce || !argsEqual(lastArgs, newArgs)) {
|
|
37
|
-
const newResult = fn(...newArgs);
|
|
38
|
-
lastResult = newResult;
|
|
39
|
-
lastArgs = newArgs;
|
|
40
|
-
calledOnce = true;
|
|
41
|
-
}
|
|
42
|
-
return lastResult;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
return memoized as F;
|
|
46
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { memoize } from './memoize';
|
|
2
|
-
|
|
3
|
-
// Turn XML into readable DOM with DOMParser and use XPathEvaluator to do the equivalent of querySelectoring text inside an element
|
|
4
|
-
// https://developer.mozilla.org/en-US/docs/Web/XML/XPath/Guides/Snippets
|
|
5
|
-
const parseXml = (data: string, aExpr: string) => {
|
|
6
|
-
const parser = new DOMParser();
|
|
7
|
-
const aNode = parser.parseFromString(data, 'application/xml');
|
|
8
|
-
const xpe = new XPathEvaluator();
|
|
9
|
-
const nsResolver = aNode.documentElement;
|
|
10
|
-
const result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
|
|
11
|
-
const found = [];
|
|
12
|
-
let res;
|
|
13
|
-
while ((res = result.iterateNext())) found.push(res);
|
|
14
|
-
return found;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const parseXmlMemo = memoize(parseXml);
|
|
18
|
-
|
|
19
|
-
export { parseXmlMemo as parseXml };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
import type { Location } from '../../types/locations';
|
|
3
|
-
import type { RateLodging } from '../../types/expenses';
|
|
4
|
-
import type { DateRaw } from '../../types/dates';
|
|
5
|
-
|
|
6
|
-
// Utils
|
|
7
|
-
import { fetchJsonGSA } from '../fetch';
|
|
8
|
-
import { getValidAPIYear } from '../dates';
|
|
9
|
-
import { sortLocations } from './sortLocations';
|
|
10
|
-
import { keepUniqueLocations } from './keepUniqueLocations';
|
|
11
|
-
|
|
12
|
-
const getCities = (data: RateLodging[], country: string): RateLodging[] => {
|
|
13
|
-
return data.filter(
|
|
14
|
-
rate =>
|
|
15
|
-
rate.State !== null &&
|
|
16
|
-
rate.State !== '' &&
|
|
17
|
-
rate.State.toLowerCase() === country.toLowerCase(),
|
|
18
|
-
);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const createLocations = (data: RateLodging[], country: string): Location[] => {
|
|
22
|
-
return data.map(rate => {
|
|
23
|
-
return rate.County === null ?
|
|
24
|
-
{ city: rate.City, country: country, label: rate.City }
|
|
25
|
-
: {
|
|
26
|
-
city: rate.City,
|
|
27
|
-
country: country,
|
|
28
|
-
label: `${rate.City} / ${rate.County}`,
|
|
29
|
-
};
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const getCitiesDomestic = async (
|
|
34
|
-
date: DateRaw,
|
|
35
|
-
country: string,
|
|
36
|
-
): Promise<Location[]> => {
|
|
37
|
-
try {
|
|
38
|
-
return await fetchJsonGSA<RateLodging>(getValidAPIYear(date), 'lodging')
|
|
39
|
-
.then(data => getCities(data, country))
|
|
40
|
-
.then(data => createLocations(data, country))
|
|
41
|
-
.then(data => sortLocations(data, 'city', 'domesticCities'))
|
|
42
|
-
.then(data => keepUniqueLocations(data, 'city'));
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`Failed to get domestic cities for ${date} - ${country} - ${error}`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
};
|