@per-diem-calculator/vanilla 1.0.0

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.
Files changed (103) hide show
  1. package/.prettierrc +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1 -0
  4. package/eslint.config.js +29 -0
  5. package/index.html +11 -0
  6. package/package.json +49 -0
  7. package/public/output.css +2503 -0
  8. package/src/css/_styles.css +8 -0
  9. package/src/css/colors.css +45 -0
  10. package/src/css/fonts.css +9 -0
  11. package/src/css/rows/_heights.css +6 -0
  12. package/src/css/rows/_index.css +15 -0
  13. package/src/css/rows/add.css +18 -0
  14. package/src/css/rows/animate-btns.css +18 -0
  15. package/src/css/rows/animate-row-close.css +18 -0
  16. package/src/css/rows/animate-row-open.css +14 -0
  17. package/src/css/rows/animate-row-other.css +5 -0
  18. package/src/css/rows/btn-add-row.css +41 -0
  19. package/src/css/rows/btn-delete.css +22 -0
  20. package/src/css/rows/btn-expenses-calculate.css +22 -0
  21. package/src/css/rows/btn-expenses-category.css +22 -0
  22. package/src/css/rows/delete.css +10 -0
  23. package/src/css/rows/details.css +22 -0
  24. package/src/css/rows/expense.css +18 -0
  25. package/src/css/rows/location.css +34 -0
  26. package/src/css/rows/summary.css +22 -0
  27. package/src/css/tom-select/defaults.css +530 -0
  28. package/src/css/tom-select/overrides.css +55 -0
  29. package/src/css/tw-shadow-props.css +50 -0
  30. package/src/index.ts +1 -0
  31. package/src/ts/components/Button/Button.ts +50 -0
  32. package/src/ts/components/Button/template.html +34 -0
  33. package/src/ts/components/ExpenseRow/ExpenseRow.ts +397 -0
  34. package/src/ts/components/ExpenseRow/template.html +260 -0
  35. package/src/ts/components/Label/Label.ts +45 -0
  36. package/src/ts/components/Label/template.html +1 -0
  37. package/src/ts/components/LocationCategory/LocationCategory.ts +226 -0
  38. package/src/ts/components/LocationCategory/template.html +520 -0
  39. package/src/ts/components/LocationDate/LocationDate.ts +366 -0
  40. package/src/ts/components/LocationDate/template.html +27 -0
  41. package/src/ts/components/LocationSelect/LocationSelect.ts +299 -0
  42. package/src/ts/components/LocationSelect/template.html +45 -0
  43. package/src/ts/components/index.ts +6 -0
  44. package/src/ts/controller.ts +193 -0
  45. package/src/ts/model.ts +163 -0
  46. package/src/ts/types/config.ts +22 -0
  47. package/src/ts/types/dates.ts +82 -0
  48. package/src/ts/types/expenses.ts +73 -0
  49. package/src/ts/types/locations.ts +25 -0
  50. package/src/ts/utils/config/configDefault.ts +13 -0
  51. package/src/ts/utils/config/index.ts +12 -0
  52. package/src/ts/utils/config/numbers.ts +24 -0
  53. package/src/ts/utils/config/sanitizeConfig.ts +39 -0
  54. package/src/ts/utils/dates/INPUT_DATE_MINMAX.ts +5 -0
  55. package/src/ts/utils/dates/YEAR_REGEX.ts +4 -0
  56. package/src/ts/utils/dates/getDateSlice.ts +54 -0
  57. package/src/ts/utils/dates/getValidAPIYear.ts +17 -0
  58. package/src/ts/utils/dates/index.ts +19 -0
  59. package/src/ts/utils/dates/isDateRaw.ts +90 -0
  60. package/src/ts/utils/dates/isShortMonth.ts +24 -0
  61. package/src/ts/utils/dates/isYYYY.ts +10 -0
  62. package/src/ts/utils/dates/offsetDateString.ts +17 -0
  63. package/src/ts/utils/expenses/INTL_MIE_RATES.ts +2125 -0
  64. package/src/ts/utils/expenses/createExpenseObjs.ts +35 -0
  65. package/src/ts/utils/expenses/getLodgingRateDomestic.ts +73 -0
  66. package/src/ts/utils/expenses/getLodgingRateIntl.ts +119 -0
  67. package/src/ts/utils/expenses/getMieRates.ts +84 -0
  68. package/src/ts/utils/expenses/index.ts +5 -0
  69. package/src/ts/utils/expenses/parseIntlLodgingRates.ts +124 -0
  70. package/src/ts/utils/expenses/returnValidStateExpense.ts +46 -0
  71. package/src/ts/utils/fetch/fetchJsonGSA.ts +29 -0
  72. package/src/ts/utils/fetch/fetchXmlDOD.ts +38 -0
  73. package/src/ts/utils/fetch/index.ts +3 -0
  74. package/src/ts/utils/fetch/memoize.ts +46 -0
  75. package/src/ts/utils/fetch/parseXml.ts +19 -0
  76. package/src/ts/utils/locations/getCitiesDomestic.ts +48 -0
  77. package/src/ts/utils/locations/getCitiesIntl.ts +63 -0
  78. package/src/ts/utils/locations/getCountriesDomestic.ts +237 -0
  79. package/src/ts/utils/locations/getCountriesIntl.ts +34 -0
  80. package/src/ts/utils/locations/index.ts +6 -0
  81. package/src/ts/utils/locations/keepUniqueLocations.ts +12 -0
  82. package/src/ts/utils/locations/locationKeys.ts +10 -0
  83. package/src/ts/utils/locations/returnValidStateLocation.ts +13 -0
  84. package/src/ts/utils/locations/sortLocations.ts +19 -0
  85. package/src/ts/utils/misc/USD.ts +4 -0
  86. package/src/ts/utils/misc/debounce.ts +22 -0
  87. package/src/ts/utils/misc/handlePointerDown.ts +3 -0
  88. package/src/ts/utils/misc/handlePointerUp.ts +22 -0
  89. package/src/ts/utils/misc/inPrimitiveType.ts +4 -0
  90. package/src/ts/utils/misc/index.ts +6 -0
  91. package/src/ts/utils/misc/wait.ts +4 -0
  92. package/src/ts/utils/styles/applyStyles.ts +19 -0
  93. package/src/ts/utils/styles/highlightInput.ts +15 -0
  94. package/src/ts/utils/styles/index.ts +3 -0
  95. package/src/ts/utils/styles/removeStyles.ts +14 -0
  96. package/src/ts/views/Expense/Expense.ts +465 -0
  97. package/src/ts/views/Expense/template.html +176 -0
  98. package/src/ts/views/Location/Location.ts +763 -0
  99. package/src/ts/views/Location/template-row.html +146 -0
  100. package/src/ts/views/Location/template.html +130 -0
  101. package/src/ts/views/index.ts +2 -0
  102. package/tsconfig.json +27 -0
  103. package/vite.config.ts +12 -0
