(add): rest of search form
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
163
webapp/src/lib/components/SearchHeader.svelte
Normal file
163
webapp/src/lib/components/SearchHeader.svelte
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script>
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import hotkeys from "hotkeys-js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
searchForm = $bindable(),
|
||||||
|
headerHeight = $bindable(),
|
||||||
|
functions,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
hotkeys.filter = function (event) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
hotkeys("alt+n", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.nextPage();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+b", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.prevPage();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+j", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.duplicateDown();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+u", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.duplicateUp();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+l", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.gotoNext();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+o", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.gotoPrev();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+c", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.copy();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+v", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.paste();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
hotkeys("alt+s", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
functions.saveAll();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="formheader" bind:offsetHeight={headerHeight}>
|
||||||
|
<div class="flex-row-space tb-margin">
|
||||||
|
<div class="flex-row">
|
||||||
|
<div class="column">
|
||||||
|
<div>First Name</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style="width: 20ch"
|
||||||
|
bind:value={searchForm.first_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div>Last Name</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style="width: 20ch"
|
||||||
|
bind:value={searchForm.last_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div>Phone Number</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style="width: 20ch"
|
||||||
|
bind:value={searchForm.phone_number}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
onclick={() => {
|
||||||
|
if (
|
||||||
|
searchForm.first_name ||
|
||||||
|
searchForm.last_name ||
|
||||||
|
searchForm.phone_number
|
||||||
|
) {
|
||||||
|
functions.refreshPage();
|
||||||
|
}
|
||||||
|
}}>Refresh</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-space tb-margin">
|
||||||
|
<div class="flex-row">
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + J"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.duplicateDown}>Duplicate Down</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + U"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.duplicateUp}>Duplicate Up</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + L"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.gotoNext}>Next</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + O"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.gotoPrev}>Previous</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + C"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.copy}>Copy</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + V"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.paste}>Paste</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<button
|
||||||
|
class="styled"
|
||||||
|
title="Alt + S"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={functions.saveAll}>Save All</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#formheader {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
border-bottom: solid 1px #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
current_prefix = {...new_prefix};
|
current_prefix = {...new_prefix};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
document.title = `${venue} - Main Menu`;
|
document.title = `${venue} - Main Menu`;
|
||||||
hotkeys.filter = function(event) {return true};
|
hotkeys.filter = function(event) {return true};
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
<div><h2>Admin Mode:</h2></div>
|
<div><h2>Admin Mode:</h2></div>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</a>
|
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</a>
|
||||||
|
<a href="/search/tickets" target="_blank" class="styled">Search Tickets</a>
|
||||||
<a href="/backuprestore" target="_blank" class="styled">Backup/Restore</a>
|
<a href="/backuprestore" target="_blank" class="styled">Backup/Restore</a>
|
||||||
<a href="/settings" target="_blank" class="styled">Settings</a>
|
<a href="/settings" target="_blank" class="styled">Settings</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,27 @@ export async function GET({ url }) {
|
|||||||
sPhoneNumber = url.searchParams.get("phone_number") || "";
|
sPhoneNumber = url.searchParams.get("phone_number") || "";
|
||||||
|
|
||||||
if (env.TAM3_REMOTE) {
|
if (env.TAM3_REMOTE) {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
api_key: env.TAM3_REMOTE_KEY,
|
||||||
|
first_name: sFirstName,
|
||||||
|
last_name: sLastName,
|
||||||
|
phone_number: sPhoneNumber,
|
||||||
|
});
|
||||||
|
const res = await fetch(
|
||||||
|
`${env.TAM3_REMOTE}/api/search/tickets/?${searchParams.toString()}`,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
return new Response(JSON.stringify([]), {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status: 200,
|
||||||
|
statusText: "Fetched successfully",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const results = await db
|
const results = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
248
webapp/src/routes/search/tickets/+page.svelte
Normal file
248
webapp/src/routes/search/tickets/+page.svelte
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<script>
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import SearchHeader from "$lib/components/SearchHeader.svelte";
|
||||||
|
import { focusElement } from "$lib/focusElement.js";
|
||||||
|
|
||||||
|
let searchForm = $state({
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
phone_number: "",
|
||||||
|
});
|
||||||
|
let current_idx = $state(0);
|
||||||
|
let next_idx = $derived(current_idx + 1);
|
||||||
|
let prev_idx = $derived(current_idx - 1);
|
||||||
|
let current_tickets = $state([]);
|
||||||
|
let copy_buffer = $state({
|
||||||
|
prefix: "",
|
||||||
|
t_id: 0,
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
phone_number: "",
|
||||||
|
preference: "CALL",
|
||||||
|
changed: true,
|
||||||
|
});
|
||||||
|
let headerHeight = $state();
|
||||||
|
|
||||||
|
function changeFocus(idx) {
|
||||||
|
const focusFn = document.getElementById(`${idx}_fn`);
|
||||||
|
if (focusFn) {
|
||||||
|
focusFn.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const functions = {
|
||||||
|
refreshPage: async () => {
|
||||||
|
if (
|
||||||
|
current_tickets.filter((ticket) => ticket.changed === true)
|
||||||
|
.length > 0
|
||||||
|
) {
|
||||||
|
functions.saveAll();
|
||||||
|
}
|
||||||
|
const searchParams = new URLSearchParams({ ...searchForm });
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/search/tickets?${searchParams.toString()}`,
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
current_tickets = [...data];
|
||||||
|
setTimeout(() => changeFocus(0), 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prevPage: () => {
|
||||||
|
const diff = current_tickets.length;
|
||||||
|
pagerForm.id_from = pagerForm.id_from - diff;
|
||||||
|
pagerForm.id_to = pagerForm.id_to - diff;
|
||||||
|
functions.refreshPage();
|
||||||
|
},
|
||||||
|
nextPage: () => {
|
||||||
|
const diff = current_tickets.length;
|
||||||
|
pagerForm.id_from = pagerForm.id_from + diff;
|
||||||
|
pagerForm.id_to = pagerForm.id_to + diff;
|
||||||
|
functions.refreshPage();
|
||||||
|
},
|
||||||
|
duplicateDown: () => {
|
||||||
|
if (current_tickets[next_idx]) {
|
||||||
|
current_tickets[next_idx] = {
|
||||||
|
...current_tickets[current_idx],
|
||||||
|
prefix: current_tickets[next_idx].prefix,
|
||||||
|
t_id: current_tickets[next_idx].t_id,
|
||||||
|
changed: true,
|
||||||
|
};
|
||||||
|
changeFocus(next_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(current_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
duplicateUp: () => {
|
||||||
|
if (prev_idx >= 0) {
|
||||||
|
current_tickets[prev_idx] = {
|
||||||
|
...current_tickets[current_idx],
|
||||||
|
prefix: current_tickets[prev_idx].prefix,
|
||||||
|
t_id: current_tickets[prev_idx].t_id,
|
||||||
|
changed: true,
|
||||||
|
};
|
||||||
|
changeFocus(prev_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(current_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoNext: () => {
|
||||||
|
if (current_tickets[next_idx]) {
|
||||||
|
changeFocus(next_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(current_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoPrev: () => {
|
||||||
|
if (prev_idx >= 0) {
|
||||||
|
changeFocus(prev_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(current_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copy: () => {
|
||||||
|
copy_buffer = { ...current_tickets[current_idx] };
|
||||||
|
changeFocus(current_idx);
|
||||||
|
},
|
||||||
|
paste: () => {
|
||||||
|
current_tickets[current_idx] = {
|
||||||
|
...copy_buffer,
|
||||||
|
prefix: current_tickets[current_idx].prefix,
|
||||||
|
t_id: current_tickets[current_idx].t_id,
|
||||||
|
changed: true,
|
||||||
|
};
|
||||||
|
changeFocus(current_idx);
|
||||||
|
},
|
||||||
|
saveAll: async () => {
|
||||||
|
const to_save = current_tickets.filter(
|
||||||
|
(ticket) => ticket.changed === true,
|
||||||
|
);
|
||||||
|
const res = await fetch(`/api/tickets`, {
|
||||||
|
body: JSON.stringify(to_save),
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
for (let ticket of current_tickets) {
|
||||||
|
ticket.changed = false;
|
||||||
|
}
|
||||||
|
changeFocus(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
document.title = `Ticket Search`;
|
||||||
|
window.addEventListener("beforeunload", function (e) {
|
||||||
|
if (
|
||||||
|
current_tickets.filter((ticket) => ticket.changed === true)
|
||||||
|
.length > 0
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>Ticket Search</h1>
|
||||||
|
<SearchHeader {functions} bind:searchForm bind:headerHeight />
|
||||||
|
<table>
|
||||||
|
<thead style="top: {headerHeight + 2}px">
|
||||||
|
<tr>
|
||||||
|
<th>Prefix</th>
|
||||||
|
<th style="width: 12ch">Ticket ID</th>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th>Phone Number</th>
|
||||||
|
<th>Preference</th>
|
||||||
|
<th>Changed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each current_tickets as ticket, idx}
|
||||||
|
<tr onfocusin={() => (current_idx = idx)}>
|
||||||
|
<td>{ticket.prefix}</td>
|
||||||
|
<td>{ticket.t_id}</td>
|
||||||
|
<td
|
||||||
|
><input
|
||||||
|
id="{idx}_fn"
|
||||||
|
type="text"
|
||||||
|
bind:value={ticket.first_name}
|
||||||
|
onfocus={focusElement}
|
||||||
|
onchange={() => (ticket.changed = true)}
|
||||||
|
/></td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><input
|
||||||
|
id="{idx}_ln"
|
||||||
|
type="text"
|
||||||
|
bind:value={ticket.last_name}
|
||||||
|
onfocus={focusElement}
|
||||||
|
onchange={() => (ticket.changed = true)}
|
||||||
|
/></td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><input
|
||||||
|
id="{idx}_pn"
|
||||||
|
type="text"
|
||||||
|
bind:value={ticket.phone_number}
|
||||||
|
onfocus={focusElement}
|
||||||
|
onchange={() => (ticket.changed = true)}
|
||||||
|
/></td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><select
|
||||||
|
id="{idx}_pr"
|
||||||
|
style="width: 100%"
|
||||||
|
bind:value={ticket.preference}
|
||||||
|
onfocus={focusElement}
|
||||||
|
onchange={() => (ticket.changed = true)}
|
||||||
|
>
|
||||||
|
<option value="CALL">Call</option>
|
||||||
|
<option value="TEXT">Text</option>
|
||||||
|
</select></td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><button
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={() => (ticket.changed = !ticket.changed)}
|
||||||
|
>{ticket.changed ? "Y" : "N"}</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
thead {
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
border: solid 1px #000000;
|
||||||
|
}
|
||||||
|
tbody tr:nth-child(2n) {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
tbody tr:focus-within td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: solid 1px;
|
||||||
|
border-bottom: solid 1px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background: transparent;
|
||||||
|
border: solid 1px #000000;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user