Merge pull request 'experiment/tanstack' (#172) from experiment/tanstack into dev
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #172
This commit is contained in:
Nicolai Ort 2023-04-12 19:31:47 +00:00
commit d88f3a5a27
Signed by: git.odit.services
GPG Key ID: 76E155504123332E
60 changed files with 3464 additions and 1804 deletions

View File

@ -1,6 +1,7 @@
{
"name": "@odit/lfk-frontend",
"version": "0.17.3",
"type": "module",
"scripts": {
"i18n-order": "node order.js",
"dev": "vite",
@ -10,18 +11,15 @@
},
"license": "CC-BY-NC-SA-4.0",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "1.0.0-next.6",
"@odit/license-exporter": "0.0.12",
"@types/html-minifier": "4.0.2",
"@sveltejs/vite-plugin-svelte": "2.0.4",
"auto-changelog": "2.4.0",
"autoprefixer": "10.4.14",
"html-minifier": "4.0.0",
"postcss": "8.4.21",
"release-it": "14.6.1",
"svelte-preprocess": "4.7.0",
"release-it": "15.10.1",
"svelte-select": "3.17.0",
"tailwindcss": "3.2.7",
"vite": "2.1.5"
"tailwindcss": "3.3.1",
"vite": "4.2.1"
},
"release-it": {
"git": {
@ -41,21 +39,20 @@
}
},
"dependencies": {
"tinro": "0.6.12",
"toastify-js": "1.12.0",
"validator": "13.9.0",
"xlsx": "0.16.9",
"@odit/lfk-client-js": "0.14.0",
"@vincjo/datatables": "^1.4.0",
"@odit/lfk-client-js": "0.14.3",
"@paralleldrive/cuid2": "^2.2.0",
"@tanstack/svelte-table": "^8.8.5",
"check-password-strength": "2.0.7",
"csvtojson": "2.0.10",
"gridjs": "3.4.0",
"localforage": "1.10.0",
"marked": "2.0.3",
"svelte": "3.37.0",
"svelte-focus-trap": "1.2.0",
"svelte-i18n": "3.3.9",
"@paralleldrive/cuid2": "^2.2.0"
"svelte": "3.58.0",
"svelte-i18n": "3.6.0",
"tinro": "0.6.12",
"toastify-js": "1.12.0",
"validator": "13.9.0",
"xlsx": "0.18.5"
},
"volta": {
"node": "19.7.0"

3070
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerCardService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
export let bulk_modal_open;
export let current_cards;
function focus(el) {
el.focus();
}
const dispatch = createEventDispatcher();
$: card_count = 0;
$: is_card_count_valid = card_count > 0;
$: processed_last_submit = true;
@ -34,7 +36,7 @@
text: $_("creating-blanco-cards"),
duration: -1,
}).showToast();
RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, false)
RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, true)
.then((result) => {
bulk_modal_open = false;
//
@ -43,6 +45,7 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
dispatch("created", {cards: result})
})
.catch((err) => {
//
@ -71,11 +74,11 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_cards = current_cards.concat(result);
const toast = Toastify({
text: $_("generating-pdf"),
duration: -1,
}).showToast();
dispatch("created", {cards: result})
fetch(
`${config.baseurl_documentserver}/cards?&download=true&key=${config.documentserver_key}`,
{
@ -133,7 +136,7 @@
{#if bulk_modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
bulk_modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
RunnerCardService,
RunnerService,
@ -9,9 +9,10 @@
} from "@odit/lfk-client-js";
import Select from "svelte-select";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
export let modal_open;
export let current_cards;
const dispatch = createEventDispatcher();
const getRunnerLabel = (option) => {
if (option.middlename) {
return option.firstname + " " + option.middlename + " " + option.lastname;
@ -21,7 +22,7 @@
const filterRunners = (label, filterText, option) => {
if (filterText.startsWith("#")) {
return option.value.id == parseInt(filterText.replace("#",""))
return option.value.id == parseInt(filterText.replace("#", ""));
}
return (
label.toLowerCase().includes(filterText.toLowerCase()) ||
@ -79,8 +80,7 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_cards.push(result);
current_cards = current_cards;
dispatch("created", { cards: [result] });
})
.catch((err) => {
//
@ -97,7 +97,6 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,13 +1,12 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerCardService, RunnerService } from "@odit/lfk-client-js";
import Select from "svelte-select";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
export let edit_modal_open;
export let current_cards;
export let runner = {};
export let editable = {};
export let original_data = {};
@ -30,9 +29,6 @@
$: enabled = true;
$: processed_last_submit = true;
const dispatch = createEventDispatcher();
function dataUpdated() {
dispatch('dataUpdated',);
}
RunnerService.runnerControllerGetAll().then((val) => {
runners = val.map((r) => {
return { label: getRunnerLabel(r), value: r };
@ -75,9 +71,7 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_cards[current_cards.findIndex((c) => c.id === id)] = result;
current_cards = current_cards;
dataUpdated();
dispatch('dataUpdated', {card: result});
})
.catch((err) => {
//
@ -94,7 +88,7 @@
{#if edit_modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
edit_modal_open = false;

View File

@ -0,0 +1,16 @@
<script>
import { _ } from "svelte-i18n";
export let runner;
</script>
{#if !runner}
{$_("non-blanko")}
{:else}
<a href={`/runners/${runner.id}`}>
{#if runner.middlename}
{runner.firstname} {runner.middlename} {runner.lastname}
{:else}
{runner.firstname} {runner.lastname}
{/if}
</a>
{/if}

View File

@ -0,0 +1,16 @@
<script>
import { _ } from "svelte-i18n";
export let enabled = false;
</script>
{#if enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>{$_("enabled")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
>{$_("disabled")}</span
>
{/if}

View File

@ -7,34 +7,48 @@
$: current_cards = [];
export let modal_open = false;
export let bulk_modal_open = false;
let addCards;
</script>
<section class="container p-5">
<span class="mb-1 text-3xl font-extrabold leading-tight">
{$_('cards')}
{#if store.state.jwtinfo.userdetails.permissions.includes('CARD:CREATE')}
{$_("cards")}
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")}
<button
on:click={() => {
modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('add-card')}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("add-card")}
</button>
<button
on:click={() => {
bulk_modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create-bulk-cards')}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("create-bulk-cards")}
</button>
{/if}
</span>
<CardsOverview bind:current_cards />
<CardsOverview bind:current_cards bind:addCards />
</section>
{#if store.state.jwtinfo.userdetails.permissions.includes('CARD:CREATE')}
<AddCardModal bind:current_cards bind:modal_open />
<AddCardBulkModal bind:current_cards bind:bulk_modal_open />
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")}
<AddCardModal
bind:modal_open
on:created={(event) => {
console.log(event)
addCards(event.detail.cards);
}}
/>
<AddCardBulkModal
bind:bulk_modal_open
on:created={(event) => {
addCards(event.detail.cards);
}}
/>
{/if}

View File

@ -3,35 +3,129 @@
import { RunnerCardService } from "@odit/lfk-client-js";
import store from "../../store";
import Toastify from "toastify-js";
import { DataHandler, Datatable, Th, ThFilter } from "@vincjo/datatables";
import CardsEmptyState from "./CardsEmptyState.svelte";
import CardDetailModal from "./CardDetailModal.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import ThFilterStatus from "./ThFilterStatus.svelte";
import ThFilterRunner from "./ThFilterRunner.svelte";
import InputElement from "../shared/InputElement.svelte";
import {
createSvelteTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
renderComponent,
} from "@tanstack/svelte-table";
import { writable } from "svelte/store";
import TableBottom from "../shared/TableBottom.svelte";
import TableActions from "../shared/TableActions.svelte";
import TableHeader from "../shared/TableHeader.svelte";
import CardStatus from "./CardStatus.svelte";
import CardRunner from "./CardRunner.svelte";
import { onMount } from "svelte";
import { runnerFilter, statusFilter } from "../shared/tablefilters";
import DeleteCardModal from "./DeleteCardModal.svelte";
export let edit_modal_open = false;
export let runner = {};
export let editable = {};
export let original_data = {};
export let current_cards = [];
const handler = new DataHandler(current_cards, { rowsPerPage: 50 });
const rows = handler.getRows();
$: active_deletes = [];
export const addCards = (cards) => {
console.log(cards)
current_cards = current_cards.concat(...cards);
options.update((options) => ({
...options,
data: current_cards,
}));
};
$: dataLoaded = false;
$: selected =
$table?.getSelectedRowModel().rows.map((row) => row.index) || [];
$: selectedCards =
$table?.getSelectedRowModel().rows.map((row) => row.original) || [];
$: active_delete = undefined;
$: cards_show = generate_cards.length > 0;
$: generate_cards = [];
const cards_promise = RunnerCardService.runnerCardControllerGetAll().then(
(val) => {
current_cards = val;
handler.setRows(val);
}
);
const getRunnerLabel = (option) =>
option?.firstname +
" " +
(option?.middlename || "") +
" " +
(option?.lastname || "{$_('non-blanko')}");
const columns = [
{
accessorKey: "code",
header: () => $_("code"),
filterFn: `includesString`,
},
{
accessorKey: "runner",
header: () => $_("runner"),
cell: (info) => {
return renderComponent(CardRunner, { runner: info.getValue() });
},
filterFn: `runner`,
},
{
accessorKey: "enabled",
cell: (info) => {
return renderComponent(CardStatus, { enabled: info.getValue() });
},
header: () => $_("status"),
filterFn: `status`,
},
{
accessorKey: "actions",
header: () => $_("action"),
cell: (info) => {
return renderComponent(TableActions, {
detailsAction: () => {
open_edit_modal(
current_cards[
current_cards.findIndex((r) => r.id == info.row.original.id)
]
);
},
deleteAction: () => {
active_delete =
current_cards[
current_cards.findIndex((r) => r.id == info.row.original.id)
];
},
deleteEnabled:
store.state.jwtinfo.userdetails.permissions.includes("CARD:DELETE"),
});
},
enableColumnFilter: false,
enableSorting: false,
},
];
const options = writable({
data: [],
columns: columns,
initialState: {
pagination: {
pageSize: 50,
},
},
filterFns: {
runner: runnerFilter,
status: statusFilter,
},
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const table = createSvelteTable(options);
function open_edit_modal(card) {
const getRunnerLabel = (option) =>
option.firstname +
" " +
(option.middlename || "") +
" " +
option.lastname;
if (card.runner?.id) {
runner = Object.assign(
{ runner },
@ -46,21 +140,61 @@
original_data = Object.assign(original_data, card);
edit_modal_open = true;
}
async function deleteCard(delete_card_id) {
await RunnerCardService.runnerCardControllerRemove(delete_card_id, true);
current_cards = current_cards.filter((r) => r.id !== delete_card_id);
options.update((options) => ({
...options,
data: current_cards,
}));
Toastify({
text: $_("card-deleted"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
onMount(() => {
RunnerCardService.runnerCardControllerGetAll().then((val) => {
current_cards = val;
options.update((options) => ({
...options,
data: current_cards,
}));
dataLoaded = true;
});
});
</script>
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:UPDATE")}
<CardDetailModal
bind:current_cards
bind:edit_modal_open
bind:runner
bind:editable
bind:original_data
on:dataUpdated={handler.setRows(current_cards)}
on:dataUpdated={(event) => {
current_cards[
current_cards.findIndex((c) => c.id === event.detail.card.id)
] = event.detail.card;
current_cards = current_cards;
options.update((options) => ({
...options,
data: current_cards,
}));
}}
/>
{/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")}
{#await cards_promise}
<DeleteCardModal
delete_card={active_delete}
modal_open={active_delete != undefined}
on:delete={(event) => {
deleteCard(event.detail.id);
}}
/>
{#if !dataLoaded}
<div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert"
@ -68,205 +202,99 @@
<p class="font-bold">{$_("loading-cards")}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div>
{:then}
{#if current_cards.length === 0}
<CardsEmptyState />
{:else}
<div class="h-12 mt-1">
{#if cards_show}
<button
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
on:click={async () => {
const prom = [];
for (const card of generate_cards) {
prom.push(
RunnerCardService.runnerCardControllerRemove(card.id, true)
);
}
await Promise.all(prom);
Toastify({
text: $_("cards-deleted"),
duration: 3500,
backgroundColor:
"linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
//TODO: Delete cards from table
}}
{:else if current_cards.length === 0}
<CardsEmptyState />
{:else}
<div class="h-12 mt-1">
{#if selected.length > 0}
<button
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
on:click={async () => {
const prom = [];
for (const card of selectedCards) {
prom.push(
await RunnerCardService.runnerCardControllerRemove(
card.id,
true
)
);
}
await Promise.all(prom);
for (const card of selectedCards) {
current_cards = current_cards.filter((r) => r.id !== card.id);
}
options.update((options) => ({
...options,
data: current_cards,
}));
$table.resetRowSelection()
}}
>
{$_("delete-cards")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
{$_("delete-cards")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
{/if}
<GenerateRunnerCards bind:cards_show bind:generate_cards />
</div>
<Datatable {handler}>
<table>
<thead>
<tr>
<th style="border-bottom: 1px solid #ddd;">
<input
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
{/if}
<GenerateRunnerCards cards_show={selected.length>0} bind:generate_cards={selectedCards} />
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
{#each $table.getHeaderGroups() as headerGroup}
<tr class="select-none">
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px">
<InputElement
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
checked={generate_cards.length == current_cards.length}
on:click={() => {
if (generate_cards.length != current_cards.length) {
generate_cards = current_cards;
} else {
generate_cards = [];
}
}}
checked={$table.getIsAllRowsSelected()}
indeterminate={$table.getIsSomeRowsSelected()}
on:change={() => $table.toggleAllRowsSelected()}
/>
</th>
<Th {handler} orderBy="code">{$_("code")}</Th>
<Th {handler} orderBy="runner">{$_("runner")}</Th>
<Th {handler} orderBy="status">{$_("status")}</Th>
<th style="border-bottom: 1px solid #ddd;">{$_("action")}</th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $table.getRowModel().rows as row}
<tr>
<th style="border-bottom: 1px solid #ddd;" />
<ThFilter {handler} filterBy="code" />
<ThFilterRunner {handler} />
<ThFilterStatus {handler} />
<th style="border-bottom: 1px solid #ddd;" />
</tr>
</thead>
<tbody>
{#each $rows as row}
<tr>
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px">
<InputElement
type="checkbox"
checked={row.getIsSelected()}
on:change={() => row.toggleSelected()}
/>
</td>
{#each row.getVisibleCells() as cell}
<td>
<input
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
checked={generate_cards.filter((i) => i.id == row.id)
.length > 0}
on:click={() => {
if (
generate_cards.findIndex((i) => i.id == row.id) == -1
) {
generate_cards.push(row);
generate_cards = generate_cards;
} else {
generate_cards = generate_cards.filter(
(r) => r.id != row.id
);
}
console.log(generate_cards);
}}
<svelte:component
this={flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
/>
</td>
<td>{row.code}</td>
<td>
{#if row.runner}
<a
href="../runners/{row.runner.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>{row.runner.firstname}
{row.runner.middlename || ""}
{row.runner.lastname}</a
>
{:else}{$_("non-blanko")}{/if}
</td>
<td>
{#if row.enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>{$_("enabled")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
>{$_("disabled")}</span
>
{/if}
</td>
<td>
{#if active_deletes[row.id] === true}
<button
on:click={() => {
active_deletes[row.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
>{$_("cancel-delete")}</button
>
<button
on:click={() => {
RunnerCardService.runnerCardControllerRemove(
row.id,
true
)
.then((resp) => {
current_cards = current_cards.filter(
(obj) => obj.id !== row.id
);
})
.catch((err) => {});
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("confirm-delete")}</button
>
{:else}
<button
on:click={() => {
open_edit_modal(row);
}}
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</button
>
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:DELETE")}
<button
on:click={() => {
active_deletes[row.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</Datatable>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_("general_promise_error")}</b>
{error}
</span>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
<TableBottom {table} {selected} />
{/if}
{/if}
<style>
table tbody {
display: block;
overflow-y: scroll;
}
table thead,
table tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
</style>

View File

@ -0,0 +1,128 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_card = {
id: 0,
code: "",
runner: {
firstname: "",
lastname: "",
},
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_card.id });
modal_open = false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="h-6 w-6 text-blue-600"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z"
/></svg
>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("confirm-delete")}
</h3>
<div class="mt-2 mb-6">
<p class="text-sm text-gray-500">
{$_("please-confirm-the-deletion-of-card")}
</p>
</div>
<div class="w-full">
{$_("card")} #{delete_card.code}<br />
<span class="inline-block">
{$_("runner")}:
{#if delete_card.runner}
<span class="inline-block"
>{delete_card.runner.firstname}
{delete_card.runner.lastname}</span
>
{:else}
{$_("non-blanko")}
{/if}</span
>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
on:click={submit}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
GroupContactService,
RunnerTeamService,
@ -145,7 +145,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -31,7 +31,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
DonationService,
DonorService,
@ -156,7 +156,7 @@ import { is_promise } from "svelte/internal";
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { DonationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
export let payment_modal_open = false;
@ -96,7 +96,7 @@
{#if payment_modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
payment_modal_open = false;

View File

@ -49,7 +49,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
DonorService
} from "@odit/lfk-client-js";
@ -134,7 +134,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { DonorService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -32,7 +32,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -51,7 +51,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
export let modal_open;
(function () {
document.onkeydown = function (e) {
@ -25,7 +25,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -2,7 +2,7 @@
import { _, getLocaleFromNavigator } from "svelte-i18n";
import marked from "marked";
import Footer from "./Footer.svelte";
import * as css from "../base/simple.css";
// import * as css from "../base/simple.css?inline";
let html = "";
async function load() {
let md = await fetch("/privacy_" + getLocaleFromNavigator() + ".md");

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import Toastify from "toastify-js";
import { UserGroupService } from "@odit/lfk-client-js";
export let modal_open;
@ -69,7 +69,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -30,7 +30,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerOrganizationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
export let modal_open;
@ -89,7 +89,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerOrganizationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -32,7 +32,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -51,7 +51,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div class="h-12">
<GenerateSponsoringContracts
bind:sponsoring_contracts_show

View File

@ -1,30 +0,0 @@
<script>
import { _, json } from "svelte-i18n";
import { getlang } from "./datatable_i18n";
import { Grid } from "gridjs";
//
let table;
const datatable = new Grid({
columns: ["Name", "Email", "Phone Number"],
language: getlang($json("datatable")),
sort: true,
search: { enabled: true },
data: [
["John", "john@example.com", "(353) 01 222 3333"],
["Mark", "mark@gmail.com", "(01) 22 888 4444"],
["Eoin", "eoin@gmail.com", "0097 22 654 00033"],
["Sarah", "sarahcdd@gmail.com", "+322 876 1233"],
["Afshin", "afshin@mail.com", "(353) 22 87 8356"],
],
pagination: {
enabled: true,
limit: 2,
summary: false,
},
});
setTimeout(() => {
datatable.render(table);
}, 0);
</script>
<div bind:this={table} />

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
RunnerService,
RunnerTeamService,
@ -11,8 +11,10 @@
import isMobilePhone from "validator/es/lib/isMobilePhone";
import Toastify from "toastify-js";
import Select from "svelte-select";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let modal_open;
export let current_runners;
$: selected_team = undefined;
let firstname_input;
let lastname_input;
@ -107,8 +109,7 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_runners.push(result);
current_runners = current_runners;
dispatch("created", { runners: [result] });
})
.catch((err) => {
//
@ -125,7 +126,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -0,0 +1,115 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_runner = {
id: 0,
firstname: "",
lastname: "",
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_runner.id });
modal_open=false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-6 w-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"
/></svg
>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("confirm-delete")}
</h3>
<div class="mt-2 mb-6">
<p class="text-sm text-gray-500">
{$_("please-confirm-the-deletion-of-runner")}
</p>
</div>
<div class="w-full">
<span class="inline-block"
>{delete_runner.firstname} {delete_runner.lastname}</span
>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
on:click={submit}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -3,7 +3,7 @@
import { read as readXlsx, utils as xlsx_utils } from "xlsx";
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import Toastify from "toastify-js";
import {
ImportService,
@ -16,7 +16,6 @@
export let passed_org;
export let passed_orgs;
export let passed_team;
export let current_runners;
export let import_modal_open;
$: searchvalue = "";
$: importButtonEnabled =
@ -169,7 +168,7 @@
mapped
)
.then((resp) => {
current_runners = current_runners.concat(resp);
dispatch("created", { runners: resp });
toast.hideToast();
recent_processed = true;
Toastify({
@ -198,7 +197,7 @@
mapped
)
.then((resp) => {
current_runners = current_runners.concat(resp);
dispatch("created", { runners: resp });
toast.hideToast();
recent_processed = true;
Toastify({
@ -228,7 +227,7 @@
{#if import_modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
cancelModal();

View File

@ -7,35 +7,43 @@
$: current_runners = [];
export let modal_open = false;
export let import_modal_open = false;
let addRunners;
</script>
<section class="container p-5">
<span class="mb-1 text-3xl font-extrabold leading-tight">
{$_('runners')}
{#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:CREATE')}
{$_("runners")}
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:CREATE")}
<button
on:click={() => {
modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('laeufer-hinzufuegen')}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("laeufer-hinzufuegen")}
</button>
<button
on:click={() => {
import_modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('import-runners')}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("import-runners")}
</button>
{/if}
</span>
<RunnersOverview bind:current_runners />
<RunnersOverview bind:current_runners bind:addRunners />
</section>
{#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:CREATE')}
<AddRunnerModal bind:current_runners bind:modal_open />
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:CREATE")}
<AddRunnerModal
bind:modal_open
on:created={(event) => {
addRunners(event.detail.runners);
}}
/>
<ImportRunnerModal
on:cancelDelete={(event) => {
import_modal_open = false;
@ -43,7 +51,10 @@
passed_team={{}}
passed_orgs={[]}
passed_org={{}}
bind:current_runners
opened_from="RunnerOverview"
bind:import_modal_open />
bind:import_modal_open
on:created={(event) => {
addRunners(event.detail.runners);
}}
/>
{/if}

View File

@ -1,41 +1,171 @@
<script>
import { _ } from "svelte-i18n";
import {
RunnerOrganizationService,
RunnerService,
RunnerTeamService,
RunnerOrganizationService,
} from "@odit/lfk-client-js";
import ThFilterGroup from "./ThFilterGroup.svelte";
import { DataHandler, Datatable, Th, ThFilter } from "@vincjo/datatables";
import {
createSvelteTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
renderComponent,
} from "@tanstack/svelte-table";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
import { writable } from "svelte/store";
import store from "../../store";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import { onMount } from "svelte";
$: active_deletes = [];
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import InputElement from "../shared/InputElement.svelte";
import TableActions from "../shared/TableActions.svelte";
import { groupFilter } from "../shared/tablefilters";
import DeleteRunnerModal from "./DeleteRunnerModal.svelte";
import Toastify from "toastify-js";
import TableBottom from "../shared/TableBottom.svelte";
import TableHeader from "../shared/TableHeader.svelte";
$: selectedRunners =
$table?.getSelectedRowModel().rows.map((row) => row.original) || [];
$: selected =
$table?.getSelectedRowModel().rows.map((row) => row.index) || [];
$: active_delete = undefined;
let dataLoaded = false;
let current_runners = [];
const handler = new DataHandler(current_runners, { rowsPerPage: 50 });
const rows = handler.getRows();
$: sponsoring_contracts_show = generate_runners.length > 0;
$: cards_show = generate_runners.length > 0;
$: certificates_show = generate_runners.length > 0;
$: generate_runners = []; //current_runners.filter((r) => r.selected === true);
export let current_runners = [];
$: sponsoring_contracts_show = selected.length > 0;
$: cards_show = selected.length > 0;
$: certificates_show = selected.length > 0;
$: teams = [];
$: orgs = [];
$: mappedteams = teams.map(function (g) {
return { value: g.id, label: g.parentGroup.name + " > " + g.name };
export const addRunners = (runners) => {
current_runners = current_runners.concat(...runners);
options.update((options) => ({
...options,
data: current_runners,
}));
};
//Section table
const columns = [
{
accessorKey: "id",
header: () => "id",
filterFn: `equalsString`,
},
{
accessorKey: "firstname",
header: () => $_("first-name"),
filterFn: `includesString`,
},
{
accessorKey: "middlename",
header: () => $_("middle-name"),
cell: (info) => {
if (!info || !info.getValue()) {
return "";
}
return info.getValue();
},
filterFn: `includesString`,
},
{
accessorKey: "lastname",
header: () => $_("last-name"),
filterFn: `includesString`,
},
{
accessorKey: "group",
header: () => $_("group"),
cell: (info) => {
const group = info.getValue();
if (group.responseType === "RUNNERORGANIZATION") {
return group.name;
}
return `${group.parentGroup.name} > ${group.name}`;
},
filterFn: `group`,
},
{
accessorKey: "distance",
header: () => $_("distance"),
cell: (info) => {
if (info.getValue() < 1000) {
return `${info.getValue()} m`;
}
return `${(info.getValue() / 1000).toFixed(1)} km`;
},
enableColumnFilter: false,
},
{
accessorKey: "actions",
header: () => $_("action"),
cell: (info) => {
return renderComponent(TableActions, {
detailsLink: `./${info.row.original.id}`,
deleteAction: () => {
active_delete =
current_runners[
current_runners.findIndex((r) => r.id == info.row.original.id)
];
},
deleteEnabled:
store.state.jwtinfo.userdetails.permissions.includes(
"RUNNER:DELETE"
),
});
},
enableColumnFilter: false,
enableSorting: false,
},
];
const options = writable({
data: [],
columns: columns,
filterFns: {
group: groupFilter,
},
initialState: {
pagination: {
pageSize: 50,
},
},
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
$: selectgroups = orgs
.map(function (g) {
return { value: g.id, label: g.name };
})
.concat(mappedteams);
const table = createSvelteTable(options);
async function deleteRunner(delete_runner_id) {
await RunnerService.runnerControllerRemove(delete_runner_id, true);
current_runners = current_runners.filter((r) => r.id !== delete_runner_id);
options.update((options) => ({
...options,
data: current_runners,
}));
Toastify({
text: $_("runner-deleted"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
onMount(() => {
RunnerService.runnerControllerGetAll().then((val) => {
current_runners = val;
dataLoaded = true;
handler.setRows(val);
options.update((options) => ({
...options,
data: current_runners,
}));
});
RunnerTeamService.runnerTeamControllerGetAll().then((val) => {
teams = val;
@ -48,6 +178,13 @@
});
</script>
<DeleteRunnerModal
delete_runner={active_delete}
modal_open={active_delete != undefined}
on:delete={(event) => {
deleteRunner(event.detail.id);
}}
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET")}
{#if !dataLoaded}
<div
@ -58,166 +195,65 @@
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div>
{:else}
<div class="h-12">
<div class="h-12 mt-2">
<GenerateSponsoringContracts
bind:sponsoring_contracts_show
bind:generate_runners
bind:generate_runners={selectedRunners}
/>
<GenerateRunnerCards
bind:cards_show
bind:generate_runners={selectedRunners}
/>
<GenerateRunnerCards bind:cards_show bind:generate_runners />
<GenerateRunnerCertificates
bind:certificates_show
bind:generate_runners
bind:generate_runners={selectedRunners}
/>
</div>
<Datatable {handler}>
<table>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr>
<th style="border-bottom: 1px solid #ddd;">
<input
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
checked={generate_runners.length == current_runners.length}
on:click={() => {
if (generate_runners.length != current_runners.length) {
generate_runners = current_runners;
} else {
generate_runners = [];
}
}}
/>
</th>
<Th {handler} orderBy="id">ID</Th>
<Th {handler} orderBy="firstname">First Name</Th>
<Th {handler} orderBy="middlename">Middle Name</Th>
<Th {handler} orderBy="lastname">Last Name</Th>
<th style="border-bottom: 1px solid #ddd;">Gruppe</th>
<Th {handler} orderBy="distance">Distanz</Th>
<th style="border-bottom: 1px solid #ddd;">{$_("action")}</th>
</tr>
<tr>
<th style="border-bottom: 1px solid #ddd;" />
<ThFilter {handler} filterBy="id" />
<ThFilter {handler} filterBy="firstname" />
<ThFilter {handler} filterBy="middlename" />
<ThFilter {handler} filterBy="lastname" />
<ThFilterGroup groups={selectgroups} {handler} />
<th style="border-bottom: 1px solid #ddd;" />
<th style="border-bottom: 1px solid #ddd;" />
</tr>
{#each $table.getHeaderGroups() as headerGroup}
<tr class="select-none">
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px">
<InputElement
type="checkbox"
checked={$table.getIsAllRowsSelected()}
indeterminate={$table.getIsSomeRowsSelected()}
on:change={() => $table.toggleAllRowsSelected()}
/>
</th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $rows as row}
{#each $table.getRowModel().rows as row}
<tr>
<td>
<input
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px">
<InputElement
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
checked={generate_runners.filter((i)=>i.id == row.id).length > 0}
on:click={() => {
if (
generate_runners.findIndex((i) => i.id == row.id) == -1
) {
generate_runners.push(row);
generate_runners = generate_runners;
} else {
generate_runners = generate_runners.filter(
(r) => r.id != row.id
);
}
console.log(generate_runners)
}}
checked={row.getIsSelected()}
on:change={() => row.toggleSelected()}
/>
</td>
<td>{row.id}</td>
<td>{row.firstname}</td>
<td>{row.middlename || ""}</td>
<td>{row.lastname}</td>
<td
>{#if row.group.responseType === "RUNNERTEAM"}
<a
href="../teams/{row.group.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>{row.group.parentGroup.name} &gt; {row.group.name}</a
>
{/if}
{#if row.group.responseType === "RUNNERORGANIZATION"}
<a
href="../orgs/{row.group.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>{row.group.name}</a
>
{/if}</td
>
<td>{row.distance / 1000} km</td>
<td>
{#if active_deletes[row.id] === true}
<button
on:click={() => {
active_deletes[row.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
>{$_("cancel-delete")}</button
>
<button
on:click={() => {
RunnerService.runnerControllerRemove(row.id, true)
.then((resp) => {
current_runners = current_runners.filter(
(obj) => obj.id !== row.id
);
})
.catch((err) => {});
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("confirm-delete")}</button
>
{:else}
<a
href="./{row.id}"
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:DELETE")}
<button
on:click={() => {
active_deletes[row.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}
{/if}
</td>
{#each row.getVisibleCells() as cell}
<td>
<svelte:component
this={flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</Datatable>
</div>
<div class="h-2" />
{/if}
{/if}
<style>
thead {
background: #fff;
}
thead {
position: sticky;
inset-block-start: 0;
}
tbody td {
padding: 4px;
}
tbody tr:nth-child(even) {
background: #fafafa;
}
tbody tr {
transition: all, 0.2s;
}
tbody tr:hover {
background: #f5f5f5;
}
</style>
<TableBottom {table} {selected} />

View File

@ -1,15 +1,15 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
RunnerService,
ScanService,
} from "@odit/lfk-client-js";
import Select from "svelte-select";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
export let modal_open;
export let current_scans;
const getRunnerLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const filterRunners = (label, filterText, option) =>
@ -18,6 +18,7 @@
function focus(el) {
el.focus();
}
const dispatch = createEventDispatcher();
$: runner = 0;
$: runners = [];
RunnerService.runnerControllerGetAll().then((val) => {
@ -63,8 +64,7 @@
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_scans.push(result);
current_scans = current_scans;
dispatch("created", { scans: [result] });
})
.catch((err) => {
//
@ -81,7 +81,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -0,0 +1,110 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_scan = {
id: 0,
runner: {
firstname: "",
lastname: "",
},
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_scan.id });
modal_open = false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-6 w-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"
/></svg
>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("confirm-delete")}
</h3>
<div class="mt-2 mb-6">
{$_("please-confirm-the-deletion-of-scan")} #{delete_scan.id}
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
on:click={submit}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,16 @@
<script>
import { _ } from "svelte-i18n";
export let valid = false;
</script>
{#if valid}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>{$_("valid")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
>{$_("invalid")}</span
>
{/if}

View File

@ -5,6 +5,7 @@
import ScansOverview from "./ScansOverview.svelte";
$: current_scans = [];
export let modal_open = false;
let addScans;
</script>
<section class="container p-5">
@ -21,9 +22,11 @@
</button>
{/if}
</span>
<ScansOverview bind:current_scans />
<ScansOverview bind:current_scans bind:addScans />
</section>
{#if store.state.jwtinfo.userdetails.permissions.includes('SCAN:CREATE')}
<AddScanModal bind:current_scans bind:modal_open />
<AddScanModal bind:modal_open on:created={(event)=>{
addScans(event.detail.scans)
}} />
{/if}

View File

@ -1,21 +1,54 @@
<script>
import { _ } from "svelte-i18n";
import { DataHandler, Datatable, Th, ThFilter } from "@vincjo/datatables";
import { ScanService, TrackService } from "@odit/lfk-client-js";
import store from "../../store";
import {
createSvelteTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
renderComponent,
} from "@tanstack/svelte-table";
import { onMount } from "svelte";
import { writable } from "svelte/store";
import Toastify from "toastify-js";
import TableBottom from "../shared/TableBottom.svelte";
import TableHeader from "../shared/TableHeader.svelte";
import ScansEmptyState from "./ScansEmptyState.svelte";
import ThFilterRunner from "./ThFilterRunner.svelte";
import ThFilterTrack from "./ThFilterTrack.svelte";
$: active_deletes = [];
import InputElement from "../shared/InputElement.svelte";
import TableActions from "../shared/TableActions.svelte";
import { runnerFilter, statusFilter } from "../shared/tablefilters";
import CardRunner from "../cards/CardRunner.svelte";
import ScanValid from "./ScanValid.svelte";
import DeleteScanModal from "./DeleteScanModal.svelte";
$: selectedScans =
$table?.getSelectedRowModel().rows.map((row) => row.original) || [];
$: selected =
$table?.getSelectedRowModel().rows.map((row) => row.index) || [];
$: active_delete = undefined;
export let current_scans = [];
const handler = new DataHandler(current_scans, { rowsPerPage: 20 });
const rows = handler.getRows();
export const addScans = (scans) => {
current_scans = current_scans.concat(...scans);
options.update((options) => ({
...options,
data: current_scans,
}));
};
const scans_promise = ScanService.scanControllerGetAll().then((val) => {
current_scans = val;
handler.setRows(val);
// handler.setRows(val);
current_scans = val;
options.update((options) => ({
...options,
data: current_scans,
}));
});
$: allTracks = [];
let allTracks = [];
TrackService.trackControllerGetAll().then((val) => {
allTracks = val;
});
@ -39,8 +72,118 @@
Math.floor(laptime / 60) * 60
}`;
}
const columns = [
{
accessorKey: "id",
header: () => "id",
filterFn: `equalsString`,
},
{
accessorKey: "runner",
header: () => $_("runner"),
cell: (info) => {
return renderComponent(CardRunner, { runner: info.getValue() });
},
filterFn: `runner`,
},
{
accessorKey: "lapTime",
header: () => $_("laptime"),
cell: (info) => {
return format_laptime(info.getValue());
},
enableColumnFilter: false,
},
{
accessorKey: "distance",
header: () => $_("distance"),
cell: (info) => {
if (info.getValue() < 1000) {
return `${info.getValue()}m`;
}
return `${(info.getValue() / 1000).toFixed(1)}km`;
},
enableColumnFilter: false,
},
{
accessorKey: "track",
header: () => $_("track"),
cell: (info) => {
const track = info.getValue();
return track?.name || "?";
},
enableColumnFilter: true,
},
{
accessorKey: "valid",
cell: (info) => {
return renderComponent(ScanValid, { valid: info.getValue() });
},
header: () => $_("status"),
filterFn: `status`,
},
{
accessorKey: "actions",
header: () => $_("action"),
cell: (info) => {
return renderComponent(TableActions, {
detailsLink: `./${info.row.original.id}`,
deleteAction: () => {
active_delete =
current_scans[
current_scans.findIndex((r) => r.id == info.row.original.id)
];
},
deleteEnabled:
store.state.jwtinfo.userdetails.permissions.includes("SCAN:DELETE"),
});
},
enableColumnFilter: false,
enableSorting: false,
},
];
const options = writable({
data: [],
columns: columns,
initialState: {
pagination: {
pageSize: 50,
},
},
filterFns: {
runner: runnerFilter,
status: statusFilter,
},
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const table = createSvelteTable(options);
async function deleteScan(scan_id) {
await ScanService.scanControllerRemove(scan_id, true);
current_scans = current_scans.filter((r) => r.id !== scan_id);
options.update((options) => ({
...options,
data: current_scans,
}));
Toastify({
text: $_("scan-deleted"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
</script>
<DeleteScanModal
delete_scan={active_delete}
modal_open={active_delete != undefined}
on:delete={(event) => {
deleteScan(event.detail.id);
}}
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")}
{#await scans_promise}
<div
@ -54,173 +197,94 @@
{#if current_scans.length === 0}
<ScansEmptyState />
{:else}
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll"
>
<Datatable {handler}>
<table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50">
<tr>
<Th {handler} orderBy="id">ID</Th>
<Th {handler}>
{$_("runner")}
</Th>
<Th {handler}>
{$_("distance")}
</Th>
<Th {handler}>
{$_("track")}
</Th>
<Th {handler}>
{$_("laptime")}
</Th>
<Th {handler}>
{$_("status")}
</Th>
<th
scope="col"
class="relative px-6 py-3"
style="border-bottom: 1px solid #ddd;"
>
{$_("action")}
{#if selected.length > 0}
<button
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
on:click={async () => {
const prom = [];
for (const scan of selectedScans) {
prom.push(ScanService.scanControllerRemove(scan.id, true));
}
await Promise.all(prom);
for (const scan of selectedScans) {
current_scans = current_scans.filter((r) => r.id !== scan.id);
}
options.update((options) => ({
...options,
data: current_scans,
}));
$table.resetRowSelection();
Toastify({
text: $_("scan-deleted"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}}
>
{$_("delete-scans")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
{/if}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
{#each $table.getHeaderGroups() as headerGroup}
<tr class="select-none">
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px">
<InputElement
type="checkbox"
checked={$table.getIsAllRowsSelected()}
indeterminate={$table.getIsSomeRowsSelected()}
on:change={() => $table.toggleAllRowsSelected()}
/>
</th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $table.getRowModel().rows as row}
<tr>
<ThFilter {handler} filterBy="id" />
<ThFilterRunner {handler} />
<th style="border-bottom: 1px solid #ddd;" />
<ThFilterTrack tracks={allTracks} {handler} />
<!-- <th style="border-bottom: 1px solid #ddd;" /> -->
<th style="border-bottom: 1px solid #ddd;" />
<th style="border-bottom: 1px solid #ddd;" />
<!-- TODO: filter status -->
<th style="border-bottom: 1px solid #ddd;" />
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px">
<InputElement
type="checkbox"
checked={row.getIsSelected()}
on:change={() => row.toggleSelected()}
/>
</td>
{#each row.getVisibleCells() as cell}
<td>
<svelte:component
this={flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
/>
</td>
{/each}
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each $rows as scan}
<tr data-rowid="scan_{scan.id}">
<td class="px-6 py-4 whitespace-nowrap text-left">
<div class="text-sm font-medium text-gray-900">
{scan.id}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<a
href="../runners/{scan.runner.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>{scan.runner.firstname}
{scan.runner.middlename || ""}
{scan.runner.lastname}</a
>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-left">
<div class="text-sm font-medium text-gray-900">
{#if scan.distance < 1000}
{scan.distance}m
{:else}{scan.distance / 1000}km{/if}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-left">
<div class="text-sm font-medium text-gray-900">
{#if scan.track}
<a
href="../tracks"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>{scan.track.name}
</a>
{/if}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-left">
{#if scan.responseType === "TRACKSCAN"}
<div class="text-sm font-medium text-gray-900">
{format_laptime(scan.lapTime)}
</div>
{:else}
<div class="text-sm font-medium text-gray-900">
{$_("scan-with-fixed-distance")}
</div>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if scan.valid}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>{$_("valid")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
>{$_("invalid")}</span
>
{/if}
</div>
</td>
{#if active_deletes[scan.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>
<button
on:click={() => {
active_deletes[scan.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
>{$_("cancel-delete")}</button
>
<button
on:click={() => {
ScanService.scanControllerRemove(scan.id, false).then(
(resp) => {
current_scans = current_scans.filter(
(obj) => obj.id !== scan.id
);
Toastify({
text: "Scan deleted",
duration: 500,
backgroundColor:
"linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
);
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("confirm-delete")}</button
>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>
<a
href="./{scan.id}"
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:DELETE")}
<button
on:click={() => {
active_deletes[scan.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
</Datatable>
{/each}
</tbody>
</table>
</div>
<TableBottom {table} {selected} />
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { ScanStationService, TrackService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import Select from "svelte-select";
@ -81,7 +81,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { ScanStationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -32,7 +32,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -1,6 +1,6 @@
<script>
import { _ } from "svelte-i18n";
import { focusTrap } from "svelte-focus-trap";
import Toastify from "toastify-js";
import { tick, createEventDispatcher } from "svelte";
export let copy_modal_open;
@ -46,7 +46,7 @@
{#if valueCopy != null}
<textarea bind:this={areaDom}>{valueCopy}</textarea>
{/if}
<div class="fixed z-10 inset-0 overflow-y-auto" use:focusTrap>
<div class="fixed z-10 inset-0 overflow-y-auto" >
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">

View File

@ -41,7 +41,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { MeService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -30,7 +30,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -0,0 +1,20 @@
<script>
let className = "";
export { className as class };
export let type;
</script>
<input
class={`border-1 border-stone-300 border rounded-md shadow ${className} ${
type === "checkbox" && "w-5 h-5 text-orange-400"
}`}
{type}
{...$$restProps}
on:click
on:change
on:keydown
on:keyup
on:mouseenter
on:mouseleave
/>

View File

@ -0,0 +1,26 @@
<script>
import { _ } from "svelte-i18n";
export let detailsLink;
export let detailsAction;
export let deleteEnabled;
export let deleteAction;
</script>
{#if detailsLink}
<a href={detailsLink} class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{:else if detailsAction}
<button on:click={detailsAction} class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</button
>
{/if}
{#if deleteEnabled}
<button
tabindex="0"
on:click={deleteAction}
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}

View File

@ -0,0 +1,71 @@
<script>
export let table;
export let selected;
</script>
<div class="flex items-center gap-2">
<button
class="border rounded p-1"
on:click={() => $table.setPageIndex(0)}
disabled={!$table.getCanPreviousPage()}
>
{"<<"}
</button>
<button
class="border rounded p-1"
on:click={() => $table.previousPage()}
disabled={!$table.getCanPreviousPage()}
>
{"<"}
</button>
<button
class="border rounded p-1"
on:click={() => $table.nextPage()}
disabled={!$table.getCanNextPage()}
>
{">"}
</button>
<button
class="border rounded p-1"
on:click={() => $table.setPageIndex($table.getPageCount() - 1)}
disabled={!$table.getCanNextPage()}
>
{">>"}
</button>
<span class="flex items-center gap-1">
<div>Page</div>
<strong>
{$table.getState().pagination.pageIndex + 1} of{" "}
{$table.getPageCount()}
</strong>
</span>
<span class="flex items-center gap-1">
| Go to page:
<input
type="number"
defaultValue={$table.getState().pagination.pageIndex + 1}
on:change={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
$table.setPageIndex(page);
}}
class="border p-1 rounded w-16"
/>
</span>
<select
value={$table.getState().pagination.pageSize}
on:input={(e) => {
const ps = Number(e.target.value);
console.log({ ps });
$table.setPageSize(Number(e.target.value));
}}
>
{#each [25, 50, 100, 250, 500] as pageSize}
<option value={pageSize}>{pageSize}</option>
{/each}
</select>
</div>
<!-- <pre>{JSON.stringify($table.getState(), null, 2)}</pre> -->
<div>
{Object.keys(selected).length} of{" "}
{$table.getPreFilteredRowModel().rows.length} Total Rows Selected
</div>

View File

@ -0,0 +1,57 @@
<script>
import { flexRender } from "@tanstack/svelte-table";
export let header;
</script>
<th class="cursor-pointer min-w-[5rem]">
{#if !header.isPlaceholder}
<button
class="w-full"
tabindex="0"
on:click={header.column.getToggleSortingHandler()}
>
<svelte:component
this={flexRender(header.column.columnDef.header, header.getContext())}
/>
{#if header.column
.getIsSorted()
.toString() == "asc" && header.column.getCanSort()}
🔼
{:else if header.column
.getIsSorted()
.toString() == "desc" && header.column.getCanSort()}
🔽
{/if}
</button>
{/if}
{#if header.column.getCanFilter()}
<div class="relative max-w-xs">
<input
title="name-search"
value={header.column.getFilterValue() || ""}
on:keyup={(e) => {
header.column.setFilterValue(e.target.value);
}}
type="text"
class="block w-full rounded-md border-gray-200 py-2 pl-8 text-xs focus:border-blue-500 focus:ring-blue-500"
placeholder=""
/>
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2"
>
<svg
class="h-3.5 w-3.5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
/>
</svg>
</div>
</div>
{/if}
</th>

View File

@ -0,0 +1,34 @@
export const groupFilter = (row, columnId, value) => {
const group = row.getValue(columnId);
if (group.responseType === "RUNNERORGANIZATION") {
return group.name.toLowerCase().includes(value.toLowerCase());
} else if (value.includes(">")) {
return (
`${group.parentGroup.name} > ${group.name}`.toLowerCase().includes(value.toLowerCase())
);
} else {
return (
group.name.toLowerCase().includes(value.toLowerCase()) ||
group.parentGroup.name.toLowerCase().includes(value.toLowerCase())
);
}
};
export const runnerFilter = (row, columnId, value) => {
const runner = row.getValue(columnId);
if(!runner && value == "blanko"){return true}
if(!runner){return false}
if(value.startsWith("#")){
return runner.id == value.replace("#","")
}
if(runner.middlename){
return `${runner.firstname} ${runner.middlename} ${runner.lastname}`.toLowerCase().includes(value.toLowerCase())
}
return `${runner.firstname} ${runner.lastname}`.toLowerCase().includes(value.toLowerCase())
};
export const statusFilter = (row, columnId, value) => {
const status = row.getValue(columnId);
return status.toString().includes(value);
};

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { StatsClientService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
export let modal_open;
@ -66,7 +66,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { ScanStationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -32,7 +32,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -1,6 +1,6 @@
<script>
import { _ } from "svelte-i18n";
import { focusTrap } from "svelte-focus-trap";
import Toastify from "toastify-js";
import { tick, createEventDispatcher } from "svelte";
export let copy_modal_open;
@ -46,7 +46,7 @@
{#if valueCopy != null}
<textarea bind:this={areaDom}>{valueCopy}</textarea>
{/if}
<div class="fixed z-10 inset-0 overflow-y-auto" use:focusTrap>
<div class="fixed z-10 inset-0 overflow-y-auto" >
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">

View File

@ -41,7 +41,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import {
RunnerOrganizationService,
RunnerTeamService,
@ -77,7 +77,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerTeamService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
@ -29,7 +29,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div

View File

@ -57,7 +57,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<div class="h-12">
<GenerateSponsoringContracts
bind:sponsoring_contracts_show

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { tracks as tracksstore } from "../../store.js";
import { TrackService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
@ -75,7 +75,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -1,7 +1,7 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { UserService } from "@odit/lfk-client-js";
import isEmail from "validator/es/lib/isEmail";
import Toastify from "toastify-js";
@ -92,7 +92,7 @@
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={() => {
modal_open = false;

View File

@ -36,7 +36,7 @@
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
class="mb-4" />
<!-- {/if} -->
<!-- <button
on:click={() => {

View File

@ -475,5 +475,12 @@
"you-must-create-at-least-one-card-or-cancel": "Du musst mindestens eine Blankokarte erstellen (oder abbrechen).",
"zip-postal-code": "Postleitzahl",
"delete-cards": "Karten löschen",
"cards-deleted": "Karten gelöscht"
"cards-deleted": "Karten gelöscht",
"please-confirm-the-deletion-of-runner": "Bitte bestätige die Löschung der Läufer:in",
"runner-deleted": "Läufer:in gelöscht",
"scan-deleted": "Scan gelöscht",
"please-confirm-the-deletion-of-scan": "Bitte bestätige die Löschung des Scans",
"please-confirm-the-deletion-of-card": "Bitte bestätige die Löschung der Karte",
"card": "Läuferkarte",
"delete-scans": "Scans löschen"
}

View File

@ -475,5 +475,12 @@
"you-must-create-at-least-one-card-or-cancel": "You must create at least one card (or cancel).",
"zip-postal-code": "ZIP/ postal code",
"delete-cards": "Delete cards",
"cards-deleted": "Cards deleted"
"cards-deleted": "Cards deleted",
"please-confirm-the-deletion-of-runner": "Please confirm the deletion of this runner",
"runner-deleted": "runner deleted",
"scan-deleted": "scan deleted",
"please-confirm-the-deletion-of-scan": "Please confirm the deletion of scan",
"please-confirm-the-deletion-of-card": "Please confirm the deletion of this card",
"card": "card",
"delete-scans": "Delete scans"
}

View File

@ -1,6 +1,6 @@
module.exports = {
mode: 'jit',
purge: [ './src/**/*.svelte' ],
content: [ './src/**/*.svelte' ],
theme: {
extend: {
colors: {

View File

@ -1,39 +1,11 @@
import svelte from '@sveltejs/vite-plugin-svelte';
import { minify } from 'html-minifier';
// vite.config.js
import { defineConfig } from 'vite';
//
const indexReplace = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return minify(html, {
collapseWhitespace: true
});
}
};
};
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
// base: './',
build: {
polyfillDynamicImport: false,
cssCodeSplit: false,
minify: isProduction,
target: ["es2020", "esnext", "edge88", "chrome87", "safari14"]
},
plugins: [
svelte({
//@ts-ignore
hot: !isProduction,
emitCss: true,
extensions: ['.md', '.svx', '.svelte'],
preprocess: [
//
]
}),
indexReplace()
]
};
export default defineConfig({
plugins: [
svelte({
/* plugin options */
})
]
});