@@ -0,0 +1,45 @@
1
+ <div
2
+ class="group flex items-center justify-between border-b border-b-neutral-100
3
+ px-3"
4
+ >
5
+ <pdc-label styled="true"></pdc-label>
6
+ <select hidden></select>
7
+ <button
8
+ tabindex="0"
9
+ type="button"
10
+ id=""
11
+ class="group-[.active]:focus-visible:border-primary-800 absolute
12
+ right-[24px] rounded-full border-3 border-transparent p-1
13
+ transition-colors focus:outline-none
14
+ group-[.active]:focus-visible:[&_svg]:-rotate-180"
15
+ >
16
+ <svg
17
+ class="group-[.active]:group-hover:stroke-primary-800
18
+ group-[.active]:group-focus-visible:stroke-primary-800 size-4
19
+ shrink-0 rotate-0 fill-none stroke-neutral-400
20
+ transition-[transform_stroke] duration-500
21
+ group-[.active]:group-hover:-rotate-180 sm:size-5"
22
+ xmlns="http://www.w3.org/2000/svg"
23
+ viewBox="0 0 24 24"
24
+ >
25
+ <path
26
+ stroke-linecap="round"
27
+ stroke-linejoin="round"
28
+ stroke-width="2"
29
+ d="M19 9l-7 7-7-7"
30
+ />
31
+ </svg>
32
+ </button>
33
+ <!-- Loading indicator-->
34
+ <div
35
+ id="loading-spinner"
36
+ class="absolute z-0 flex w-full items-center justify-center bg-inherit
37
+ opacity-0 transition-opacity duration-700 [.active]:z-50
38
+ [.active]:opacity-100"
39
+ >
40
+ <div
41
+ class="border-t-primary-600 border-primary-50 h-10 w-10 animate-spin
42
+ rounded-full border-4 border-t-4"
43
+ ></div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,6 @@
1
+ export { PdcExpenseRow } from './ExpenseRow/ExpenseRow';
2
+ export { PdcLocationCategory } from './LocationCategory/LocationCategory';
3
+ export { PdcLocationSelect } from './LocationSelect/LocationSelect';
4
+ export { PdcLocationDate } from './LocationDate/LocationDate';
5
+ export { PdcButton } from './Button/Button';
6
+ export { PdcLabel } from './Label/Label';
@@ -0,0 +1,193 @@
1
+ // Types
2
+ import type {
3
+ LocationKeys,
4
+ StateLocationItem,
5
+ AllViewLocationsValid,
6
+ } from './types/locations';
7
+ import { StateExpenseItemUpdate } from './types/expenses';
8
+ import type { Config } from './types/config';
9
+
10
+ // Utils
11
+ import { sanitizeConfig } from './utils/config';
12
+
13
+ // Model
14
+ import * as model from './model';
15
+
16
+ // Views
17
+ import { PdcLocationView } from './views';
18
+ import { PdcExpenseView } from './views';
19
+ customElements.define('pdc-expense-view', PdcExpenseView);
20
+ customElements.define('pdc-location-view', PdcLocationView);
21
+
22
+ export class Pdc {
23
+ /* SETUP
24
+ */
25
+ #container: Element;
26
+ #config: Config;
27
+ #viewLocation;
28
+ #viewExpense;
29
+ #styled;
30
+ #eventTarget;
31
+
32
+ constructor(container: Element, configUser: Partial<Config> | null = null) {
33
+ this.#container = container;
34
+ this.#config = sanitizeConfig(configUser);
35
+ this.#styled = this.#config.styled;
36
+
37
+ this.#viewLocation = new PdcLocationView(
38
+ this.#styled,
39
+ this.#config.location,
40
+ );
41
+ this.#viewExpense = new PdcExpenseView();
42
+
43
+ this.#viewLocation.controllerHandler(
44
+ this.#locationUpdated,
45
+ this.#locationDeleted,
46
+ this.#locationsValidate,
47
+ );
48
+ this.#eventTarget = new EventTarget();
49
+
50
+ this.#container.insertAdjacentElement('afterbegin', this.#viewLocation);
51
+ this.#container.insertAdjacentElement('beforeend', this.#viewExpense);
52
+ }
53
+
54
+ #dispatchEvent() {
55
+ const event = new CustomEvent('expenseUpdate', {
56
+ detail: {
57
+ data: model.returnExpenses(),
58
+ },
59
+ });
60
+ this.#eventTarget.dispatchEvent(event);
61
+ }
62
+
63
+ addEventListener(eventName: 'expenseUpdate', callback: EventListener) {
64
+ this.#eventTarget.addEventListener(eventName, callback);
65
+ }
66
+
67
+ removeEventListener(eventName: 'expenseUpdate', callback: EventListener) {
68
+ this.#eventTarget.removeEventListener(eventName, callback);
69
+ }
70
+
71
+ /* LOCATION
72
+ */
73
+ #locationDeleted = async (
74
+ updatedRows: StateLocationItem[],
75
+ ): Promise<void> => {
76
+ this.#viewExpense.renderEmtpy();
77
+ model.updateAllStateLocations(updatedRows);
78
+ const row = updatedRows[updatedRows.length - 1];
79
+ const { index, start, end, category, country, city } = row;
80
+ if (!city && country) {
81
+ this.#viewLocation.restrictRow(index, 'country');
82
+ await this.#createSelectOptions(row);
83
+ }
84
+ if (!country && category) {
85
+ this.#viewLocation.restrictRow(index, 'category');
86
+ await this.#createSelectOptions(row);
87
+ }
88
+ if (!end && start) this.#viewLocation.restrictRow(index, 'end');
89
+ if (start && !end) this.#viewLocation.restrictRow(index, 'start');
90
+ };
91
+
92
+ #locationUpdated = async (
93
+ row: StateLocationItem,
94
+ changedAttr: LocationKeys,
95
+ newValue: string | null,
96
+ ): Promise<void> => {
97
+ const { index, start, end, category, country, city } = row;
98
+ this.#viewExpense.renderEmtpy();
99
+ model.updateStateLocation(row);
100
+ let startDate, endDate;
101
+ switch (true) {
102
+ case !newValue && !!country && !!category && !!end && !!start:
103
+ this.#viewLocation.restrictRow(index, 'country');
104
+ await this.#createSelectOptions(row);
105
+ return;
106
+ case !newValue && !!category && !!end && !!start:
107
+ this.#viewLocation.restrictRow(index, 'category');
108
+ await this.#createSelectOptions(row);
109
+ return;
110
+ case !newValue && !!end && !!start:
111
+ this.#viewLocation.restrictRow(index, 'end');
112
+ return;
113
+ case !newValue:
114
+ this.#viewLocation.restrictRow(index, 'start');
115
+ return;
116
+ case (changedAttr === 'start' || changedAttr === 'end') && // To account for when rows are deleted and the only updates are to the start/end dates of prev/next rows
117
+ !!start &&
118
+ !!end &&
119
+ !!category &&
120
+ !!country &&
121
+ !!city:
122
+ startDate = new Date(start);
123
+ endDate = new Date(end);
124
+ if (startDate <= endDate) {
125
+ return;
126
+ } else {
127
+ this.#viewLocation.restrictRow(index, changedAttr);
128
+ }
129
+ return;
130
+ default:
131
+ this.#viewLocation.restrictRow(index, changedAttr);
132
+ if (changedAttr === 'category' || changedAttr === 'country') {
133
+ await this.#createSelectOptions(row);
134
+ }
135
+ return;
136
+ }
137
+ };
138
+
139
+ #createSelectOptions = async (row: StateLocationItem) => {
140
+ const { index } = row;
141
+ const locationCategory = row.country ? 'city' : 'country';
142
+ this.#viewLocation.showLoadingSpinner(index, true, locationCategory);
143
+ const list = await model.returnOptions(row);
144
+ this.#viewLocation.createOptions(index, list, locationCategory);
145
+ this.#viewLocation.showLoadingSpinner(index, false, locationCategory);
146
+ };
147
+
148
+ #locationsValidate = (viewValidator: AllViewLocationsValid): void => {
149
+ this.#viewExpense.renderEmtpy();
150
+ if (!viewValidator.valid) return;
151
+ if (!model.validateStateLocations()) return;
152
+ this.#createExpenses(viewValidator);
153
+ };
154
+
155
+ /* EXPENSE
156
+ */
157
+ async #createExpenses(viewValidator: AllViewLocationsValid) {
158
+ try {
159
+ this.#viewExpense.render(this.#styled, this.#config.expense);
160
+ this.#viewExpense.renderLoadingSpinner(true);
161
+ this.#viewExpense.controllerHandler(
162
+ this.#expenseUpdated,
163
+ this.#expenseTable,
164
+ );
165
+ const expenses = await model.generateExpenses(viewValidator);
166
+ await this.#viewExpense.addRows(
167
+ expenses,
168
+ viewValidator.expensesCategory,
169
+ );
170
+ this.#viewExpense.renderLoadingSpinner(false);
171
+ this.#dispatchEvent();
172
+ } catch (error) {
173
+ console.error(error);
174
+ }
175
+ }
176
+
177
+ #expenseUpdated = (row: StateExpenseItemUpdate): void => {
178
+ const { date, newRowMieTotal, totalMie, totalLodging } =
179
+ model.updateStateExpenseItem(row);
180
+ this.#viewExpense.updateRowMie(
181
+ date,
182
+ newRowMieTotal,
183
+ totalMie,
184
+ totalLodging,
185
+ );
186
+ this.#viewExpense.emptyExpenseTable();
187
+ this.#dispatchEvent();
188
+ };
189
+
190
+ #expenseTable = (): void => {
191
+ this.#viewExpense.createExpenseTable(model.returnExpenses());
192
+ };
193
+ }
@@ -0,0 +1,163 @@
1
+ // Types
2
+ import type {
3
+ AllViewLocationsValid,
4
+ Location,
5
+ StateLocationItem,
6
+ } from './types/locations';
7
+ import type {
8
+ StateExpenseItemUpdate,
9
+ StateExpenseItemValid,
10
+ } from './types/expenses';
11
+ import type { State } from './types/config';
12
+
13
+ // Utils
14
+ import {
15
+ getCountriesDomestic,
16
+ getCountriesIntl,
17
+ getCitiesDomestic,
18
+ getCitiesIntl,
19
+ returnValidStateLocation,
20
+ } from './utils/locations';
21
+ import { createExpenseObjs, getMieRates } from './utils/expenses';
22
+ import {
23
+ getLodgingRateDomestic,
24
+ getLodgingRateIntl,
25
+ returnValidStateExpense,
26
+ } from './utils/expenses';
27
+ import { US_STATE_LENGTH } from './utils/config';
28
+
29
+ const state: State = {
30
+ locations: [],
31
+ locationsValid: [],
32
+ expenses: [],
33
+ expensesValid: [],
34
+ };
35
+
36
+ export const updateAllStateLocations = (
37
+ updatedRows: StateLocationItem[],
38
+ ): void => {
39
+ state.locations = updatedRows;
40
+ // logStateLocation();
41
+ };
42
+
43
+ export const updateStateLocation = (
44
+ updatedLocation: StateLocationItem,
45
+ ): void => {
46
+ state.locations[updatedLocation.index] = updatedLocation;
47
+ // logStateLocation();
48
+ };
49
+
50
+ // Function to log every time state is updated during development
51
+ // const logStateLocation = () => {
52
+ // console.table(state.locations, [
53
+ // 'start',
54
+ // 'end',
55
+ // 'category',
56
+ // 'country',
57
+ // 'city',
58
+ // ]);
59
+ // }
60
+
61
+ export const returnOptions = async (
62
+ row: StateLocationItem,
63
+ ): Promise<Location[]> => {
64
+ const { index, end, category, country } = row;
65
+ const listType = country ? 'city' : 'country';
66
+ if (!end) throw new Error(`Location ${index} - no end date`);
67
+ const list =
68
+ country ?
69
+ country.length === US_STATE_LENGTH ?
70
+ // Domestic countries are all state abbreviations with length of 2 (e.g. 'NY')
71
+ await getCitiesDomestic(end, country)
72
+ : await getCitiesIntl(end, country)
73
+ : category === 'domestic' ? await getCountriesDomestic(end)
74
+ : await getCountriesIntl(end);
75
+ if (list.length === 0)
76
+ throw new Error(`Failed to get ${listType} list for ${end}`);
77
+ return list;
78
+ };
79
+
80
+ export const validateStateLocations = (): boolean => {
81
+ state.locationsValid.length = 0;
82
+ state.locations.forEach(location => {
83
+ const validLocation = returnValidStateLocation(location);
84
+ if (validLocation) state.locationsValid.push(validLocation);
85
+ });
86
+ return state.locations.length === state.locationsValid.length;
87
+ };
88
+
89
+ export const generateExpenses = async (
90
+ viewValidator: AllViewLocationsValid,
91
+ ) => {
92
+ const { expensesCategory } = viewValidator;
93
+ state.expenses.length = 0;
94
+ state.locationsValid.map(location =>
95
+ state.expenses.push(...createExpenseObjs(location)),
96
+ );
97
+ const expensesWithSomeRates = await Promise.all(
98
+ state.expenses.map(expense => {
99
+ return expense.country.length === US_STATE_LENGTH ?
100
+ getLodgingRateDomestic(expense)
101
+ : getLodgingRateIntl(expense);
102
+ }),
103
+ );
104
+ const expensesWithAllRates = await Promise.all(
105
+ expensesWithSomeRates.map(expense => {
106
+ return getMieRates(expensesCategory, expense);
107
+ }),
108
+ );
109
+ state.expensesValid = expensesWithAllRates.map(expense => {
110
+ return returnValidStateExpense(expense);
111
+ });
112
+ return state.expensesValid;
113
+ };
114
+
115
+ export const updateStateExpenseItem = (update: StateExpenseItemUpdate) => {
116
+ const { date, lodgingAmount, ...deductions } = update;
117
+ const item = state.expensesValid.find(expense => expense.date === date);
118
+ if (!item) throw new Error(`Failed to find expense matching the update.`);
119
+ item.lodgingAmount = lodgingAmount;
120
+ item.deductions = {
121
+ ...item.deductions,
122
+ ...deductions,
123
+ };
124
+ if (item.mieAmount > 0) updateExpenseMie(item, update);
125
+ item.totalAmount = item.lodgingAmount + item.mieAmount;
126
+ const { totalMie, totalLodging } = getExpenseSubtotals();
127
+ return {
128
+ date: item.date,
129
+ newRowMieTotal: item.mieAmount,
130
+ totalMie,
131
+ totalLodging,
132
+ };
133
+ };
134
+
135
+ const updateExpenseMie = (
136
+ item: StateExpenseItemValid,
137
+ update: StateExpenseItemUpdate,
138
+ ) => {
139
+ let total =
140
+ item.deductions.FirstLastDay ?
141
+ item.rates.maxMieFirstLast
142
+ : item.rates.maxMie;
143
+ if (update.breakfastProvided) total -= item.rates.deductionBreakfast;
144
+ if (update.lunchProvided) total -= item.rates.deductionLunch;
145
+ if (update.dinnerProvided) total -= item.rates.deductionDinner;
146
+ if (total < item.rates.maxIncidental) total = item.rates.maxIncidental;
147
+ item.mieAmount = total;
148
+ };
149
+
150
+ const getExpenseSubtotals = () => {
151
+ return state.expensesValid.reduce(
152
+ (result, item) => {
153
+ result.totalMie += item.mieAmount;
154
+ result.totalLodging += item.lodgingAmount;
155
+ return result;
156
+ },
157
+ { totalMie: 0, totalLodging: 0 },
158
+ );
159
+ };
160
+
161
+ export const returnExpenses = () => {
162
+ return state.expensesValid;
163
+ };
@@ -0,0 +1,22 @@
1
+ import type { StateLocationItem, StateLocationItemValid } from './locations';
2
+ import type { StateExpenseItem, StateExpenseItemValid } from './expenses';
3
+
4
+ export interface Config {
5
+ styled: boolean;
6
+ location: ConfigSectionText;
7
+ expense: ConfigSectionText;
8
+ }
9
+
10
+ export interface ConfigSectionText {
11
+ heading?: string;
12
+ headingPrint?: string;
13
+ body?: string;
14
+ bodyPrint?: string;
15
+ }
16
+
17
+ export interface State {
18
+ locations: StateLocationItem[];
19
+ locationsValid: StateLocationItemValid[];
20
+ expenses: StateExpenseItem[];
21
+ expensesValid: StateExpenseItemValid[];
22
+ }
@@ -0,0 +1,82 @@
1
+ export type YYYY =
2
+ | '2021'
3
+ | '2022'
4
+ | '2023'
5
+ | '2024'
6
+ | '2025'
7
+ | '2026'
8
+ | '2027'
9
+ | '2028'
10
+ | '2029'
11
+ | '2030'
12
+ | '2031'
13
+ | '2032'
14
+ | '2033'
15
+ | '2034'
16
+ | '2035'
17
+ | '2036'
18
+ | '2037'
19
+ | '2038'
20
+ | '2039'
21
+ | '2040';
22
+
23
+ type MM =
24
+ | '01'
25
+ | '02'
26
+ | '03'
27
+ | '04'
28
+ | '05'
29
+ | '06'
30
+ | '07'
31
+ | '08'
32
+ | '09'
33
+ | '10'
34
+ | '11'
35
+ | '12';
36
+ type DD =
37
+ | '01'
38
+ | '02'
39
+ | '03'
40
+ | '04'
41
+ | '05'
42
+ | '06'
43
+ | '07'
44
+ | '08'
45
+ | '09'
46
+ | '10'
47
+ | '11'
48
+ | '12'
49
+ | '13'
50
+ | '14'
51
+ | '15'
52
+ | '16'
53
+ | '17'
54
+ | '18'
55
+ | '19'
56
+ | '20'
57
+ | '21'
58
+ | '22'
59
+ | '23'
60
+ | '24'
61
+ | '25'
62
+ | '26'
63
+ | '27'
64
+ | '28'
65
+ | '29'
66
+ | '30'
67
+ | '31';
68
+
69
+ export type DateRaw = `${YYYY}-${MM}-${DD}`;
70
+ export type ShortMonth =
71
+ | 'Jan'
72
+ | 'Feb'
73
+ | 'Mar'
74
+ | 'Apr'
75
+ | 'May'
76
+ | 'Jun'
77
+ | 'Jul'
78
+ | 'Aug'
79
+ | 'Sep'
80
+ | 'Oct'
81
+ | 'Nov'
82
+ | 'Dec';
@@ -0,0 +1,73 @@
1
+ import type { DateRaw } from './dates';
2
+
3
+ // Format of lodging rate data coming from GSA
4
+ export interface RateLodging {
5
+ Jan: string;
6
+ Feb: string;
7
+ Mar: string;
8
+ Apr: string;
9
+ May: string;
10
+ Jun: string;
11
+ Jul: string;
12
+ Aug: string;
13
+ Sep: string;
14
+ Oct: string;
15
+ Nov: string;
16
+ Meals: number;
17
+ City: string;
18
+ State: string;
19
+ County: string | null;
20
+ DID: number;
21
+ Dec: string;
22
+ }
23
+
24
+ // Format of meals rate data coming from GSA
25
+ export interface RateMeals {
26
+ total: number;
27
+ breakfast: number;
28
+ lunch: number;
29
+ dinner: number;
30
+ incidental: number;
31
+ FirstLastDay: number;
32
+ max?: number;
33
+ }
34
+
35
+ interface ExpenseDeductions {
36
+ FirstLastDay: boolean;
37
+ breakfastProvided: boolean;
38
+ lunchProvided: boolean;
39
+ dinnerProvided: boolean;
40
+ }
41
+
42
+ export interface ExpenseRates {
43
+ maxLodging: number;
44
+ maxMie: number;
45
+ maxMieFirstLast?: number;
46
+ deductionBreakfast?: number;
47
+ deductionLunch?: number;
48
+ deductionDinner?: number;
49
+ maxIncidental?: number;
50
+ effDate: DateRaw;
51
+ }
52
+
53
+ export interface StateExpenseItem {
54
+ date: DateRaw;
55
+ country: string;
56
+ city: string;
57
+ deductions: ExpenseDeductions;
58
+ rates?: ExpenseRates;
59
+ }
60
+
61
+ export type StateExpenseItemInclRates = StateExpenseItem & {
62
+ rates: Required<ExpenseRates>;
63
+ lodgingAmount: number;
64
+ mieAmount: number;
65
+ totalAmount: number;
66
+ };
67
+
68
+ export type StateExpenseItemValid = Required<StateExpenseItemInclRates> & {
69
+ source: string;
70
+ };
71
+
72
+ export type StateExpenseItemUpdate = Omit<ExpenseDeductions, 'FirstLastDay'> &
73
+ Pick<StateExpenseItemValid, 'date' | 'lodgingAmount'>;
@@ -0,0 +1,25 @@
1
+ import { DateRaw } from './dates';
2
+
3
+ export interface Location {
4
+ city?: string;
5
+ country?: string;
6
+ label?: string;
7
+ category?: 'domestic' | 'intl';
8
+ }
9
+
10
+ export interface AllViewLocationsValid {
11
+ valid: boolean;
12
+ expensesCategory: 'mie' | 'lodging' | 'both';
13
+ }
14
+
15
+ export type StateLocationItem = Omit<Location, 'label'> & {
16
+ index: number;
17
+ start?: DateRaw;
18
+ end?: DateRaw;
19
+ };
20
+
21
+ export type LocationKeys = keyof Omit<StateLocationItem, 'index'>;
22
+
23
+ export type StateLocationItemValid = Required<
24
+ Omit<StateLocationItem, 'index' | 'category'>
25
+ >;
@@ -0,0 +1,13 @@
1
+ // Types
2
+ import type { Config } from '../../types/config';
3
+
4
+ export const configDefault: Config = {
5
+ styled: true,
6
+ location: {
7
+ heading: 'Trip Details',
8
+ },
9
+ expense: {
10
+ heading: 'Expenses',
11
+ body: 'Confirm lodging amount and meals provided for each day',
12
+ },
13
+ };
@@ -0,0 +1,12 @@
1
+ export { configDefault } from './configDefault';
2
+ export {
3
+ ROW_CLOSED_HEIGHT,
4
+ DEBOUNCE_TIME,
5
+ BTN_ANIMATE_MS,
6
+ ROW_ANIMATE_MS,
7
+ US_STATE_LENGTH,
8
+ MILLISECONDS_IN_DAY,
9
+ APPROX_DAYS_IN_6_MONTHS,
10
+ OCTOBER,
11
+ } from './numbers';
12
+ export { sanitizeConfig } from './sanitizeConfig';
@@ -0,0 +1,24 @@
1
+ export const ROW_CLOSED_HEIGHT = 96;
2
+ export const DEBOUNCE_TIME = 150;
3
+ export const BTN_ANIMATE_MS = 450;
4
+ export const ROW_ANIMATE_MS = 800;
5
+ export const US_STATE_LENGTH = 'NY'.length;
6
+ export const OCTOBER = 10;
7
+
8
+ const MILLISECONDS_IN_SECOND = 1000;
9
+ const SECONDS_IN_MINUTE = 60;
10
+ const MINUTES_IN_HOUR = 60;
11
+ const HOURS_IN_DAY = 24;
12
+ export const MILLISECONDS_IN_DAY =
13
+ MILLISECONDS_IN_SECOND * SECONDS_IN_MINUTE * MINUTES_IN_HOUR * HOURS_IN_DAY;
14
+
15
+ const AVERAGE_DAYS_IN_YEAR = 365.25;
16
+ const MONTHS_IN_YEAR = 12;
17
+ const MONTHS_IN_DURATION = 6;
18
+
19
+ // Calculate the average days in a month
20
+ const AVERAGE_DAYS_IN_MONTH = AVERAGE_DAYS_IN_YEAR / MONTHS_IN_YEAR;
21
+
22
+ // The constant for approximate days in 6 months
23
+ export const APPROX_DAYS_IN_6_MONTHS =
24
+ AVERAGE_DAYS_IN_MONTH * MONTHS_IN_DURATION;