Account for nights and double meals in estimator
This commit is contained in:
parent
545a341efb
commit
3f53d3c289
1 changed files with 194 additions and 10 deletions
|
|
@ -30,6 +30,7 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
<p class="print-only print-report-note">
|
||||
Rates include 18% GST. Accommodation rates are for one night.
|
||||
</p>
|
||||
<p class="print-only report-meta">Estimate date: <span data-report-date></span></p>
|
||||
</div>
|
||||
<button class="button button--secondary print-button" type="button" data-print-estimate>
|
||||
Print Cost Estimate
|
||||
|
|
@ -62,11 +63,11 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
<dd>{currency.format(withGst(rates.double))}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Lunch</dt>
|
||||
<dt>Lunch Per Person</dt>
|
||||
<dd>{currency.format(withGst(rates.lunch))}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Dinner</dt>
|
||||
<dt>Dinner Per Person</dt>
|
||||
<dd>{currency.format(withGst(rates.dinner))}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
|
@ -76,7 +77,7 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
<div class="table-head">
|
||||
<div>
|
||||
<h2 id="estimate-table-title">Estimate</h2>
|
||||
<p>Rates include 18% GST.</p>
|
||||
<p>Rates include 18% GST. Lunch and dinner are counted twice for double occupancy.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -85,6 +86,23 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">
|
||||
<span class="column-heading">
|
||||
Nights
|
||||
<label class="night-global-control">
|
||||
<span class="sr-only">Set nights for all visible rows</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value="1"
|
||||
inputmode="numeric"
|
||||
data-nights-all
|
||||
aria-label="Set number of nights for all visible rows"
|
||||
/>
|
||||
</label>
|
||||
</span>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="column-heading">
|
||||
Single Occupancy
|
||||
|
|
@ -136,6 +154,20 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
data-1p-ignore
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<label class="sr-only" for={`nights-${row}`}>Number of nights for person {row}</label>
|
||||
<input
|
||||
id={`nights-${row}`}
|
||||
class="night-input"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value="1"
|
||||
inputmode="numeric"
|
||||
data-nights
|
||||
aria-label={`Number of nights for person ${row}`}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<label class="check-cell">
|
||||
<input
|
||||
|
|
@ -162,14 +194,14 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
</td>
|
||||
<td>
|
||||
<label class="check-cell">
|
||||
<input type="checkbox" value="lunch" data-cost="lunch" aria-label={`Lunch for person ${row}`} />
|
||||
<span>Lunch</span>
|
||||
<input type="checkbox" value="lunch" data-cost="lunch" data-meal aria-label={`Lunch for person ${row}`} />
|
||||
<span data-meal-label data-base-label="Lunch">Lunch</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<label class="check-cell">
|
||||
<input type="checkbox" value="dinner" data-cost="dinner" aria-label={`Dinner for person ${row}`} />
|
||||
<span>Dinner</span>
|
||||
<input type="checkbox" value="dinner" data-cost="dinner" data-meal aria-label={`Dinner for person ${row}`} />
|
||||
<span data-meal-label data-base-label="Dinner">Dinner</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -186,6 +218,9 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
</div>
|
||||
<p data-total-note>For 5 people.</p>
|
||||
</div>
|
||||
<p class="calculator-disclaimer">
|
||||
These numbers are indicative and not binding.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -211,6 +246,8 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
const totalNote = document.querySelector<HTMLElement>("[data-total-note]");
|
||||
const printButton = document.querySelector<HTMLButtonElement>("[data-print-estimate]");
|
||||
const columnToggleButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-toggle-column]"));
|
||||
const globalNightsInput = document.querySelector<HTMLInputElement>("[data-nights-all]");
|
||||
const reportDate = document.querySelector<HTMLElement>("[data-report-date]");
|
||||
|
||||
function getPeopleCount() {
|
||||
return Number(peopleInput?.value ?? 1);
|
||||
|
|
@ -238,6 +275,24 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
return rows.filter((row) => !row.hidden);
|
||||
}
|
||||
|
||||
function normalizeNights(value: string | number | undefined) {
|
||||
const nights = Math.floor(Number(value));
|
||||
return Number.isFinite(nights) && nights > 0 ? nights : 1;
|
||||
}
|
||||
|
||||
function getNights(row: HTMLTableRowElement) {
|
||||
return normalizeNights(row.querySelector<HTMLInputElement>("[data-nights]")?.value);
|
||||
}
|
||||
|
||||
function syncGlobalNights() {
|
||||
if (!globalNightsInput) return;
|
||||
|
||||
const visibleNightValues = getVisibleRows().map(getNights);
|
||||
const firstValue = visibleNightValues[0] ?? 1;
|
||||
const sameValue = visibleNightValues.every((value) => value === firstValue);
|
||||
globalNightsInput.value = sameValue ? String(firstValue) : "";
|
||||
}
|
||||
|
||||
function getColumnInputs(cost: keyof typeof rateMap) {
|
||||
return getVisibleRows()
|
||||
.map((row) => row.querySelector<HTMLInputElement>(`input[data-cost='${cost}']`))
|
||||
|
|
@ -255,34 +310,75 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
});
|
||||
}
|
||||
|
||||
function getMealMultiplier(row: HTMLTableRowElement) {
|
||||
return row.querySelector<HTMLInputElement>("input[data-cost='double']")?.checked ? 2 : 1;
|
||||
}
|
||||
|
||||
function syncPrintValues() {
|
||||
rows.forEach((row) => {
|
||||
row.querySelectorAll<HTMLLabelElement>(".check-cell").forEach((label) => {
|
||||
const input = label.querySelector<HTMLInputElement>("input[type='checkbox']");
|
||||
label.dataset.printValue = input?.checked ? "Yes" : "No";
|
||||
const isDoubledMeal = input?.checked && ["lunch", "dinner"].includes(input.dataset.cost ?? "") && getMealMultiplier(row) === 2;
|
||||
label.dataset.printValue = input?.checked ? (isDoubledMeal ? "Yes (2)" : "Yes") : "No";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncMealLabels() {
|
||||
rows.forEach((row) => {
|
||||
const mealMultiplier = getMealMultiplier(row);
|
||||
const rowIndex = row.dataset.rowIndex ?? "";
|
||||
|
||||
row.querySelectorAll<HTMLElement>("[data-meal-label]").forEach((label) => {
|
||||
const baseLabel = label.dataset.baseLabel ?? label.textContent ?? "";
|
||||
label.textContent = mealMultiplier === 2 ? `${baseLabel} x2` : baseLabel;
|
||||
});
|
||||
|
||||
row.querySelectorAll<HTMLInputElement>("input[data-meal]").forEach((input) => {
|
||||
const baseLabel = input.dataset.cost === "dinner" ? "Dinner" : "Lunch";
|
||||
input.setAttribute(
|
||||
"aria-label",
|
||||
`${baseLabel}${mealMultiplier === 2 ? " for two people" : ""} for person ${rowIndex}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncReportDate() {
|
||||
if (!reportDate) return;
|
||||
|
||||
reportDate.textContent = new Intl.DateTimeFormat("en-IN", {
|
||||
dateStyle: "medium",
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
function updateTotal() {
|
||||
let total = 0;
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row.hidden) return;
|
||||
|
||||
const mealMultiplier = getMealMultiplier(row);
|
||||
const nights = getNights(row);
|
||||
|
||||
row.querySelectorAll<HTMLInputElement>("input[type='checkbox']:checked").forEach((input) => {
|
||||
const cost = input.dataset.cost as keyof typeof rateMap | undefined;
|
||||
if (cost) total += rateMap[cost];
|
||||
if (!cost) return;
|
||||
|
||||
const multiplier = cost === "lunch" || cost === "dinner" ? mealMultiplier : nights;
|
||||
total += rateMap[cost] * multiplier;
|
||||
});
|
||||
});
|
||||
|
||||
if (totalCost) totalCost.textContent = currencyFormatter.format(total);
|
||||
syncColumnToggles();
|
||||
syncMealLabels();
|
||||
syncPrintValues();
|
||||
}
|
||||
|
||||
peopleInput?.addEventListener("input", () => {
|
||||
syncRows();
|
||||
syncGlobalNights();
|
||||
updateTotal();
|
||||
});
|
||||
|
||||
|
|
@ -299,6 +395,26 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
|
||||
updateTotal();
|
||||
});
|
||||
|
||||
row.addEventListener("input", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLInputElement) || !target.matches("[data-nights]")) return;
|
||||
|
||||
syncGlobalNights();
|
||||
updateTotal();
|
||||
});
|
||||
});
|
||||
|
||||
globalNightsInput?.addEventListener("input", () => {
|
||||
const nights = normalizeNights(globalNightsInput.value);
|
||||
|
||||
getVisibleRows().forEach((row) => {
|
||||
const input = row.querySelector<HTMLInputElement>("[data-nights]");
|
||||
if (input) input.value = String(nights);
|
||||
});
|
||||
|
||||
syncGlobalNights();
|
||||
updateTotal();
|
||||
});
|
||||
|
||||
columnToggleButtons.forEach((button) => {
|
||||
|
|
@ -327,10 +443,13 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
|
||||
printButton?.addEventListener("click", () => {
|
||||
syncPrintValues();
|
||||
syncReportDate();
|
||||
window.print();
|
||||
});
|
||||
|
||||
syncRows();
|
||||
syncGlobalNights();
|
||||
syncReportDate();
|
||||
updateTotal();
|
||||
</script>
|
||||
|
||||
|
|
@ -457,7 +576,7 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
}
|
||||
|
||||
.calculator-table {
|
||||
min-width: 760px;
|
||||
min-width: 860px;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
|
|
@ -495,6 +614,37 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
box-shadow: 0 0 0 1px color-mix(in oklch, var(--primary) 24%, var(--border));
|
||||
}
|
||||
|
||||
.night-global-control {
|
||||
display: block;
|
||||
width: 5.2rem;
|
||||
}
|
||||
|
||||
.night-global-control input,
|
||||
.night-input {
|
||||
width: 5.2rem;
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 0.35rem 0.45rem;
|
||||
box-shadow: 0 0 0 1px color-mix(in oklch, var(--border) 86%, transparent);
|
||||
}
|
||||
|
||||
.night-input {
|
||||
min-height: 44px;
|
||||
background: color-mix(in oklch, var(--muted-2) 34%, white);
|
||||
}
|
||||
|
||||
.night-global-control input:focus,
|
||||
.night-input:focus {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.calculator-table td {
|
||||
padding: 0.6rem 0.7rem;
|
||||
vertical-align: middle;
|
||||
|
|
@ -571,6 +721,13 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.calculator-disclaimer {
|
||||
margin-top: 0.8rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
|
@ -656,6 +813,12 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
font-size: 9.5pt;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
margin-top: 0.16rem;
|
||||
color: #50554c;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.calculator-panel {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
|
@ -753,6 +916,10 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.night-global-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.check-cell span {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -783,6 +950,16 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.night-input {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: #151812;
|
||||
font-size: 9pt;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.total-bar {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
|
|
@ -794,6 +971,13 @@ const currency = new Intl.NumberFormat("en-IN", {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.calculator-disclaimer {
|
||||
margin-top: 0.35rem;
|
||||
color: #50554c;
|
||||
font-size: 8.5pt;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-bar span {
|
||||
color: #50554c;
|
||||
font-size: 8pt;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue