Compare commits

...

2 Commits

Author SHA1 Message Date
d95b6cf589
chore(release): 1.13.0
All checks were successful
Build release images / build-container (push) Successful in 1m9s
2025-05-16 16:41:21 +02:00
51ba1c852c
feat(tools): Added tool for fast sponsoring creation 2025-05-16 16:40:58 +02:00
9 changed files with 1436 additions and 1077 deletions

View File

@ -2,8 +2,19 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.13.0](https://git.odit.services/lfk/frontend/compare/1.12.8...1.13.0)
- feat(tools): Basic mobile scanner [`500886e`](https://git.odit.services/lfk/frontend/commit/500886e4106f4b53fbc40fb0fa15653f574c8328)
- feat(tools): Added tool for fast sponsoring creation [`51ba1c8`](https://git.odit.services/lfk/frontend/commit/51ba1c852cad6243e935409da1eacecc5dcfa5fa)
- feat(dev): Enable devserver with https-support to circumvent ios https requirements for camera access [`25c38ea`](https://git.odit.services/lfk/frontend/commit/25c38ea3812a529a90294ff8834bdb65c487f8c4)
- feat(tools): Remove requirement for ten-diget codes [`80ca7aa`](https://git.odit.services/lfk/frontend/commit/80ca7aa08bdd44591e2d3efaa8e59dd4db5c864e)
- feat(dashboard): Added scanclient tool to dashboard [`06cfd60`](https://git.odit.services/lfk/frontend/commit/06cfd603cae79e0237bbece43203083f198d03a1)
#### [1.12.8](https://git.odit.services/lfk/frontend/compare/1.12.7...1.12.8) #### [1.12.8](https://git.odit.services/lfk/frontend/compare/1.12.7...1.12.8)
> 1 May 2025
- chore(release): 1.12.8 [`51d9b35`](https://git.odit.services/lfk/frontend/commit/51d9b35dc41fea0d0245fd136556f9fada3559da)
- feat(dasboard): Added section headers to main nav [`3a8533a`](https://git.odit.services/lfk/frontend/commit/3a8533a7baef02f7bc9780ce37be1a350bd92270) - feat(dasboard): Added section headers to main nav [`3a8533a`](https://git.odit.services/lfk/frontend/commit/3a8533a7baef02f7bc9780ce37be1a350bd92270)
- fic(locales): Updated dashboard translations [`5ac6fe3`](https://git.odit.services/lfk/frontend/commit/5ac6fe30b5b9e34043c734d51d5da137fdf7ac38) - fic(locales): Updated dashboard translations [`5ac6fe3`](https://git.odit.services/lfk/frontend/commit/5ac6fe30b5b9e34043c734d51d5da137fdf7ac38)
- feat(runners): Created_via filters can now be set via query params [`14501d3`](https://git.odit.services/lfk/frontend/commit/14501d3828dd0d48ba0baeeddf936ba275f7b9b7) - feat(runners): Created_via filters can now be set via query params [`14501d3`](https://git.odit.services/lfk/frontend/commit/14501d3828dd0d48ba0baeeddf936ba275f7b9b7)

View File

@ -13,7 +13,7 @@
<body> <body>
<span style="display: none; visibility: hidden" id="buildinfo" <span style="display: none; visibility: hidden" id="buildinfo"
>RELEASE_INFO-1.12.8-RELEASE_INFO</span >RELEASE_INFO-1.13.0-RELEASE_INFO</span
> >
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script src="/env.js"></script> <script src="/env.js"></script>

View File

@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.12.8", "version": "1.13.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"i18n-order": "node order.js", "i18n-order": "node order.js",
@ -52,6 +52,7 @@
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"localforage": "1.10.0", "localforage": "1.10.0",
"papaparse": "^5.5.2", "papaparse": "^5.5.2",
"svelecte": "3",
"svelte": "3.58.0", "svelte": "3.58.0",
"svelte-french-toast": "1.2.0", "svelte-french-toast": "1.2.0",
"svelte-i18n": "4.0.1", "svelte-i18n": "4.0.1",

15
pnpm-lock.yaml generated
View File

@ -38,6 +38,9 @@ importers:
papaparse: papaparse:
specifier: ^5.5.2 specifier: ^5.5.2
version: 5.5.2 version: 5.5.2
svelecte:
specifier: '3'
version: 3.17.3
svelte: svelte:
specifier: 3.58.0 specifier: 3.58.0
version: 3.58.0 version: 3.58.0
@ -1983,6 +1986,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
svelecte@3.17.3:
resolution: {integrity: sha512-wnvoRxJIFFkm+CmXgjL4R3i/TcuYUIBkE+jDJSBD7AdSOzk1K6u3+nW4zwxaGT29zyZpiZkWeiy7lO62r5F+tg==}
svelte-french-toast@1.2.0: svelte-french-toast@1.2.0:
resolution: {integrity: sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==} resolution: {integrity: sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==}
peerDependencies: peerDependencies:
@ -2004,6 +2010,9 @@ packages:
svelte-select@3.17.0: svelte-select@3.17.0:
resolution: {integrity: sha512-ITmX/XUiSdkaILmsTviKRkZPaXckM5/FA7Y8BhiUPoamaZG/ZDyOo6ydjFu9fDVFTbwoAUGUi6HBjs+ZdK2AwA==} resolution: {integrity: sha512-ITmX/XUiSdkaILmsTviKRkZPaXckM5/FA7Y8BhiUPoamaZG/ZDyOo6ydjFu9fDVFTbwoAUGUi6HBjs+ZdK2AwA==}
svelte-tiny-virtual-list@2.1.2:
resolution: {integrity: sha512-jeP/WMvgFUR4mYXHGPiCexjX5DuzSO+3xzHNhxfcsFyy+uYPtnqI5UGb383swpzQAyXB0OBqYfzpYihD/5gxnA==}
svelte-writable-derived@3.1.1: svelte-writable-derived@3.1.1:
resolution: {integrity: sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==} resolution: {integrity: sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==}
peerDependencies: peerDependencies:
@ -3946,6 +3955,10 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svelecte@3.17.3:
dependencies:
svelte-tiny-virtual-list: 2.1.2
svelte-french-toast@1.2.0(svelte@3.58.0): svelte-french-toast@1.2.0(svelte@3.58.0):
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.58.0
@ -3968,6 +3981,8 @@ snapshots:
svelte-select@3.17.0: {} svelte-select@3.17.0: {}
svelte-tiny-virtual-list@2.1.2: {}
svelte-writable-derived@3.1.1(svelte@3.58.0): svelte-writable-derived@3.1.1(svelte@3.58.0):
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.58.0

View File

@ -72,6 +72,7 @@
import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte"; import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte";
import CardReplacement from "./components/tools/CardReplacement.svelte"; import CardReplacement from "./components/tools/CardReplacement.svelte";
import ScanClient from "./components/tools/ScanClient.svelte"; import ScanClient from "./components/tools/ScanClient.svelte";
import DonationCreate from "./components/tools/DonationCreate.svelte";
store.init(); store.init();
</script> </script>
@ -144,6 +145,9 @@
<Route path="/scanclient/"> <Route path="/scanclient/">
<ScanClient /> <ScanClient />
</Route> </Route>
<Route path="/donationcreate/">
<DonationCreate />
</Route>
</Route> </Route>
<Route path="/teams/*"> <Route path="/teams/*">
<Route path="/"> <Route path="/">

View File

@ -105,6 +105,26 @@
<span>{$_("scanclient")}</span> <span>{$_("scanclient")}</span>
</a> </a>
<a
class:activenav={$router.path.includes("/tools/donationcreate/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
href="/tools/donationcreate/"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
>
<path
fill-rule="evenodd"
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z"
clip-rule="evenodd"
/>
</svg>
<span>{$_("donation-quick-add")}</span>
</a>
<h2 class="px-4 py-2 text-xs font-semibold text-gray-600 uppercase"> <h2 class="px-4 py-2 text-xs font-semibold text-gray-600 uppercase">
{$_("management")} {$_("management")}
</h2> </h2>

View File

@ -0,0 +1,300 @@
<script>
import { _ } from "svelte-i18n";
import {
DonationService,
DonorService,
RunnerService,
} from "@odit/lfk-client-js";
import Svelecte from "svelecte";
import Select from "svelte-select";
import toast from "svelte-french-toast";
let runners = [];
let donors = [];
let runnerinfo = { id: 0, firstname: "", lastname: "" };
let donorinfo = { id: 0, firstname: "", lastname: "" };
let address = {
address1: "",
address2: "",
city: "",
postalcode: "",
country: "Germany",
};
let amount = 0;
let lastname = "";
let address_checked = false;
RunnerService.runnerControllerGetAll()
.then((val) => {
runners = val.map((r) => {
return { label: getRunnerLabel(r), value: r };
});
})
.catch((err) => {
console.log("error fetching runners:", err);
});
function loadDonors() {
DonorService.donorControllerGetAll()
.then((val) => {
donors = val.map((r) => {
return { label: getDonorlabel(r), value: r };
});
console.log("refreshed donors");
setTimeout(() => {
loadDonors;
}, 30000);
})
.catch((err) => {
console.log("error fetching donors:", err);
});
}
loadDonors();
const getRunnerLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const getDonorlabel = (option) => `${option.firstname} (${option.lastname})`;
const filterRunners = (label, filterText, option) => {
if (filterText.startsWith("#")) {
return option.value.id == parseInt(filterText.replace("#", ""));
}
return (
label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.toString().startsWith(filterText.toLowerCase())
);
};
function resetAll() {
runnerinfo = { id: 0, firstname: "", lastname: "" };
donorinfo = { id: 0, firstname: "", lastname: "" };
amount = 0;
}
</script>
<div class="p-4">
<h3 class="text-3xl font-bold">{$_("fast_donation_create")}</h3>
<!-- -->
<div class="grid grid-cols-6 gap-4">
<div class="col-span-2">
<h4 class="text-xl font-semibold">
{$_("runner")}
</h4>
<Select
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) =>
filterRunners(label, filterText, option)}
items={runners}
showChevron={true}
placeholder={$_("search-for-runner-by-name-or-id")}
noOptionsMessage={$_("no-runners-found")}
on:select={(selectedValue) => {
runnerinfo = selectedValue.detail.value;
}}
on:clear={() => (runnerinfo.runner = null)}
/>
</div>
<div class="col-span-2">
<h4 class="text-xl font-semibold">
{$_("donor")}
</h4>
<div class="mb-2">
<Svelecte
name="donor_fistname"
placeholder={$_("first-name")}
clearable={true}
options={donors}
keepCreated={false}
creatable={true}
labelField="label"
on:change={(e) => {
if (!e.detail?.value) {
donorinfo = { id: 0, firstname: "", lastname: "" };
return;
}
if (!e.detail?.$created) {
donorinfo = e.detail.value;
lastname = e.detail.value.lastname;
} else {
console.log("created option", e);
donorinfo.firstname = e.detail.value;
}
}}
class="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-0.5"
/>
<input
autocomplete="off"
placeholder={$_("last-name")}
class:border-red-500={donorinfo.lastname?.length == 0}
class:focus:border-red-500={donorinfo.lastname?.length == 0}
class:focus:ring-red-500={donorinfo.lastname?.length == 0}
bind:value={lastname}
on:input={e => {
donorinfo.lastname = e.target.value;
}}
type="text"
name="lastname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
{#if donorinfo.id == 0}
<input
bind:checked={address_checked}
id="comments"
name="comments"
type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded"
/>
{:else}
<input
checked={true}
disabled
id="comments"
name="comments"
type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded"
/>
{/if}
</div>
<div class="ml-3 text-sm">
<label for="comments" class="font-semibold text-gray-700"
>{$_("receipt-needed")}</label
>
</div>
</div>
{#if address_checked}
<div class="col-span-6">
<label for="address1" class="block text-sm font-medium text-gray-700"
>{$_("address")}</label
>
<input
autocomplete="off"
placeholder="Address"
class:border-red-500={address.address1.length == 0}
class:focus:border-red-500={address.address1.length == 0}
class:focus:ring-red-500={address.address1.length == 0}
bind:value={address.address1}
type="text"
name="address1"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
</div>
<div class="col-span-6">
<label for="address2" class="block text-sm font-medium text-gray-700"
>{$_("apartment-suite-etc")}</label
>
<input
autocomplete="off"
placeholder={$_("apartment-suite-etc")}
bind:value={address.address2}
type="text"
name="address2"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
</div>
<div class="col-span-6">
<label for="zipcode" class="block text-sm font-medium text-gray-700"
>{$_("zip-postal-code")}</label
>
<input
autocomplete="off"
placeholder={$_("zip-postal-code")}
class:border-red-500={address.postalcode.length == 0}
class:focus:border-red-500={address.postalcode.length == 0}
class:focus:ring-red-500={address.postalcode.length == 0}
bind:value={address.postalcode}
type="text"
name="zipcode"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
</div>
<div class="col-span-6">
<label for="city" class="block text-sm font-medium text-gray-700"
>City</label
>
<input
autocomplete="off"
placeholder="City"
class:border-red-500={address.city.length == 0}
class:focus:border-red-500={address.city.length == 0}
class:focus:ring-red-500={address.city.length == 0}
bind:value={address.city}
type="text"
name="city"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
</div>
{/if}
</div>
<div>
<h4 class="text-xl font-semibold">
{$_("amount-per-kilometer")}
</h4>
<div class="mt-1 flex rounded-md shadow-sm">
<input
autocomplete="off"
class:border-red-500={!amount > 0}
class:focus:border-red-500={!amount > 0}
class:focus:ring-red-500={!amount > 0}
bind:value={amount}
type="number"
step="0.01"
name="donation_amount_eur"
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 p-2"
placeholder="2.00"
/>
<span
class="inline-flex items-center px-3 rounded-r-md border border-neutral-300 bg-neutral-50 text-neutral-500 text-sm"
>€</span
>
</div>
</div>
<div>
<h4 class="text-xl font-semibold">
{$_("confirm")}
</h4>
<button
disabled={amount <= 0 ||
runnerinfo.id == 0 ||
(donorinfo.firstname.length == 0 || donorinfo.lastname.length == 0)}
class="py-2 px-4 text-center inline-flex items-center text-md font-medium rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 focus:outline-hidden focus:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-500 dark:bg-blue-800/30 dark:hover:bg-blue-800/20 dark:focus:bg-blue-800/20"
on:click={async () => {
toast.loading($_("creating-donation"));
if (donorinfo.id == 0) {
if (!address_checked) {
address = null
}
donorinfo = await DonorService.donorControllerPost({
firstname: donorinfo.firstname,
lastname: lastname,
receiptNeeded: address_checked,
address: address,
});
loadDonors();
}
await DonationService.donationControllerPostDistance({
amountPerDistance: amount*100,
runner: runnerinfo.id,
donor: donorinfo.id,
});
toast.dismiss();
toast.success($_("donation-created"));
resetAll();
}}>{$_("create")}</button
>
{amount <= 0 ||
runnerinfo.id == 0 ||
(donorinfo.firstname.length == 0 && donorinfo.lastname.length == 0)}
{amount} - {runnerinfo.id} - {donorinfo.id} - {donorinfo.firstname} - {donorinfo.lastname}
</div>
</div>
</div>
<style>
:global(:root) {
--sv-bg: #ffffff;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff