Refactor ThirdPartyEdit.vue to improve validation handling and streamline input components.

- Replaced individual validation logic with `useViolationList` composable for centralization and consistency.
- Enhanced input components with floating labels, validation error feedback, and improved class binding for better UX.
- Updated API to include `WriteThirdPartyViolationMap` interface for structured validation error mapping.
- Refactored imports and adjusted spacing for better readability and adherence to coding standards.
This commit is contained in:
2025-10-29 12:26:56 +01:00
parent 1e186fab58
commit e291c7abec
2 changed files with 242 additions and 131 deletions

View File

@@ -14,6 +14,22 @@ export const getThirdparty = async (id: number) : Promise<Thirdparty> => {
});
};
export interface WriteThirdPartyViolationMap
extends Record<string, Record<string, string>> {
email: {
"{{ value }}": string;
},
name: {
"{{ value }}": string;
},
telephone: {
"{{ value }}": string;
}
telephone2: {
"{{ value }}": string;
}
}
/*
* POST a new thirdparty
*/

View File

@@ -2,7 +2,7 @@
<div>
<div v-if="parent">
<div class="parent-info">
<i class="fa fa-li fa-hand-o-right" />
<i class="fa fa-li fa-hand-o-right"/>
<b class="me-2">{{ trans(THIRDPARTY_MESSAGES_CHILD_OF) }}</b>
<span class="chill-entity badge-thirdparty">{{ parent.text }}</span>
</div>
@@ -56,104 +56,146 @@
<div v-if="thirdParty.kind === 'child' || thirdParty.kind === 'contact'">
<div class="child-info">
<div class="input-group mb-3 input-section">
<select
class="form-select form-select-lg"
id="civility"
v-model="civility"
>
<option selected disabled :value="null">
{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}
</option>
<option
v-for="civility in civilities"
:key="civility.id"
:value="civility.id"
>
{{ localizeString(civility.name) }}
</option>
</select>
<div class="input-section">
<div class="mb-3">
<div class="input-group">
<div class="form-floating">
<select
class="form-select form-select-lg"
name="civility"
id="civility"
v-model="civility"
>
<option selected disabled :value="null">
{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}
</option>
<option
v-for="civility in civilities"
:key="civility.id"
:value="civility.id"
>
{{ localizeString(civility.name) }}
</option>
</select>
<label for="civility">{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}</label>
</div>
</div>
</div>
</div>
<div class="input-group mb-3 input-section">
<input
class="form-control form-control-lg"
v-model="thirdParty.profession"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
aria-describedby="profession"
/>
<div class="input-section">
<div class="mb-3">
<div class="form-floating">
<input
class="form-control form-control-lg"
name="profession"
v-model="thirdParty.profession"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
aria-describedby="profession"
/>
<label for="profession">{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION) }}</label>
</div>
</div>
</div>
</div>
<div class="child-info">
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="firstname"
v-model="thirdParty.firstname"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)"
/>
<label for="firstname">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
<div class="mb-3">
<div class="input-group">
<div class="form-floating">
<input
class="form-control form-control-lg"
id="firstname"
v-model="thirdParty.firstname"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)"
/>
<label for="firstname">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
}}</label>
</div>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
</div>
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('name', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
<div class="mb3">
<div class="input-group has-validation">
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{ 'is-invalid': violations.hasViolation('name') }"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
}}</label>
</div>
</div>
<div
v-for="err in violations.violationTitles('name')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('name', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div v-if="thirdParty.kind === 'company'">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)
}}</label>
</div>
<div v-if="query">
<ul class="list-suggest add-items inline">
<li @click="addQuery(query)">
<span class="person-text">{{ query }}</span>
</li>
</ul>
<div class="mb-3">
<div class="input-group">
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{ 'is-invalid': violations.hasViolation('name') }"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)
}}</label>
</div>
</div>
<div
v-for="err in violations.violationTitles('name')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
<div v-if="query">
<ul class="list-suggest add-items inline">
<li @click="addQuery(query)">
<span class="person-text">{{ query }}</span>
</li>
</ul>
</div>
</div>
</div>
@@ -167,49 +209,86 @@
/>
</template>
<div class="input-group mb-3">
<span class="input-group-text" id="email"
><i class="fa fa-fw fa-envelope"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.email"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
aria-describedby="email"
/>
<div class="mb-3">
<div class="input-group has-validation">
<span id="email" class="input-group-text">
<i class="fa fa-fw fa-at"/>
</span>
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{ 'is-invalid': violations.hasViolation('email') }"
name="email"
v-model="thirdParty.email"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
aria-describedby="email"
/>
<label for="email">{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL) }}</label>
</div>
</div>
<div
v-for="err in violations.violationTitles('email')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.telephone"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
aria-describedby="phonenumber"
/>
<div class="mb-3">
<div class="input-group has-validation">
<span class="input-group-text" id="phonenumber">
<i class="fa fa-fw fa-phone"/>
</span>
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{'is-invalid': violations.hasViolation('telephone') }"
name="phonenumber"
v-model="thirdParty.telephone"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
aria-describedby="phonenumber"
/>
<label for="phonenumber">{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER) }}</label>
</div>
</div>
<div
v-for="err in violations.violationTitles('telephone')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber2"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.telephone2"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
aria-describedby="phonenumber2"
/>
<div class="mb-3">
<div class="input-group has-validation">
<span class="input-group-text" id="phonenumber2"><i class="fa fa-fw fa-phone"/></span>
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{ 'is-invalid': violations.hasViolation('telephone2') }"
name="phonenumber2"
v-model="thirdParty.telephone2"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
aria-describedby="phonenumber2"
/>
<label for="phonenumber2">{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2) }}</label>
</div>
</div>
<div
v-for="err in violations.violationTitles('telephone2')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
</div>
<div v-if="props.action !== 'addContact'">
<div class="input-group mb-3">
<span class="input-group-text" id="comment"
><i class="fa fa-fw fa-pencil"
><i class="fa fa-fw fa-pencil"
/></span>
<textarea
class="form-control form-control-lg"
@@ -222,12 +301,12 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue'
import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue'
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import {createThirdParty, getThirdparty} from '../../_api/OnTheFly'
import {createThirdParty, getThirdparty, WriteThirdPartyViolationMap} from '../../_api/OnTheFly'
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue'
import { localizeString as _localizeString } from 'ChillMainAssets/lib/localizationHelper/localizationHelper'
import {localizeString as _localizeString} from 'ChillMainAssets/lib/localizationHelper/localizationHelper'
import {
trans,
THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME,
@@ -248,6 +327,8 @@ import {
import {Thirdparty, ThirdpartyCompany, ThirdPartyKind, ThirdPartyWrite} from "../../../types";
import {Civility, SetCivility} from "ChillMainAssets/types";
import {isValidationException} from "ChillMainAssets/lib/api/apiMethods";
import {useViolationList} from "ChillMainAssets/vuejs/_composables/violationList";
import {useToast} from "vue-toast-notification";
interface ThirdPartyEditWriteProps {
@@ -273,7 +354,9 @@ const props = withDefaults(defineProps<ThirdPartyEditProps>(), {type: 'thirdpart
const emit =
defineEmits<(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }) => void>();
defineExpose({ postThirdParty });
defineExpose({postThirdParty});
const toast = useToast();
const thirdParty = ref<ThirdPartyWrite>({
type: "thirdparty",
@@ -290,7 +373,7 @@ const thirdParty = ref<ThirdPartyWrite>({
parent: null,
});
const civility = computed<number|null>({
const civility = computed<number | null>({
get: () => {
if (thirdParty.value.civility !== null) {
return thirdParty.value.civility.id;
@@ -300,7 +383,7 @@ const civility = computed<number|null>({
},
set: (value: number | null) => {
thirdParty.value.civility =
value !== null ? { id: value, type: "chill_main_civility" } : null;
value !== null ? {id: value, type: "chill_main_civility"} : null;
},
});
@@ -310,8 +393,8 @@ const addAddress = reactive({
options: {
openPanesInModal: true,
onlyButton: false,
button: { size: 'btn-sm' },
title: { create: 'add_an_address_title', edit: 'edit_address' },
button: {size: 'btn-sm'},
title: {create: 'add_an_address_title', edit: 'edit_address'},
},
})
const addAddressRef = ref<any>(null)
@@ -330,7 +413,7 @@ const kind = computed<ThirdPartyKind>({
const context = computed(() => {
const ctx: any = {
target: { name: props.type, id: props.id },
target: {name: props.type, id: props.id},
edit: false,
addressId: null as number | null,
defaults: (window as any).addaddress,
@@ -387,7 +470,7 @@ async function loadData(): Promise<void> {
function submitAddress(payload: { addressId: number }) {
console.log(payload);
thirdParty.value.address = {id: payload.addressId};
thirdParty.value.address = {id: payload.addressId};
}
function addQueryItem(field: 'name' | 'firstName', queryItem: string) {
@@ -409,19 +492,30 @@ function addQuery(query: string) {
thirdParty.value.name = query
}
const violations = useViolationList<WriteThirdPartyViolationMap>();
async function postThirdParty(): Promise<void> {
try {
const tp = await createThirdParty(thirdParty.value);
emit('onThirdPartyCreated', { thirdParty: tp });
emit('onThirdPartyCreated', {thirdParty: tp});
} catch (e: unknown) {
throw e;
if (isValidationException<WriteThirdPartyViolationMap>(e)) {
violations.setValidationException(e);
} else {
toast.error("An error occurred while creating the third party");
throw e;
}
}
}
</script>
<style scoped lang="scss">
.was-validated-force {
display: block;
}
.parent-info {
margin-bottom: 1rem;
}
@@ -429,6 +523,7 @@ async function postThirdParty(): Promise<void> {
.child-info {
display: flex;
justify-content: space-between;
.input-section {
width: 49%;
}