@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.
- package/.prettierrc +17 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/eslint.config.js +29 -0
- package/index.html +11 -0
- package/package.json +49 -0
- package/public/output.css +2503 -0
- package/src/css/_styles.css +8 -0
- package/src/css/colors.css +45 -0
- package/src/css/fonts.css +9 -0
- package/src/css/rows/_heights.css +6 -0
- package/src/css/rows/_index.css +15 -0
- package/src/css/rows/add.css +18 -0
- package/src/css/rows/animate-btns.css +18 -0
- package/src/css/rows/animate-row-close.css +18 -0
- package/src/css/rows/animate-row-open.css +14 -0
- package/src/css/rows/animate-row-other.css +5 -0
- package/src/css/rows/btn-add-row.css +41 -0
- package/src/css/rows/btn-delete.css +22 -0
- package/src/css/rows/btn-expenses-calculate.css +22 -0
- package/src/css/rows/btn-expenses-category.css +22 -0
- package/src/css/rows/delete.css +10 -0
- package/src/css/rows/details.css +22 -0
- package/src/css/rows/expense.css +18 -0
- package/src/css/rows/location.css +34 -0
- package/src/css/rows/summary.css +22 -0
- package/src/css/tom-select/defaults.css +530 -0
- package/src/css/tom-select/overrides.css +55 -0
- package/src/css/tw-shadow-props.css +50 -0
- package/src/index.ts +1 -0
- package/src/ts/components/Button/Button.ts +50 -0
- package/src/ts/components/Button/template.html +34 -0
- package/src/ts/components/ExpenseRow/ExpenseRow.ts +397 -0
- package/src/ts/components/ExpenseRow/template.html +260 -0
- package/src/ts/components/Label/Label.ts +45 -0
- package/src/ts/components/Label/template.html +1 -0
- package/src/ts/components/LocationCategory/LocationCategory.ts +226 -0
- package/src/ts/components/LocationCategory/template.html +520 -0
- package/src/ts/components/LocationDate/LocationDate.ts +366 -0
- package/src/ts/components/LocationDate/template.html +27 -0
- package/src/ts/components/LocationSelect/LocationSelect.ts +299 -0
- package/src/ts/components/LocationSelect/template.html +45 -0
- package/src/ts/components/index.ts +6 -0
- package/src/ts/controller.ts +193 -0
- package/src/ts/model.ts +163 -0
- package/src/ts/types/config.ts +22 -0
- package/src/ts/types/dates.ts +82 -0
- package/src/ts/types/expenses.ts +73 -0
- package/src/ts/types/locations.ts +25 -0
- package/src/ts/utils/config/configDefault.ts +13 -0
- package/src/ts/utils/config/index.ts +12 -0
- package/src/ts/utils/config/numbers.ts +24 -0
- package/src/ts/utils/config/sanitizeConfig.ts +39 -0
- package/src/ts/utils/dates/INPUT_DATE_MINMAX.ts +5 -0
- package/src/ts/utils/dates/YEAR_REGEX.ts +4 -0
- package/src/ts/utils/dates/getDateSlice.ts +54 -0
- package/src/ts/utils/dates/getValidAPIYear.ts +17 -0
- package/src/ts/utils/dates/index.ts +19 -0
- package/src/ts/utils/dates/isDateRaw.ts +90 -0
- package/src/ts/utils/dates/isShortMonth.ts +24 -0
- package/src/ts/utils/dates/isYYYY.ts +10 -0
- package/src/ts/utils/dates/offsetDateString.ts +17 -0
- package/src/ts/utils/expenses/INTL_MIE_RATES.ts +2125 -0
- package/src/ts/utils/expenses/createExpenseObjs.ts +35 -0
- package/src/ts/utils/expenses/getLodgingRateDomestic.ts +73 -0
- package/src/ts/utils/expenses/getLodgingRateIntl.ts +119 -0
- package/src/ts/utils/expenses/getMieRates.ts +84 -0
- package/src/ts/utils/expenses/index.ts +5 -0
- package/src/ts/utils/expenses/parseIntlLodgingRates.ts +124 -0
- package/src/ts/utils/expenses/returnValidStateExpense.ts +46 -0
- package/src/ts/utils/fetch/fetchJsonGSA.ts +29 -0
- package/src/ts/utils/fetch/fetchXmlDOD.ts +38 -0
- package/src/ts/utils/fetch/index.ts +3 -0
- package/src/ts/utils/fetch/memoize.ts +46 -0
- package/src/ts/utils/fetch/parseXml.ts +19 -0
- package/src/ts/utils/locations/getCitiesDomestic.ts +48 -0
- package/src/ts/utils/locations/getCitiesIntl.ts +63 -0
- package/src/ts/utils/locations/getCountriesDomestic.ts +237 -0
- package/src/ts/utils/locations/getCountriesIntl.ts +34 -0
- package/src/ts/utils/locations/index.ts +6 -0
- package/src/ts/utils/locations/keepUniqueLocations.ts +12 -0
- package/src/ts/utils/locations/locationKeys.ts +10 -0
- package/src/ts/utils/locations/returnValidStateLocation.ts +13 -0
- package/src/ts/utils/locations/sortLocations.ts +19 -0
- package/src/ts/utils/misc/USD.ts +4 -0
- package/src/ts/utils/misc/debounce.ts +22 -0
- package/src/ts/utils/misc/handlePointerDown.ts +3 -0
- package/src/ts/utils/misc/handlePointerUp.ts +22 -0
- package/src/ts/utils/misc/inPrimitiveType.ts +4 -0
- package/src/ts/utils/misc/index.ts +6 -0
- package/src/ts/utils/misc/wait.ts +4 -0
- package/src/ts/utils/styles/applyStyles.ts +19 -0
- package/src/ts/utils/styles/highlightInput.ts +15 -0
- package/src/ts/utils/styles/index.ts +3 -0
- package/src/ts/utils/styles/removeStyles.ts +14 -0
- package/src/ts/views/Expense/Expense.ts +465 -0
- package/src/ts/views/Expense/template.html +176 -0
- package/src/ts/views/Location/Location.ts +763 -0
- package/src/ts/views/Location/template-row.html +146 -0
- package/src/ts/views/Location/template.html +130 -0
- package/src/ts/views/index.ts +2 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
import type { StateExpenseItemValid } from '../../types/expenses';
|
|
3
|
+
|
|
4
|
+
// Utils
|
|
5
|
+
import {
|
|
6
|
+
handlePointerDown,
|
|
7
|
+
handlePointerUp,
|
|
8
|
+
USD,
|
|
9
|
+
wait,
|
|
10
|
+
} from '../../utils/misc';
|
|
11
|
+
import {
|
|
12
|
+
applyStyles,
|
|
13
|
+
removeStyles,
|
|
14
|
+
highlightSuccess,
|
|
15
|
+
} from '../../utils/styles';
|
|
16
|
+
import { ROW_CLOSED_HEIGHT, ROW_ANIMATE_MS } from '../../utils/config';
|
|
17
|
+
import { getShortMonth, getDD, getYYYY, getMM } from '../../utils/dates';
|
|
18
|
+
|
|
19
|
+
// HTML/CSS
|
|
20
|
+
import templateHTML from './template.html?raw';
|
|
21
|
+
|
|
22
|
+
// Template for this Custom Element
|
|
23
|
+
const template = document.createElement('template');
|
|
24
|
+
|
|
25
|
+
// Custom Element
|
|
26
|
+
export class PdcExpenseRow extends HTMLElement {
|
|
27
|
+
/* SETUP
|
|
28
|
+
*/
|
|
29
|
+
#expense: StateExpenseItemValid;
|
|
30
|
+
#expensesCategory;
|
|
31
|
+
#styled: boolean;
|
|
32
|
+
#maxLodging: number;
|
|
33
|
+
#maxMie: number;
|
|
34
|
+
#lodgingAmount: number;
|
|
35
|
+
#mieAmount: number;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
expense: StateExpenseItemValid,
|
|
39
|
+
styled: boolean,
|
|
40
|
+
expensesCategory: 'mie' | 'lodging' | 'both',
|
|
41
|
+
) {
|
|
42
|
+
super();
|
|
43
|
+
this.attachShadow({ mode: 'open' });
|
|
44
|
+
|
|
45
|
+
this.#expense = expense;
|
|
46
|
+
this.#styled = styled;
|
|
47
|
+
this.#expensesCategory = expensesCategory;
|
|
48
|
+
|
|
49
|
+
const { deductions, rates, lodgingAmount, mieAmount } = this.#expense;
|
|
50
|
+
this.#maxLodging = rates.maxLodging;
|
|
51
|
+
this.#maxMie =
|
|
52
|
+
deductions.FirstLastDay ? rates.maxMieFirstLast : rates.maxMie;
|
|
53
|
+
this.#lodgingAmount = lodgingAmount;
|
|
54
|
+
this.#mieAmount = mieAmount;
|
|
55
|
+
|
|
56
|
+
this.render(styled);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render(styled: boolean) {
|
|
60
|
+
this.#shadowRoot.innerHTML = '';
|
|
61
|
+
if (styled) {
|
|
62
|
+
template.innerHTML = templateHTML;
|
|
63
|
+
applyStyles(this.#shadowRoot);
|
|
64
|
+
} else template.innerHTML = removeStyles(templateHTML);
|
|
65
|
+
this.#shadowRoot.appendChild(template.content.cloneNode(true));
|
|
66
|
+
|
|
67
|
+
// Update text elements w/ values
|
|
68
|
+
this.#setRowDateText();
|
|
69
|
+
this.#setRowDetailsText();
|
|
70
|
+
|
|
71
|
+
// Update custom element's attribute values
|
|
72
|
+
this.setAttribute('date', this.#expense.date);
|
|
73
|
+
this.#updateLodgingAmount(this.#lodgingAmount.toString());
|
|
74
|
+
this.updateMieAmount(this.#mieAmount);
|
|
75
|
+
this.#updateTotalAmount();
|
|
76
|
+
|
|
77
|
+
this.#disableUnusedRowEls();
|
|
78
|
+
this.#addEventListeners();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#setRowDateText() {
|
|
82
|
+
const monthEl = this.#shadowRoot.querySelector('#month');
|
|
83
|
+
const dayEl = this.#shadowRoot.querySelector('#day');
|
|
84
|
+
const yearEl = this.#shadowRoot.querySelector('#year');
|
|
85
|
+
const date = new Date(this.#expense.date);
|
|
86
|
+
if (!(monthEl && dayEl && yearEl))
|
|
87
|
+
throw new Error(`Failed to render row's date elements.`);
|
|
88
|
+
monthEl.textContent = getShortMonth(date.toUTCString());
|
|
89
|
+
dayEl.textContent = getDD(this.#expense.date);
|
|
90
|
+
yearEl.textContent = getYYYY(this.#expense.date);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#setRowDetailsText() {
|
|
94
|
+
const locationEl = this.#shadowRoot.querySelector('#location');
|
|
95
|
+
const lodgingRateEl = this.#shadowRoot.querySelector('#lodging-rate');
|
|
96
|
+
const mieRateEl = this.#shadowRoot.querySelector('#mie-rate');
|
|
97
|
+
if (!(locationEl && lodgingRateEl && mieRateEl))
|
|
98
|
+
throw new Error(`Failed to render row's rate elements.`);
|
|
99
|
+
|
|
100
|
+
locationEl.textContent = `${this.#expense.city} (${this.#expense.country})`;
|
|
101
|
+
lodgingRateEl.setAttribute(
|
|
102
|
+
'text',
|
|
103
|
+
`<p class="font-semibold">Lodging</p>
|
|
104
|
+
<p class="text-sm sm:text-base">Max ${USD.format(this.#maxLodging)}</p>`,
|
|
105
|
+
);
|
|
106
|
+
mieRateEl.setAttribute(
|
|
107
|
+
'text',
|
|
108
|
+
`<p class="font-semibold">M&IE</p>
|
|
109
|
+
<p class="text-sm sm:text-base">Max ${USD.format(this.#maxMie)}</p>`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#disableUnusedRowEls() {
|
|
114
|
+
const lodgingEl = this.#shadowRoot.querySelector('#lodging');
|
|
115
|
+
const deductionsEl = this.#shadowRoot.querySelector('#deductions');
|
|
116
|
+
if (this.#expensesCategory === 'mie') {
|
|
117
|
+
lodgingEl?.classList.add('disabled');
|
|
118
|
+
this.#shadowRoot
|
|
119
|
+
.querySelector<HTMLInputElement>('#lodging-amount')
|
|
120
|
+
?.setAttribute('disabled', '');
|
|
121
|
+
}
|
|
122
|
+
if (this.#expensesCategory === 'lodging') {
|
|
123
|
+
deductionsEl?.classList.add('disabled');
|
|
124
|
+
deductionsEl
|
|
125
|
+
?.querySelectorAll('input')
|
|
126
|
+
.forEach(el => el.setAttribute('disabled', ''));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* EVENTS
|
|
131
|
+
*/
|
|
132
|
+
#addEventListeners() {
|
|
133
|
+
// Input change events
|
|
134
|
+
this.#row.addEventListener('change', e => {
|
|
135
|
+
this.#handleInputs(e);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Mouse, touch events
|
|
139
|
+
let pointerStartX = 0;
|
|
140
|
+
let pointerStartY = 0;
|
|
141
|
+
this.#row.addEventListener('pointerdown', e => {
|
|
142
|
+
if (!(e instanceof PointerEvent)) return;
|
|
143
|
+
const result = handlePointerDown(e);
|
|
144
|
+
pointerStartX = result.pointerStartX;
|
|
145
|
+
pointerStartY = result.pointerStartY;
|
|
146
|
+
});
|
|
147
|
+
this.#row.addEventListener('pointerup', e => {
|
|
148
|
+
if (e instanceof PointerEvent) {
|
|
149
|
+
const result = handlePointerUp(
|
|
150
|
+
e,
|
|
151
|
+
this.#handleClicks.bind(this),
|
|
152
|
+
pointerStartX,
|
|
153
|
+
pointerStartY,
|
|
154
|
+
);
|
|
155
|
+
pointerStartX = result.pointerStartX;
|
|
156
|
+
pointerStartY = result.pointerStartY;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Keyboard events
|
|
161
|
+
this.#row.addEventListener('keydown', e => {
|
|
162
|
+
if (!(e.key === 'Enter' || e.key === ' ')) return;
|
|
163
|
+
this.#handleClicks(e);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#handleInputs(e: Event) {
|
|
168
|
+
const target = e.target;
|
|
169
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
170
|
+
if (target.getAttribute('id') === 'lodging-amount') {
|
|
171
|
+
this.#updateLodgingAmount(target.value);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const attrName = target.getAttribute('name');
|
|
175
|
+
if (
|
|
176
|
+
!(
|
|
177
|
+
attrName === 'breakfast' ||
|
|
178
|
+
attrName === 'lunch' ||
|
|
179
|
+
attrName === 'dinner'
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return;
|
|
183
|
+
|
|
184
|
+
this.setAttribute(`${attrName}provided`, target.checked ? 'yes' : 'no');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#handleClicks(e: Event) {
|
|
188
|
+
const target = e.target;
|
|
189
|
+
if (!(target instanceof SVGElement || target instanceof HTMLElement))
|
|
190
|
+
return;
|
|
191
|
+
if (target.closest('[data-pdc="expense-row-toggle"]')) this.rowToggle();
|
|
192
|
+
if (
|
|
193
|
+
!!target.closest('#deductions') &&
|
|
194
|
+
e instanceof KeyboardEvent &&
|
|
195
|
+
target instanceof HTMLLabelElement
|
|
196
|
+
) {
|
|
197
|
+
target.click();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* GET ELS
|
|
202
|
+
*/
|
|
203
|
+
get #row() {
|
|
204
|
+
const el = this.#shadowRoot.querySelector<HTMLElement>('#expense-row');
|
|
205
|
+
if (!el) throw new Error('Failed to render row elements.');
|
|
206
|
+
return el;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get #rowAnimatedEls() {
|
|
210
|
+
const summary = this.#row.querySelector<HTMLElement>(
|
|
211
|
+
'[data-pdc="expense-row-summary"]',
|
|
212
|
+
);
|
|
213
|
+
const details = this.#row.querySelector<HTMLElement>(
|
|
214
|
+
'[data-pdc="expense-row-details"]',
|
|
215
|
+
);
|
|
216
|
+
if (!(summary && details))
|
|
217
|
+
throw new Error('Failed to render row summary elements.');
|
|
218
|
+
return {
|
|
219
|
+
summary,
|
|
220
|
+
details,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get #shadowRoot() {
|
|
225
|
+
if (!this.shadowRoot)
|
|
226
|
+
throw new Error(
|
|
227
|
+
'Failed to create ShadowRoot for expense row custom element',
|
|
228
|
+
);
|
|
229
|
+
return this.shadowRoot;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* VISUAL METHODS
|
|
233
|
+
*/
|
|
234
|
+
rowToggle = async (toggle: 'open' | 'close' | null = null) => {
|
|
235
|
+
if (!this.#styled || this.#row.classList.contains('toggling')) return;
|
|
236
|
+
|
|
237
|
+
if (!toggle) {
|
|
238
|
+
this.rowToggle(
|
|
239
|
+
this.#row.offsetHeight === ROW_CLOSED_HEIGHT ? 'open' : 'close',
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.#row.classList.remove('pdc-row-open', 'pdc-row-close');
|
|
245
|
+
this.#row.classList.add('toggling', `pdc-row-${toggle}`);
|
|
246
|
+
|
|
247
|
+
await this.#animateRow(toggle);
|
|
248
|
+
this.#row.classList.remove('toggling');
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
#animateRow = async (direction: 'open' | 'close') => {
|
|
252
|
+
await wait(ROW_ANIMATE_MS);
|
|
253
|
+
this.#enableRowTabIndex(direction === 'open' ? true : false);
|
|
254
|
+
this.#row.style.height = `${direction === 'open' ? this.#row.scrollHeight : this.#row.clientHeight}px`;
|
|
255
|
+
this.#rowAnimatedEls.summary.style.opacity =
|
|
256
|
+
direction === 'open' ? '0' : '100';
|
|
257
|
+
this.#rowAnimatedEls.summary.style.transform =
|
|
258
|
+
direction === 'open' ? `translateY(-200%)` : `translateY(0%)`;
|
|
259
|
+
this.#rowAnimatedEls.details.style.opacity =
|
|
260
|
+
direction === 'open' ? '100' : '0';
|
|
261
|
+
this.#rowAnimatedEls.details.style.transform =
|
|
262
|
+
direction === 'open' ? `translateX(100%)` : `translateX(0%)`;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
windowResize = () => {
|
|
266
|
+
if (this.#row.offsetHeight === ROW_CLOSED_HEIGHT) return;
|
|
267
|
+
this.#row.style.height =
|
|
268
|
+
this.#rowAnimatedEls.details.scrollHeight + 'px';
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
styleRow = () => {
|
|
272
|
+
if (!this.parentNode)
|
|
273
|
+
throw new Error(`Failed to get row index in Expense view.`);
|
|
274
|
+
const index = Array.from(
|
|
275
|
+
this.parentNode.querySelectorAll('pdc-expense-row'),
|
|
276
|
+
).indexOf(this);
|
|
277
|
+
const color = index % 2 === 0 ? 'neutral-50' : 'white';
|
|
278
|
+
const oppColor = color === 'neutral-50' ? 'white' : 'neutral-50';
|
|
279
|
+
this.#row.classList.remove('bg-neutral-50', 'bg-white');
|
|
280
|
+
this.#row.classList.add(`bg-${color}`);
|
|
281
|
+
[...this.#rowAnimatedEls.details.children].forEach((el, i) => {
|
|
282
|
+
el.classList.remove('bg-white', 'bg-neutral-50');
|
|
283
|
+
el.classList.add(i % 2 === 0 ? `bg-${color}` : `bg-${oppColor}`);
|
|
284
|
+
});
|
|
285
|
+
this.style.position = 'relative';
|
|
286
|
+
this.style.overflow = 'hidden';
|
|
287
|
+
this.style.zIndex = index.toString();
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/* UPDATE METHODS
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
#enableRowTabIndex(enable: boolean) {
|
|
294
|
+
this.#shadowRoot
|
|
295
|
+
.querySelector('[data-pdc="expense-row-details"]')
|
|
296
|
+
?.querySelectorAll('[tabindex]')
|
|
297
|
+
.forEach(el => {
|
|
298
|
+
el.setAttribute('tabindex', enable ? '0' : '-1');
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
updateMieAmount(amount: number) {
|
|
303
|
+
const mieTotalAmountInp =
|
|
304
|
+
this.#shadowRoot.querySelector<HTMLInputElement>(
|
|
305
|
+
'#mie-total-amount',
|
|
306
|
+
);
|
|
307
|
+
const mieSummary = this.#shadowRoot.querySelector<HTMLInputElement>(
|
|
308
|
+
'#summary-mie-amount',
|
|
309
|
+
);
|
|
310
|
+
if (!(mieTotalAmountInp && mieSummary))
|
|
311
|
+
throw new Error(`Failed to render row's M&IE elements.`);
|
|
312
|
+
const oldAmount = mieTotalAmountInp.value;
|
|
313
|
+
const newAmount = amount.toFixed(2).toString();
|
|
314
|
+
if (oldAmount === newAmount) return;
|
|
315
|
+
|
|
316
|
+
mieTotalAmountInp.value = newAmount;
|
|
317
|
+
mieSummary.textContent = USD.format(amount);
|
|
318
|
+
this.#mieAmount = amount;
|
|
319
|
+
if (this.#styled) highlightSuccess(mieTotalAmountInp);
|
|
320
|
+
this.#updateTotalAmount();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#updateLodgingAmount(value: string) {
|
|
324
|
+
const lodgingInput =
|
|
325
|
+
this.#shadowRoot.querySelector<HTMLInputElement>('#lodging-amount');
|
|
326
|
+
const lodgingSummary = this.#shadowRoot.querySelector<HTMLInputElement>(
|
|
327
|
+
'#summary-lodging-amount',
|
|
328
|
+
);
|
|
329
|
+
if (!(lodgingInput && lodgingSummary))
|
|
330
|
+
throw new Error(
|
|
331
|
+
'Failed to render input element for lodging in Expense row.',
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Check if input was a valid amount. If yes, adopt it for the row. If no, reset the row's lodging amount to match the max lodging rate.
|
|
335
|
+
|
|
336
|
+
const isValidLodgingAmount = (value: string) => {
|
|
337
|
+
return (
|
|
338
|
+
!isNaN(parseFloat(value)) &&
|
|
339
|
+
+value >= 0 &&
|
|
340
|
+
+value <= this.#maxLodging
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (isValidLodgingAmount(value)) {
|
|
345
|
+
this.setAttribute('lodging', value);
|
|
346
|
+
lodgingInput.value = (+value).toFixed(2).toString();
|
|
347
|
+
this.#lodgingAmount = +value;
|
|
348
|
+
lodgingSummary.textContent = USD.format(+value);
|
|
349
|
+
} else {
|
|
350
|
+
this.setAttribute('lodging', this.#maxLodging.toString());
|
|
351
|
+
lodgingInput.value = this.#maxLodging.toFixed(2).toString();
|
|
352
|
+
this.#lodgingAmount = this.#maxLodging;
|
|
353
|
+
lodgingSummary.textContent = USD.format(this.#maxLodging);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (this.#styled) highlightSuccess(lodgingInput);
|
|
357
|
+
this.#updateTotalAmount();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#updateTotalAmount() {
|
|
361
|
+
const totalAmount = this.#lodgingAmount + this.#mieAmount;
|
|
362
|
+
const totalEls = this.#shadowRoot.querySelectorAll(
|
|
363
|
+
'[data-pdc="expense-total"]',
|
|
364
|
+
);
|
|
365
|
+
totalEls?.forEach(el => {
|
|
366
|
+
if (el instanceof HTMLInputElement) {
|
|
367
|
+
el.value = totalAmount.toFixed(2).toString();
|
|
368
|
+
if (this.#styled) highlightSuccess(el);
|
|
369
|
+
}
|
|
370
|
+
if (el instanceof HTMLParagraphElement)
|
|
371
|
+
el.textContent = `${USD.format(totalAmount)}`;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* GET DATA METHODS
|
|
376
|
+
*/
|
|
377
|
+
get rateSource() {
|
|
378
|
+
return this.#expense.source;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
get rateString() {
|
|
382
|
+
const { effDate, ...rates } = this.#expense.rates;
|
|
383
|
+
const { country, city } = this.#expense;
|
|
384
|
+
return JSON.stringify({ city, country, rates });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
get rateStringForTable() {
|
|
388
|
+
const { date, country, city, rates } = this.#expense;
|
|
389
|
+
const monthYear = `${getMM(date)}/${getYYYY(date)}`;
|
|
390
|
+
return JSON.stringify({ monthYear, country, city, rates });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
get amount() {
|
|
394
|
+
const { mieAmount: mie, lodgingAmount: lodging } = this.#expense;
|
|
395
|
+
return { mie, lodging };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="expense-row"
|
|
3
|
+
class="group [.pdc-row-open]:animate-row-expense-open
|
|
4
|
+
[.pdc-row-close]:animate-row-expense-close relative flex h-24
|
|
5
|
+
items-start overflow-hidden bg-white ring-2 ring-neutral-200
|
|
6
|
+
transition-shadow duration-500"
|
|
7
|
+
>
|
|
8
|
+
<!-- Row sidebar: date, toggle -->
|
|
9
|
+
<div
|
|
10
|
+
data-pdc="expense-row-sidebar"
|
|
11
|
+
class="relative z-10 mr-4 ml-2 h-[var(--row-expense-open)] sm:mr-6
|
|
12
|
+
sm:ml-4"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
data-pdc="expense-row-toggle"
|
|
16
|
+
class="flex h-24 w-full items-center text-neutral-800
|
|
17
|
+
hover:cursor-pointer *:hover:cursor-pointer
|
|
18
|
+
group-not-[.pdc-row-close]:hover:[&_button_svg]:!-rotate-0
|
|
19
|
+
group-[.pdc-row-close]:hover:[&_button_svg]:!-rotate-180"
|
|
20
|
+
>
|
|
21
|
+
<button
|
|
22
|
+
title="Toggle row"
|
|
23
|
+
tabindex="0"
|
|
24
|
+
type="button"
|
|
25
|
+
class="focus-visible:border-primary-800 rounded-full border-3
|
|
26
|
+
border-transparent transition-colors focus:outline-none"
|
|
27
|
+
>
|
|
28
|
+
<svg
|
|
29
|
+
class="size-5 shrink-0 fill-none stroke-current
|
|
30
|
+
transition-transform duration-500
|
|
31
|
+
group-[.pdc-row-close]:rotate-0
|
|
32
|
+
group-[.pdc-row-open]:-rotate-180 sm:size-6"
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
stroke-linecap="round"
|
|
38
|
+
stroke-linejoin="round"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
d="M19 9l-7 7-7-7"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
<div
|
|
45
|
+
class="ml-2 flex flex-col text-center text-xs font-semibold
|
|
46
|
+
uppercase sm:text-sm"
|
|
47
|
+
>
|
|
48
|
+
<span id="month"></span>
|
|
49
|
+
<span id="day" class="text-xl"></span>
|
|
50
|
+
<span id="year"></span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Row contents: summary, details -->
|
|
56
|
+
<div
|
|
57
|
+
class="relative z-10 flex h-[var(--row-expense-open)] w-full items-start
|
|
58
|
+
overflow-hidden"
|
|
59
|
+
>
|
|
60
|
+
<!-- Summary -->
|
|
61
|
+
<div
|
|
62
|
+
data-pdc="expense-row-toggle"
|
|
63
|
+
class="ml-1 flex h-22 w-full items-center justify-between"
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
data-pdc="expense-row-summary"
|
|
67
|
+
inert
|
|
68
|
+
class="group-[.pdc-row-open]:animate-row-open-summary
|
|
69
|
+
group-[.pdc-row-close]:animate-row-close-summary m ml-[30%]
|
|
70
|
+
flex w-full max-w-[50lvh] flex-col justify-between gap-y-0.5
|
|
71
|
+
overflow-hidden pr-4 text-sm
|
|
72
|
+
group-[.pdc-row-add]:translate-y-[-200%]
|
|
73
|
+
group-[.pdc-row-add]:opacity-0 sm:ml-[10%] sm:flex
|
|
74
|
+
sm:max-w-full sm:flex-row sm:justify-between sm:pr-12
|
|
75
|
+
sm:text-base"
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
class="grid grid-cols-2 sm:flex sm:flex-col"
|
|
79
|
+
id="summary-mie"
|
|
80
|
+
>
|
|
81
|
+
<p>M&IE</p>
|
|
82
|
+
<p class="text-right sm:text-left" id="summary-mie-amount">
|
|
83
|
+
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
class="grid grid-cols-2 sm:flex sm:flex-col"
|
|
88
|
+
id="summary-lodging"
|
|
89
|
+
>
|
|
90
|
+
<p>Lodging</p>
|
|
91
|
+
<p
|
|
92
|
+
class="text-right sm:text-left"
|
|
93
|
+
id="summary-lodging-amount"
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
class="grid grid-cols-2 sm:flex sm:flex-col"
|
|
100
|
+
id="summary-total"
|
|
101
|
+
>
|
|
102
|
+
<p>Total</p>
|
|
103
|
+
<p class="text-right sm:text-left" data-pdc="expense-total">
|
|
104
|
+
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Details -->
|
|
111
|
+
<div
|
|
112
|
+
data-pdc="expense-row-details"
|
|
113
|
+
class="group-[.pdc-row-open]:animate-row-open-details
|
|
114
|
+
group-[.pdc-row-add]:animate-row-open-details
|
|
115
|
+
group-[.pdc-row-close]:animate-row-close-details absolute top-0
|
|
116
|
+
left-[-100%] w-full overflow-hidden border-l-2
|
|
117
|
+
border-l-neutral-200 transition-colors"
|
|
118
|
+
>
|
|
119
|
+
<!-- Location -->
|
|
120
|
+
<div
|
|
121
|
+
class="flex items-center justify-between border-b
|
|
122
|
+
border-b-neutral-100 px-3 py-2"
|
|
123
|
+
>
|
|
124
|
+
<p
|
|
125
|
+
id="location"
|
|
126
|
+
class="relative my-3 truncate text-neutral-800"
|
|
127
|
+
></p>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Deductions -->
|
|
131
|
+
<div
|
|
132
|
+
id="deductions"
|
|
133
|
+
class="items-center justify-between border-b
|
|
134
|
+
border-b-neutral-100 px-3 py-4
|
|
135
|
+
[.disabled]:cursor-not-allowed [.disabled]:**:opacity-30"
|
|
136
|
+
>
|
|
137
|
+
<pdc-label styled="true" text="I was provided"></pdc-label>
|
|
138
|
+
<fieldset class="my-3 space-y-3">
|
|
139
|
+
<div
|
|
140
|
+
class="*:has-checked:bg-primary-50 *:hover:bg-primary-50
|
|
141
|
+
*:has-checked:text-primary-800
|
|
142
|
+
*:hover:text-primary-800
|
|
143
|
+
*:focus-visible:!bg-primary-50
|
|
144
|
+
*:focus-visible:!text-primary-800
|
|
145
|
+
*:focus-visible:ring-primary-800 grid grid-cols-3
|
|
146
|
+
divide-x overflow-hidden rounded-lg border
|
|
147
|
+
bg-transparent text-center text-neutral-700
|
|
148
|
+
*:cursor-pointer *:px-4 *:py-2.5 *:text-sm
|
|
149
|
+
*:font-medium *:ring-2 *:ring-transparent
|
|
150
|
+
*:transition-[box-shadow_background-color_color]
|
|
151
|
+
*:duration-500 *:select-none *:ring-inset
|
|
152
|
+
*:focus:outline-none *:focus-visible:ring-6"
|
|
153
|
+
>
|
|
154
|
+
<label tabindex="-1" ="breakfast"
|
|
155
|
+
>Break<span class="sm:hidden" aria-hidden="true"
|
|
156
|
+
>.</span
|
|
157
|
+
><span class="hidden sm:inline-block">fast</span
|
|
158
|
+
><input
|
|
159
|
+
type="checkbox"
|
|
160
|
+
name="breakfast"
|
|
161
|
+
id="breakfast"
|
|
162
|
+
class="hidden"
|
|
163
|
+
/></label>
|
|
164
|
+
<label tabindex="-1" for="lunch" class="peer"
|
|
165
|
+
>Lunch<input
|
|
166
|
+
type="checkbox"
|
|
167
|
+
name="lunch"
|
|
168
|
+
id="lunch"
|
|
169
|
+
class="hidden"
|
|
170
|
+
/></label>
|
|
171
|
+
<label tabindex="-1" for="dinner" class="peer"
|
|
172
|
+
>Dinner<input
|
|
173
|
+
type="checkbox"
|
|
174
|
+
name="dinner"
|
|
175
|
+
id="dinner"
|
|
176
|
+
class="hidden"
|
|
177
|
+
/></label>
|
|
178
|
+
</div>
|
|
179
|
+
</fieldset>
|
|
180
|
+
<!-- M&IE -->
|
|
181
|
+
<div id="mie" class="flex items-center justify-between">
|
|
182
|
+
<pdc-label styled="true" id="mie-rate"></pdc-label>
|
|
183
|
+
|
|
184
|
+
<input
|
|
185
|
+
id="mie-total-amount"
|
|
186
|
+
type="number"
|
|
187
|
+
inputmode="decimal"
|
|
188
|
+
step="0.01"
|
|
189
|
+
min="0"
|
|
190
|
+
disabled
|
|
191
|
+
class="[.success]:border-r-success-400 relative my-3
|
|
192
|
+
w-[50%] [appearance:textfield] border-r-3
|
|
193
|
+
border-r-transparent bg-transparent py-2 pr-3
|
|
194
|
+
text-right text-neutral-800
|
|
195
|
+
transition-[border-color]
|
|
196
|
+
disabled:cursor-not-allowed
|
|
197
|
+
[&::-webkit-inner-spin-button]:appearance-none
|
|
198
|
+
[&::-webkit-outer-spin-button]:appearance-none"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- Lodging -->
|
|
204
|
+
<div
|
|
205
|
+
id="lodging"
|
|
206
|
+
class="flex items-center justify-between border-b
|
|
207
|
+
border-b-neutral-100 bg-white px-3 py-2
|
|
208
|
+
[.disabled]:cursor-not-allowed [.disabled]:**:opacity-30"
|
|
209
|
+
>
|
|
210
|
+
<pdc-label styled="true" id="lodging-rate"></pdc-label>
|
|
211
|
+
|
|
212
|
+
<input
|
|
213
|
+
tabindex="-1"
|
|
214
|
+
id="lodging-amount"
|
|
215
|
+
type="number"
|
|
216
|
+
inputmode="decimal"
|
|
217
|
+
step="0.01"
|
|
218
|
+
min="0"
|
|
219
|
+
class="[.success]:border-r-success-400
|
|
220
|
+
selection:bg-primary-800 relative my-3 w-[50%]
|
|
221
|
+
[appearance:textfield] border-r-3 border-b-2
|
|
222
|
+
border-r-transparent border-b-neutral-200 bg-transparent
|
|
223
|
+
py-2 pr-3 text-right text-neutral-800
|
|
224
|
+
transition-[border-color] selection:text-neutral-50
|
|
225
|
+
focus:outline-none disabled:cursor-not-allowed
|
|
226
|
+
disabled:border-b-transparent
|
|
227
|
+
[&::-webkit-inner-spin-button]:appearance-none
|
|
228
|
+
[&::-webkit-outer-spin-button]:appearance-none"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Total -->
|
|
233
|
+
<div
|
|
234
|
+
id="total"
|
|
235
|
+
class="group flex items-center justify-between border-b
|
|
236
|
+
border-b-neutral-100 bg-white px-3"
|
|
237
|
+
>
|
|
238
|
+
<pdc-label
|
|
239
|
+
styled="true"
|
|
240
|
+
text="<p class='font-semibold'>Total</p>"
|
|
241
|
+
></pdc-label>
|
|
242
|
+
|
|
243
|
+
<input
|
|
244
|
+
data-pdc="expense-total"
|
|
245
|
+
type="number"
|
|
246
|
+
inputmode="decimal"
|
|
247
|
+
step="0.01"
|
|
248
|
+
min="0"
|
|
249
|
+
disabled
|
|
250
|
+
class="[.success]:border-r-success-400 relative my-3 w-[50%]
|
|
251
|
+
[appearance:textfield] border-r-3 border-r-transparent
|
|
252
|
+
bg-transparent py-2 pr-3 text-right text-neutral-800
|
|
253
|
+
transition-[border-color] disabled:cursor-not-allowed
|
|
254
|
+
[&::-webkit-inner-spin-button]:appearance-none
|
|
255
|
+
[&::-webkit-outer-spin-button]:appearance-none"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|