Merge pull request #193 from EETagent/frontend_prod_form

Frontend prod form
This commit is contained in:
Sebastian Pravda 2023-02-05 16:33:24 +01:00 committed by GitHub
commit dce07ce2cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 3115 additions and 131 deletions

View file

@ -54,7 +54,6 @@ export const apiFetchSubmissionProgress = async (fetchSsr?: Fetch): Promise<Subm
export const apiWhoami = async (fetchSsr?: Fetch): Promise<BaseCandidate> => { export const apiWhoami = async (fetchSsr?: Fetch): Promise<BaseCandidate> => {
const apiFetch = fetchSsr || fetch; const apiFetch = fetchSsr || fetch;
try { try {
console.log(API_URL + '/candidate/whoami');
const res = await apiFetch(API_URL + '/candidate/whoami', { const res = await apiFetch(API_URL + '/candidate/whoami', {
method: 'GET', method: 'GET',
credentials: 'include' credentials: 'include'
@ -100,7 +99,6 @@ export const apiFillDetails = async (data: CandidateData): Promise<CandidateData
}); });
} }
console.log(data);
try { try {
const res = await axios.post(API_URL + '/candidate/details', data, { withCredentials: true }); const res = await axios.post(API_URL + '/candidate/details', data, { withCredentials: true });
return res.data; return res.data;

View file

@ -0,0 +1,195 @@
[
"Česká republika",
"Slovenská republika",
"Ukrajina",
"Afghánistán",
"Albánie",
"Alžírsko",
"Andorra",
"Angola",
"a Antigua Barbuda",
"Argentina",
"Arménie",
"Austrálie",
"Ázerbájdžán",
"Bahamy",
"Bahrajn",
"Bangladéš",
"Barbados",
"Belgie",
"Belize",
"Bělorusko",
"Benin",
"Bhútán",
"Bolívie",
"Hercegovina a Bosna",
"Botswana",
"Brazílie",
"Brunej",
"Bulharsko",
"Faso Burkina",
"Burundi",
"Čad",
"Černá Hora",
"Čína",
"Dánsko",
"Dominika",
"republika Dominikánská",
"Džibutsko",
"Egypt",
"Ekvádor",
"Eritrea",
"Estonsko",
"Etiopie",
"Fidži",
"Filipíny",
"Finsko",
"Francie",
"Gabon",
"Gambie",
"Ghana",
"Grenada",
"Gruzie",
"Guatemala",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Chile",
"Chorvatsko",
"Indie",
"Indonésie",
"Irák",
"Írán",
"Irsko",
"Island",
"Itálie",
"Izrael",
"Jamajka",
"Japonsko",
"Jemen",
"Jižní Afrika",
"Jižní Korea",
"Jižní Súdán",
"Jordánsko",
"Kambodža",
"Kamerun",
"Kanada",
"Kapverdy",
"Katar",
"Kazachstán",
"Keňa",
"Kiribati",
"Kolumbie",
"Komory",
"Konžská republika",
"Konžská demokratická republika",
"Kostarika",
"Kuba",
"Kuvajt",
"Kypr",
"Kyrgyzstán",
"Laos",
"Lesotho",
"Libanon",
"Libérie",
"Libye",
"Lichtenštejnsko",
"Litva",
"Lotyšsko",
"Lucembursko",
"Madagaskar",
"Maďarsko",
"Malajsie",
"Malawi",
"Maledivy",
"Mali",
"Malta",
"Maroko",
"Marshallovy ostrovy",
"Mauricius",
"Mauritánie",
"Mexiko",
"Mikronésie",
"Moldavsko",
"Monako",
"Mongolsko",
"Mosambik",
"Myanmar (Barma)",
"Namibie",
"Nauru",
"Německo",
"Nepál",
"Niger",
"Nigérie",
"Nikaragua",
"Nizozemsko",
"Norsko",
"Zéland Nový",
"Omán",
"Pákistán",
"Palau",
"Panama",
"Papua Nová Guinea",
"Paraguay",
"Peru",
"Pobřeží slonoviny",
"Polsko",
"Portugalsko",
"Rakousko",
"Rovníková Guinea",
"Rumunsko",
"Rusko",
"Rwanda",
"Řecko",
"Salvador",
"Samoa",
"Marino San",
"Saúdská Arábie",
"Senegal",
"Korea Severní",
"Makedonie Severní",
"Seychely",
"Sierra Leone",
"Singapur",
"Slovinsko",
"Somálsko",
"arabské emiráty Spojené",
"království Spojené",
"americké státy Spojené",
"Srbsko",
"republika Středoafrická",
"Súdán",
"Surinam",
"Lucie Svatá",
"a Nevis Svatý Kryštof",
"Princův Svatý a ostrov Tomáš",
"Vincenc a Grenadiny Svatý",
"Svazijsko",
"Sýrie",
"Šalomounovy ostrovy",
"Španělsko",
"Šrí",
"Švédsko",
"Švýcarsko",
"Tádžikistán",
"Tanzanie",
"Thajsko",
"Togo",
"Tonga",
"a Trinidad Tobago",
"Tunisko",
"Turecko",
"Turkmenistán",
"Tuvalu",
"Uganda",
"Uruguay",
"Uzbekistán",
"Vanuatu",
"Venezuela",
"Vietnam",
"Timor Východní",
"Zambie",
"Zimbabwe"
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { apiGetCandidatePortfolio, apiResetCandidatePassword } from '$lib/@api/admin'; import { apiGetCandidatePortfolio, apiResetCandidatePassword } from '$lib/@api/admin';
import type { CandidateData } from '$lib/stores/candidate'; import type { CandidateData } from '$lib/stores/candidate';
import { SvelteToast, toast } from '@zerodevx/svelte-toast';
export let id: number; export let id: number;
export let candidateData: CandidateData; export let candidateData: CandidateData;
@ -13,7 +14,13 @@
const res = await apiResetCandidatePassword(id); const res = await apiResetCandidatePassword(id);
alert('Nove heslo: ' + res.password); alert('Nove heslo: ' + res.password);
} catch { } catch {
console.log('error'); toast.push('Rodné číslo neodpovídá oficiální specifikaci či datumu narození', {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
} }
} }
@ -27,11 +34,18 @@
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} catch (e) { } catch (e) {
console.log(e); toast.push('Rodné číslo neodpovídá oficiální specifikaci či datumu narození', {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
} }
} }
</script> </script>
<SvelteToast />
<div class="flex h-screen w-full items-center justify-center"> <div class="flex h-screen w-full items-center justify-center">
<div class="mr-8 max-w-sm"> <div class="mr-8 max-w-sm">
<div class="rounded-lg bg-white p-10 shadow-xl"> <div class="rounded-lg bg-white p-10 shadow-xl">

View file

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let enterAllowed: boolean;
export let value: string; export let value: string;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') { if (enterAllowed && e.key === 'Enter') {
dispatch('click'); dispatch('click');
} }
}; };

View file

@ -15,6 +15,7 @@
import { baseCandidateData, candidateData } from '$lib/stores/candidate'; import { baseCandidateData, candidateData } from '$lib/stores/candidate';
import tippy, { sticky } from 'tippy.js'; import tippy, { sticky } from 'tippy.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { pushErrorText } from '$lib/utils/toast';
export let title: string; export let title: string;
export let status: Status; export let status: Status;
@ -62,7 +63,7 @@
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} catch (e) { } catch (e) {
console.log(e); pushErrorText("Chyba při stahování portfolia");
} }
}; };

View file

@ -31,12 +31,10 @@
$: if ($submissionProgress) { $: if ($submissionProgress) {
status = getStatus(); status = getStatus();
// console.log('type' + fileType + ' status: ' + status);
fileDropped = status === 'uploaded' || status === 'submitted'; fileDropped = status === 'uploaded' || status === 'submitted';
} }
const getStatus = (): Status => { const getStatus = (): Status => {
console.log($submissionProgress);
switch ($submissionProgress.status) { switch ($submissionProgress.status) {
case UploadStatus.None: case UploadStatus.None:
return 'missing'; return 'missing';
@ -71,7 +69,6 @@
}; };
const onFileDrop = (dropped: Files) => { const onFileDrop = (dropped: Files) => {
console.log(dropped);
if (dropped.accepted.length > 0) { if (dropped.accepted.length > 0) {
fileDropped = true; fileDropped = true;
const file = dropped.accepted[0]; const file = dropped.accepted[0];
@ -79,7 +76,6 @@
dispatch('filedrop', { dispatch('filedrop', {
file: file, file: file,
callback: (progressEvent: AxiosProgressEvent) => { callback: (progressEvent: AxiosProgressEvent) => {
console.log(progressEvent.bytes);
progress = progressEvent.progress!; progress = progressEvent.progress!;
bytesTotal = progressEvent.total ?? 0; bytesTotal = progressEvent.total ?? 0;
} }

View file

@ -9,8 +9,15 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let grade: Grade; export let grade: Grade;
const SEMESTERS: Semester[] = ['1/8', '2/8', '1/9', '2/9']; const SEMESTERS: Semester[] = ['1/8', '2/8', '1/9', '2/9'];
const deleteRow = () => {
dispatch('delete');
}
</script> </script>
<div class="flex"> <div class="flex">
@ -25,6 +32,23 @@
<option value="5">5</option> <option value="5">5</option>
</select> </select>
{/each} {/each}
<!-- delete button with 'x' icon -->
<button on:click={deleteRow} class="ml-0.5 h-6 w-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 stroke-red-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
<style lang="postcss"> <style lang="postcss">

View file

@ -78,8 +78,8 @@
}; };
</script> </script>
<div class="mx-auto mt-8 flex text-gray-400 lg:w-4/5"> <div class="mx-auto mt-8 flex pr-6 text-gray-400 lg:w-4/5">
<span class="w-1/2 text-center">Známky</span> <span class="w-1/2 text-center">Předmět</span>
<span class="ml-0.5 w-1/6 text-center">1/8</span> <span class="ml-0.5 w-1/6 text-center">1/8</span>
<span class="ml-0.5 w-1/6 text-center">2/8</span> <span class="ml-0.5 w-1/6 text-center">2/8</span>
<span class="ml-0.5 w-1/6 text-center">1/9</span> <span class="ml-0.5 w-1/6 text-center">1/9</span>
@ -92,6 +92,10 @@
on:keyup={convertGradeToGradeBackend} on:keyup={convertGradeToGradeBackend}
on:change={convertGradeToGradeBackend} on:change={convertGradeToGradeBackend}
bind:grade={gradesLocal[i]} bind:grade={gradesLocal[i]}
on:delete={() => {
grades = grades.filter((grade) => grade.subject !== gradesLocal[i].subject);
gradesLocal = gradesLocal.filter((_, index) => index !== i);
}}
/> />
</div> </div>
{/each} {/each}

View file

@ -1,19 +1,33 @@
<script lang="ts"> <script lang="ts">
import LL from '$i18n/i18n-svelte'; import LL from '$i18n/i18n-svelte';
import schoollistString from '$lib/assets/schoollist.txt?raw';
import School from './School.svelte'; import School from './School.svelte';
import type { School as SchoolType } from '$lib/stores/candidate'; import type { School as SchoolType, SchoolJson } from '$lib/stores/candidate';
import SelectField from '../SelectField.svelte';
import TextField from '$lib/components/textfield/TextField.svelte';
const schoolList: Array<string> = schoollistString.split(';'); export let schoolNames: Array<string>;
export let schoolList: Array<SchoolJson>;
let fields: Array<string> = [];
let filteredSchools: Array<string> = []; let filteredSchools: Array<string> = [];
const filterSchools = () => { const filterSchools = () => {
let storageArr: Array<string> = []; let storageArr: Array<string> = [];
if (schoolNameInputValue) { if (schoolNameInputValue) {
schoolList.forEach((school) => { schoolNames.forEach((school) => {
if (school.toLowerCase().startsWith(schoolNameInputValue.toLowerCase())) { if (
school
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.includes(
schoolNameInputValue
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
)
) {
storageArr = [...storageArr, makeMatchBold(school)]; storageArr = [...storageArr, makeMatchBold(school)];
} }
}); });
@ -26,17 +40,30 @@
let schoolNameInputValue = ''; let schoolNameInputValue = '';
let schoolFieldInputValue = ''; let schoolFieldInputValue = '';
let fieldFocusInputValue = '';
$: if (!schoolNameInputValue) { $: if (!schoolNameInputValue) {
filteredSchools = []; filteredSchools = [];
hiLiteIndex = -1; hiLiteIndex = -1;
} }
const setFields = (schoolName: string) => {
let school = schoolList.find((school) => school.n === schoolName);
if (school) {
fields = school.f;
} else {
fields = [];
}
};
$: setFields(schoolNameInputValue);
const setInputVal = (schoolName: string) => { const setInputVal = (schoolName: string) => {
schoolNameInputValue = removeBold(schoolName); schoolNameInputValue = removeBold(schoolName);
filteredSchools = []; filteredSchools = [];
hiLiteIndex = -1; hiLiteIndex = -1;
searchInput.focus(); searchInput.focus();
// setFields(schoolNameInputValue);
}; };
const makeMatchBold = (str: string) => { const makeMatchBold = (str: string) => {
@ -76,33 +103,67 @@
export let selectedSchool: SchoolType; export let selectedSchool: SchoolType;
export let error: string = ''; export let error: string = '';
schoolFieldInputValue = selectedSchool.field; if (selectedSchool.field.split(';').length > 1) {
console.log(selectedSchool.field);
schoolFieldInputValue = selectedSchool.field.split(';')[0];
fieldFocusInputValue = selectedSchool.field.split(';')[1];
} else {
schoolFieldInputValue = selectedSchool.field;
}
schoolNameInputValue = selectedSchool.name; schoolNameInputValue = selectedSchool.name;
$: selectedSchool.field = schoolFieldInputValue; $: selectedSchool.field = schoolFieldInputValue + (fieldFocusInputValue ? `;${fieldFocusInputValue}` : '');
$: selectedSchool.name = schoolNameInputValue; $: selectedSchool.name = schoolNameInputValue;
let isSSPS = false;
$: isSSPS = schoolNameInputValue === 'Smíchovská střední průmyslová škola a gymnázium';
</script> </script>
<svelte:window on:keydown={navigateList} /> <svelte:window on:keydown={navigateList} />
<div class="autocomplete"> <div class="autocomplete">
<div class="flex"> <div class="flex flex-col">
<input <input
class:error class:error
class="flex-1" class=""
type="text" type="text"
bind:this={searchInput} bind:this={searchInput}
bind:value={schoolNameInputValue} bind:value={schoolNameInputValue}
on:input={filterSchools} on:input={filterSchools}
placeholder={$LL.input.schoolName()} placeholder={$LL.input.schoolName()}
/> />
<input <div class="flex mt-2">
<span class="w-1/2" class:w-full={isSSPS}>
<SelectField
on:focus={() => setFields(schoolNameInputValue)}
bind:value={schoolFieldInputValue}
options={fields}
placeholder={$LL.input.fieldOfStudy()}
/>
</span>
<span class="w-1/2 ml-2" class:hidden={isSSPS}>
<TextField
bind:value={fieldFocusInputValue}
placeholder="Zaměření (jen některé školy)"
helperText="Např. Kybernetická bezpečnost, protože obor nemá svůj vlastní kód"
/>
</span>
</div>
<!-- <select
on:focus={() => setFields(schoolNameInputValue)}
>
{#each fields as field}
<option>{field}</option>
{/each}
</select> -->
<!-- <input
on:focus={() => setFields(schoolNameInputValue)}
class:error class:error
class="ml-2 w-2/5" class="mt-4"
type="text" type="text"
bind:value={schoolFieldInputValue} bind:value={schoolFieldInputValue}
placeholder={$LL.input.fieldOfStudy()} placeholder={$LL.input.fieldOfStudy()}
/> /> -->
</div> </div>
{#if filteredSchools.length > 0} {#if filteredSchools.length > 0}
<ul bind:this={optionsList} class="schoolAutocompleteList"> <ul bind:this={optionsList} class="schoolAutocompleteList">

View file

@ -20,7 +20,6 @@
if (number !== null && number !== undefined) { if (number !== null && number !== undefined) {
country = number.country!; country = number.country!;
} }
// console.log(country);
} }
// Validity // Validity

View file

@ -1,6 +1,10 @@
import type { GradeBackend } from '$lib/components/grades/GradesTable.svelte'; import type { GradeBackend } from '$lib/components/grades/GradesTable.svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export interface SchoolJson {
n: string;
f: string[];
}
export interface School { export interface School {
name: string; name: string;
field: string; field: string;

View file

@ -14,7 +14,7 @@ export const isPersonalIdNumberValid = (personalIdNumber: string): boolean => {
} }
}; };
export const isPersonalIdNumberWithBirthdateValid = ( export const isPersonalIdMatchingBirthdate = (
personalIdNumber: string, personalIdNumber: string,
birthdate: string birthdate: string
): boolean => { ): boolean => {
@ -48,25 +48,26 @@ export const isPersonalIdNumberWithBirthdateValid = (
} }
}; };
export const deriveBirthdateFromPersonalId = ( export const parseBirthdateSexFromPersonalId = (
personalIdNumber: string personalIdNumber: string
): [birthdate: string, sex: 'MUŽ' | 'ŽENA'] => { ): [birthdate: string, sex: 'Muž' | 'Žena'] => {
const year = Number(personalIdNumber.slice(0, 2)); const yearPadded = Number(personalIdNumber.slice(0, 2));
const year = yearPadded < 24 ? yearPadded + 2000 : yearPadded + 1900;
const idMonth = Number(personalIdNumber.slice(2, 4)); const idMonth = Number(personalIdNumber.slice(2, 4));
let month; let month;
let sex: 'MUŽ' | 'ŽENA'; let sex: 'Muž' | 'Žena';
if (idMonth > 12 && idMonth <= 32) { if (idMonth > 12 && idMonth <= 32) {
month = idMonth - 20; month = idMonth - 20;
sex = 'M'; sex = 'M';
} else if (idMonth > 50 && idMonth <= 52) { } else if (idMonth > 50 && idMonth <= 52) {
month = idMonth - 50; month = idMonth - 50;
sex = ENA'; sex = ena';
} else if (idMonth > 70 && idMonth <= 82) { } else if (idMonth > 70 && idMonth <= 82) {
month = idMonth - 70; month = idMonth - 70;
sex = ENA'; sex = ena';
} else { } else {
month = idMonth; month = idMonth;
sex = 'M'; sex = 'M';
} }
const day = Number(personalIdNumber.slice(4, 6)); const day = Number(personalIdNumber.slice(4, 6));

View file

@ -0,0 +1,21 @@
import { toast } from "@zerodevx/svelte-toast";
export const pushErrorText = (text: string) => {
toast.push(text, {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
};
export const pushSuccessText = (text: string) => {
toast.push(text, {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#047857',
'--toastBarBackground': '#064e3b'
}
});
}

View file

@ -11,6 +11,8 @@
import bacgkround from '$lib/assets/background.jpg'; import bacgkround from '$lib/assets/background.jpg';
import Logout from '$lib/components/icons/Logout.svelte'; import Logout from '$lib/components/icons/Logout.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { pushErrorText } from '$lib/utils/toast';
import { SvelteToast } from '@zerodevx/svelte-toast';
export let data: PageServerData; export let data: PageServerData;
@ -20,7 +22,7 @@
try { try {
candidates = await apiListCandidates(undefined, activeFilter.filter); candidates = await apiListCandidates(undefined, activeFilter.filter);
} catch { } catch {
console.log('error'); pushErrorText('Nepodařilo se načíst uchazeče');
} }
}; };
@ -93,7 +95,7 @@
link.setAttribute('download', 'UCHAZECI' + '.csv'); link.setAttribute('download', 'UCHAZECI' + '.csv');
link.click(); link.click();
} catch (e) { } catch (e) {
console.log(e); pushErrorText('Nepodařilo se stáhnout CSV');
} }
}; };
@ -110,6 +112,7 @@
{/if} {/if}
<div> <div>
<SvelteToast />
<header class="absolute h-14 w-full"> <header class="absolute h-14 w-full">
<img class="h-12 w-full object-cover blur-sm filter" src={bacgkround} alt="Background" /> <img class="h-12 w-full object-cover blur-sm filter" src={bacgkround} alt="Background" />
</header> </header>

View file

@ -10,6 +10,8 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Submit from '$lib/components/button/Submit.svelte'; import Submit from '$lib/components/button/Submit.svelte';
import PasswordField from '$lib/components/textfield/PasswordField.svelte'; import PasswordField from '$lib/components/textfield/PasswordField.svelte';
import { SvelteToast } from '@zerodevx/svelte-toast';
import { pushErrorText } from '$lib/utils/toast';
let adminIdValue = ''; let adminIdValue = '';
let adminPasswordValue = ''; let adminPasswordValue = '';
@ -19,11 +21,12 @@
await apiLogin({ adminId: Number(adminIdValue), password: adminPasswordValue }); await apiLogin({ adminId: Number(adminIdValue), password: adminPasswordValue });
goto('/admin/dashboard'); goto('/admin/dashboard');
} catch (e) { } catch (e) {
console.log(e); pushErrorText('Neplatné heslo nebo ID!');
} }
}; };
</script> </script>
<SvelteToast />
<SplitLayout backgroundImage={background} backgroundPosition="30%"> <SplitLayout backgroundImage={background} backgroundPosition="30%">
<div class="form"> <div class="form">
<div <div
@ -44,7 +47,7 @@
</span> </span>
</div> </div>
<div class="mt-8 w-4/5 lg:w-3/5"> <div class="mt-8 w-4/5 lg:w-3/5">
<Submit value={$LL.input.submit()} on:click={login} /> <Submit enterAllowed={true} value={$LL.input.submit()} on:click={login} />
</div> </div>
</div> </div>
</SplitLayout> </SplitLayout>

View file

@ -18,15 +18,26 @@
import parsePhoneNumber from 'libphonenumber-js'; import parsePhoneNumber from 'libphonenumber-js';
import { createForm } from 'svelte-forms-lib'; import { createForm } from 'svelte-forms-lib';
import * as yup from 'yup'; import * as yup from 'yup';
import type { CandidateData } from '$lib/stores/candidate'; import type { CandidateData, SchoolJson } from '$lib/stores/candidate';
import AccountLinkCheckBox from '$lib/components/checkbox/AccountLinkCheckBox.svelte'; import AccountLinkCheckBox from '$lib/components/checkbox/AccountLinkCheckBox.svelte';
import GradesTable from '$lib/components/grades/GradesTable.svelte'; import GradesTable from '$lib/components/grades/GradesTable.svelte';
import SchoolSelect from '$lib/components/select/SchoolSelect/SchoolSelect.svelte'; import SchoolSelect from '$lib/components/select/SchoolSelect/SchoolSelect.svelte';
import PersonalIdConfirmCheckBox from '$lib/components/checkbox/PersonalIdConfirmCheckBox.svelte'; import PersonalIdConfirmCheckBox from '$lib/components/checkbox/PersonalIdConfirmCheckBox.svelte';
import { isPersonalIdNumberWithBirthdateValid } from '$lib/utils/personalIdFormat'; import {
parseBirthdateSexFromPersonalId,
isPersonalIdMatchingBirthdate
} from '$lib/utils/personalIdFormat';
import PersonalIdErrorModal from '$lib/components/modal/PersonalIdErrorModal.svelte'; import PersonalIdErrorModal from '$lib/components/modal/PersonalIdErrorModal.svelte';
import LinkErrorModal from '$lib/components/modal/LinkErrorModal.svelte'; import LinkErrorModal from '$lib/components/modal/LinkErrorModal.svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { pushErrorText, pushSuccessText } from '$lib/utils/toast';
// import schoolList from '$lib/assets/list/school.json';
import schoolList from '$lib/assets/list/high_schools.json';
import countriesList from '$lib/assets/list/countries.json';
// const schoolList = highSchoolList.map((school) => school['n']);
const schoolNames = schoolList.map((school: SchoolJson) => school['n']);
let pageIndex = 0; let pageIndex = 0;
let pagesFilled = [false, false, false, false, false, false, false, false]; let pagesFilled = [false, false, false, false, false, false, false, false];
@ -69,7 +80,7 @@
city: '', city: '',
zip: '', zip: '',
citizenship: '', citizenship: '',
personalIdNumber: '', personalIdNumber: 'TODO: remove this',
schoolName: '', schoolName: '',
healthInsurance: '', healthInsurance: '',
grades: [], grades: [],
@ -116,8 +127,21 @@
birthdate: yup birthdate: yup
.string() .string()
.required() .required()
.matches(/^([0-3]?[0-9])\.(0?[1-9]|1[0-2])\.[0-9]{4}$/), .matches(/^([0-3]?[0-9])\.(0?[1-9]|1[0-2])\.[0-9]{4}$/)
birthSurname: yup.string().required(), .test((_val) => {
if ($form.candidate.citizenship !== 'Česká republika') return true;
if (!_val) return false;
if (isPersonalIdMatchingBirthdate(
$form.candidate.personalIdNumber,
_val
)) {
return true;
} else {
pushErrorText("Datum narození a rodné číslo se neshodují.")
return false;
}
}),
birthSurname: yup.string(),
sex: yup.string(), sex: yup.string(),
address: yup.string(), address: yup.string(),
street: yup.string().required(), street: yup.string().required(),
@ -128,7 +152,7 @@
city: yup.string().required(), city: yup.string().required(),
zip: yup.string().required(), zip: yup.string().required(),
citizenship: yup.string().required(), citizenship: yup.string().required(),
personalIdNumber: yup.string().required(), personalIdNumber: yup.string(),
schoolName: yup.string().required(), schoolName: yup.string().required(),
healthInsurance: yup.number().required(), healthInsurance: yup.number().required(),
grades: yup grades: yup
@ -146,11 +170,34 @@
) )
.required(), .required(),
firstSchool: yup.object().shape({ firstSchool: yup.object().shape({
name: yup.string().required(), name: yup
.string()
.required()
.test((_val) => {
if (!_val) return false;
if (schoolNames.includes(_val)) {
return true;
} else {
pushErrorText('Vyberte prosím školu ze seznamu.');
return false;
}
}),
field: yup.string().required() field: yup.string().required()
}), }),
secondSchool: yup.object().shape({ secondSchool: yup.object().shape({
name: yup.string().required(), name: yup
.string()
.required()
.test((_val) => {
if (!_val) return false;
if (!_val) return false;
if (schoolNames.includes(_val)) {
return true;
} else {
pushErrorText('Vyberte prosím školu ze seznamu.');
return false;
}
}),
field: yup.string().required() field: yup.string().required()
}), }),
testLanguage: yup.string().required() testLanguage: yup.string().required()
@ -216,32 +263,9 @@
personalIdModal: false, personalIdModal: false,
linkErrorModal: false linkErrorModal: false
}; };
const validatePersonalId = () => {
if ($form.candidate.citizenship === 'Česká republika') {
if (
!isPersonalIdNumberWithBirthdateValid(
$form.candidate.personalIdNumber,
$form.candidate.birthdate
)
) {
toast.push('Rodné číslo neodpovídá oficiální specifikaci či datumu narození', {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
throw new Error('Rodné číslo neodpovídá datumu narození');
}
}
};
const onSubmit = async (values: CandidateData) => { const onSubmit = async (values: CandidateData) => {
console.log('submit button clicked');
console.log(pagesFilled.map((_, i) => !isPageInvalid(i)));
if (pageIndex === pageCount) { if (pageIndex === pageCount) {
console.log('submitting');
// clone values to oldValues // clone values to oldValues
let oldValues = JSON.parse(JSON.stringify(values)); let oldValues = JSON.parse(JSON.stringify(values));
try { try {
@ -336,7 +360,7 @@
$typedErrors['candidate']['healthInsurance'] || $typedErrors['candidate']['healthInsurance'] ||
$typedErrors['candidate']['birthdate'] || $typedErrors['candidate']['birthdate'] ||
$typedErrors['candidate']['birthplace'] || $typedErrors['candidate']['birthplace'] ||
$typedErrors['candidate']['personalIdNumber'] || $typedErrors['candidate']['birthSurname'] ||
$typedErrors['candidate']['testLanguage'] $typedErrors['candidate']['testLanguage']
) { ) {
return true; return true;
@ -386,11 +410,23 @@
return '+' + telephone.match(/[0-9]{1,3}/g)!.join(' '); return '+' + telephone.match(/[0-9]{1,3}/g)!.join(' ');
}; };
// TODO let lastCitizenshipSelected = $form.candidate.citizenship;
/* $form.candidate.personalIdNumber = data.whoami.personalIdNumber; $: if ($form.candidate.citizenship !== lastCitizenshipSelected) {
const [birthdate, sex] = deriveBirthdateFromPersonalId(data.whoami.personalIdNumber); lastCitizenshipSelected = $form.candidate.citizenship;
$form.candidate.birthdate = birthdate; $form.candidate.birthdate = '';
$form.candidate.sex = sex; */ $form.candidate.sex = '';
if ($form.candidate.citizenship === 'Česká republika') {
let [birthdate, sex] = parseBirthdateSexFromPersonalId(data.whoami.personalIdNumber);
$form.candidate.birthdate = birthdate;
$form.candidate.sex = sex;
if (pageIndex === 4) {
pushSuccessText(
`Datum narození a pohlaví bylo vyplněno automaticky podle Vašeho rodného čísla (${data.whoami.personalIdNumber}).`
);
}
}
}
if (details !== undefined) { if (details !== undefined) {
details.candidate.birthdate = details.candidate.birthdate.split('-').reverse().join('.'); details.candidate.birthdate = details.candidate.birthdate.split('-').reverse().join('.');
@ -442,7 +478,10 @@
personalIdNumber={baseCandidateDetails.personalIdNumber} personalIdNumber={baseCandidateDetails.personalIdNumber}
/> />
{:else if visibleModals.linkErrorModal} {:else if visibleModals.linkErrorModal}
<LinkErrorModal applications={baseCandidateDetails.applications} on:close={(_) => (visibleModals.linkErrorModal = false)} /> <LinkErrorModal
applications={baseCandidateDetails.applications}
on:close={(_) => (visibleModals.linkErrorModal = false)}
/>
{/if} {/if}
<div class="form relative bg-center"> <div class="form relative bg-center">
<div class="bottom-5/24 absolute flex w-full flex-col md:h-auto"> <div class="bottom-5/24 absolute flex w-full flex-col md:h-auto">
@ -499,24 +538,14 @@
</p> </p>
<div class="w-full"> <div class="w-full">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="field flex"> <span class="field">
<span class="w-[50%]"> <NameField
<NameField error={$typedErrors['candidate']['name'] || $typedErrors['candidate']['surname']}
error={$typedErrors['candidate']['name'] || bind:valueName={$form.candidate.name}
$typedErrors['candidate']['surname']} bind:valueSurname={$form.candidate.surname}
bind:valueName={$form.candidate.name} placeholder={$LL.input.nameSurname()}
bind:valueSurname={$form.candidate.surname} />
placeholder={$LL.input.nameSurname()} </span>
/>
</span>
<span class="ml-2 w-[50%]">
<TextField
error={$typedErrors['candidate']['birthSurname']}
bind:value={$form.candidate.birthSurname}
placeholder={$LL.input.birthSurname()}
/>
</span>
</div>
<span class="field ml-2"> <span class="field ml-2">
<TelephoneField <TelephoneField
bind:error={$typedErrors['candidate']['telephone']} bind:error={$typedErrors['candidate']['telephone']}
@ -539,7 +568,7 @@
bind:value={$form.candidate.city} bind:value={$form.candidate.city}
type="text" type="text"
placeholder={$LL.input.city()} placeholder={$LL.input.city()}
helperText="Uveďte poštovní směrovací číslo. (např. 602 00)" helperText="Uveďte okres / MČ Prahy (např. Liberec nebo Praha 5)"
/> />
</span> </span>
</div> </div>
@ -553,16 +582,16 @@
bind:valueName={$form.candidate.street} bind:valueName={$form.candidate.street}
bind:valueSurname={$form.candidate.houseNumber} bind:valueSurname={$form.candidate.houseNumber}
placeholder={$LL.input.address()} placeholder={$LL.input.address()}
helperText="Uveďte ulici a číslo popisné (např. Preslova 72)." helperText="Uveďte ulici a číslo popisné (např. Preslova 72/25)."
/> />
</span> </span>
<span class="ml-2 w-[33%]"> <span class="ml-2 w-[33%]">
<TextField <TextField
error={$typedErrors['candidate']['zip']} error={$typedErrors['candidate']['zip']}
bind:value={$form.candidate.zip} bind:value={$form.candidate.zip}
type="number" type="text"
placeholder={$LL.input.zipCode()} placeholder={$LL.input.zipCode()}
helperText="Uveďte poštovní směrovací číslo. (např. 602 00)" helperText="Uveďte poštovní směrovací číslo. (např. 150 21)"
/> />
</span> </span>
</div> </div>
@ -579,7 +608,7 @@
error={$typedErrors['candidate']['citizenship']} error={$typedErrors['candidate']['citizenship']}
bind:value={$form.candidate.citizenship} bind:value={$form.candidate.citizenship}
placeholder={$LL.input.citizenship()} placeholder={$LL.input.citizenship()}
options={['Česká republika', 'Slovenská republika', 'Ukrajina', 'Jiné']} options={countriesList}
/> />
</span> </span>
<span class="ml-2 w-[50%]"> <span class="ml-2 w-[50%]">
@ -597,7 +626,7 @@
bind:value={$form.candidate.birthdate} bind:value={$form.candidate.birthdate}
type="text" type="text"
placeholder={$LL.input.birthDate()} placeholder={$LL.input.birthDate()}
helperText="TODO: (Uveďte ve formátu DD.MM.RRRR)" helperText="Uveďte datum narození (např. 1. 1. 1970)"
/> />
<div class="ml-2"> <div class="ml-2">
<TextField <TextField
@ -605,24 +634,16 @@
bind:value={$form.candidate.birthplace} bind:value={$form.candidate.birthplace}
type="text" type="text"
placeholder={$LL.input.birthPlace()} placeholder={$LL.input.birthPlace()}
helperText="TODO: (Místo narození)" helperText="Uveďte místo narození (např. Liberec nebo Praha 5)"
/> />
</div> </div>
</div> </div>
<div class="field flex items-center justify-center"> <div class="field flex items-center justify-center">
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship} <TextField
<IdField error={$typedErrors['candidate']['birthSurname']}
error={$typedErrors['candidate']['personalIdNumber']} bind:value={$form.candidate.birthSurname}
bind:value={$form.candidate.personalIdNumber} placeholder={`${$LL.input.birthSurname()} (${$LL.input.optional()})`}
placeholder={$LL.input.personalIdentificationNumber()} />
/>
{:else}
<TextField
error={$typedErrors['candidate']['personalIdNumber']}
bind:value={$form.candidate.personalIdNumber}
placeholder={$LL.input.personalIdentificationNumber()}
/>
{/if}
<div class="ml-2"> <div class="ml-2">
<SelectField <SelectField
error={$typedErrors['candidate']['sex']} error={$typedErrors['candidate']['sex']}
@ -640,6 +661,7 @@
type="number" type="number"
bind:value={$form.candidate.schoolName} bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolIzo()} placeholder={$LL.input.schoolIzo()}
helperText="Uveďte IZO základní školy (např. 47608579)"
/> />
{:else} {:else}
<TextField <TextField
@ -647,6 +669,7 @@
type="text" type="text"
bind:value={$form.candidate.schoolName} bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolName()} placeholder={$LL.input.schoolName()}
helperText="Uveďte název základní školy (např. Masarykova základní škola, Praha 9 - Újezd nad Lesy, Polesná 1690)"
/> />
{/if} {/if}
</span> </span>
@ -657,6 +680,7 @@
type="text" type="text"
bind:value={$form.candidate.healthInsurance} bind:value={$form.candidate.healthInsurance}
placeholder={$LL.input.insuranceNumber()} placeholder={$LL.input.insuranceNumber()}
helperText="Uveďte číslo zdravotní pojišťovny (např. 111)"
/> />
</span> </span>
</div> </div>
@ -719,20 +743,43 @@
</span> </span>
</div> </div>
{:else if pageIndex === 7} {:else if pageIndex === 7}
<h1 class="title mt-8">{pageTexts[5]}</h1> <!-- <h1 class="title mt-8">{pageTexts[5]}</h1> -->
<p class="description my-8 block text-center"> <!-- <p class="description mt-8 block text-center">
{$LL.candidate.register.seventh.description()} {$LL.candidate.register.seventh.description()}
</p> </p> -->
<div class="flex h-full flex-col justify-between"> <div class="flex h-full flex-col justify-between">
<span class="field"> <span class="field">
<h2 class="text-sspsBlueDark mb-6 text-3xl font-bold">
První škola - termín JPZ: <span class="underline">13. 4. 2023</span>
</h2>
<SchoolSelect <SchoolSelect
{schoolNames}
{schoolList}
error={$typedErrors['candidate']['firstSchool']['name'] || error={$typedErrors['candidate']['firstSchool']['name'] ||
$typedErrors['candidate']['firstSchool']['field']} $typedErrors['candidate']['firstSchool']['field']}
bind:selectedSchool={$form.candidate.firstSchool} bind:selectedSchool={$form.candidate.firstSchool}
/> />
</span> </span>
<!--dotted line -->
<svg class="mt-12 h-[10px] w-full" viewBox="0 0 800 5">
<line
x1="0"
y1="0"
x2="100%"
y2="0"
stroke="black"
stroke-width="3"
stroke-dasharray="10"
/>
</svg>
<span class="field mt-10"> <span class="field mt-10">
<h2 class="text-sspsBlueDark mb-6 text-3xl font-bold">
Druhá škola - termín JPZ: <span class="underline">14. 4. 2023</span>
</h2>
<SchoolSelect <SchoolSelect
{schoolNames}
{schoolList}
error={$typedErrors['candidate']['secondSchool']['name'] || error={$typedErrors['candidate']['secondSchool']['name'] ||
$typedErrors['candidate']['secondSchool']['field']} $typedErrors['candidate']['secondSchool']['field']}
bind:selectedSchool={$form.candidate.secondSchool} bind:selectedSchool={$form.candidate.secondSchool}
@ -753,13 +800,9 @@
<div class="bottom-1/24 absolute w-full"> <div class="bottom-1/24 absolute w-full">
<div class="field"> <div class="field">
<Submit <Submit
enterAllowed={pageIndex !== 7}
on:click={async (e) => { on:click={async (e) => {
if (pageIndex === 4) {
console.log('validating personal id');
validatePersonalId();
}
await handleSubmit(e); await handleSubmit(e);
console.log(pagesFilled.map((_, i) => !isPageInvalid(i)));
if (isPageInvalid(pageIndex)) return; if (isPageInvalid(pageIndex)) return;
if (pageIndex !== pageCount) { if (pageIndex !== pageCount) {
pagesFilled[pageIndex] = true; pagesFilled[pageIndex] = true;
@ -777,9 +820,6 @@
<button <button
class:dotActive={i === pageIndex} class:dotActive={i === pageIndex}
on:click={async (e) => { on:click={async (e) => {
if (pageIndex === 4 && i > pageIndex) {
validatePersonalId();
}
pageIndex -= pageIndex === pageCount ? 1 : 0; pageIndex -= pageIndex === pageCount ? 1 : 0;
await handleSubmit(e); await handleSubmit(e);
pagesFilled = pagesFilled.map((_, i) => !isPageInvalid(i)); pagesFilled = pagesFilled.map((_, i) => !isPageInvalid(i));
@ -799,7 +839,7 @@
<style lang="postcss"> <style lang="postcss">
.field { .field {
@apply mt-4 w-full md:mt-8 lg:mx-auto lg:w-4/5; @apply lg:w-9/10 mt-4 w-full md:mt-8 lg:mx-auto 2xl:w-4/5;
} }
.form { .form {
@apply flex flex-col; @apply flex flex-col;

View file

@ -32,7 +32,7 @@
/> />
</div> </div>
<div class="mt-8 w-4/5 lg:w-3/5"> <div class="mt-8 w-4/5 lg:w-3/5">
<Submit on:click={redirectToCode} value={$LL.input.submit()} /> <Submit enterAllowed={true} on:click={redirectToCode} value={$LL.input.submit()} />
</div> </div>
</div> </div>
</SplitLayout> </SplitLayout>

View file

@ -47,7 +47,6 @@
} }
const submit = async () => { const submit = async () => {
console.log('submitting: ', codeValueArray);
try { try {
await apiLogin({ applicationId, password: codeValueMobile }); await apiLogin({ applicationId, password: codeValueMobile });
goto('/dashboard'); goto('/dashboard');

View file

@ -64,7 +64,7 @@ const cs: BaseTranslation = {
}, },
eighth: { eighth: {
title: 'Poslední krok', title: 'Poslední krok',
description: 'Přidejte prosím přepis Vaších známek z posledních dvou let studia' description: 'Přidejte prosím přepis Vaších známek z posledních dvou let studia. Známky z druhého pololetí 9. třídy nevyplňujte, pokud vysvědčení ještě nemáte.'
} }
} }
}, },
@ -140,7 +140,7 @@ const cs: BaseTranslation = {
fullAddress: 'Adresa', fullAddress: 'Adresa',
address: 'Ulice a č. p.', address: 'Ulice a č. p.',
zipCode: 'PSČ', zipCode: 'PSČ',
city: 'Město', city: 'Okres',
birthPlace: 'Místo narození', birthPlace: 'Místo narození',
birthDate: 'Datum narození', birthDate: 'Datum narození',
sex: 'Pohlaví', sex: 'Pohlaví',