- Adapting TRAICIE_SELECTION_SPECIALIST to retrieve prefered contact times using a form iso free text

- Improvement of DynamicForm en FormField to handle boolean values.
This commit is contained in:
Josako
2025-07-24 14:43:08 +02:00
parent fc3cae1986
commit 8a85b4540f
6 changed files with 684 additions and 70 deletions

View File

@@ -4,7 +4,7 @@ CATALOG_TYPES = {
"name": "Standard Catalog", "name": "Standard Catalog",
"description": "A Catalog with information in Evie's Library, to be considered as a whole", "description": "A Catalog with information in Evie's Library, to be considered as a whole",
}, },
"TRAICIE_ROLE_DEFINITION_CATALOG": { "TRAICIE_RQC": {
"name": "Role Definition Catalog", "name": "Role Definition Catalog",
"description": "A Catalog with information about roles, to be considered as a whole", "description": "A Catalog with information about roles, to be considered as a whole",
"partner": "traicie" "partner": "traicie"

View File

@@ -155,24 +155,42 @@ export default {
const fieldId = field.id || field.name; const fieldId = field.id || field.name;
if (field.required) { if (field.required) {
const value = this.localFormValues[fieldId]; const value = this.localFormValues[fieldId];
// Voor boolean velden is false een geldige waarde
if (field.type === 'boolean') {
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
if (typeof value !== 'boolean') {
missingFields.push(field.name);
}
} else {
// Bestaande validatie voor andere veldtypen
if (value === undefined || value === null || if (value === undefined || value === null ||
(typeof value === 'string' && !value.trim()) || (typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)) { (Array.isArray(value) && value.length === 0)) {
missingFields.push(field.name); missingFields.push(field.name);
} }
} }
}
}); });
} else { } else {
// Valideer object-gebaseerde velden // Valideer object-gebaseerde velden
Object.entries(this.formData.fields).forEach(([fieldId, field]) => { Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
if (field.required) { if (field.required) {
const value = this.localFormValues[fieldId]; const value = this.localFormValues[fieldId];
// Voor boolean velden is false een geldige waarde
if (field.type === 'boolean') {
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
if (typeof value !== 'boolean') {
missingFields.push(field.name);
}
} else {
// Bestaande validatie voor andere veldtypen
if (value === undefined || value === null || if (value === undefined || value === null ||
(typeof value === 'string' && !value.trim()) || (typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)) { (Array.isArray(value) && value.length === 0)) {
missingFields.push(field.name); missingFields.push(field.name);
} }
} }
}
}); });
} }
@@ -185,10 +203,23 @@ export default {
// Gebruik een vlag om recursieve updates te voorkomen // Gebruik een vlag om recursieve updates te voorkomen
if (JSON.stringify(newValues) !== JSON.stringify(this.localFormValues)) { if (JSON.stringify(newValues) !== JSON.stringify(this.localFormValues)) {
this.localFormValues = JSON.parse(JSON.stringify(newValues)); this.localFormValues = JSON.parse(JSON.stringify(newValues));
// Proactief alle boolean velden corrigeren na externe wijziging
this.$nextTick(() => {
this.initializeBooleanFields();
});
} }
}, },
deep: true deep: true
}, },
formData: {
handler() {
// Herinitialiseer boolean velden wanneer form structuur verandert
this.$nextTick(() => {
this.initializeBooleanFields();
});
},
deep: true
},
localFormValues: { localFormValues: {
handler(newValues) { handler(newValues) {
// Gebruik een vlag om recursieve updates te voorkomen // Gebruik een vlag om recursieve updates te voorkomen
@@ -202,16 +233,76 @@ export default {
created() { created() {
// Icon loading is now handled automatically by useIconManager composable // Icon loading is now handled automatically by useIconManager composable
}, },
mounted() {
// Proactief alle boolean velden initialiseren bij het laden
this.initializeBooleanFields();
},
methods: { methods: {
// Proactieve initialisatie van alle boolean velden
initializeBooleanFields() {
const updatedValues = { ...this.localFormValues };
let hasChanges = false;
// Behandel alle boolean velden in het formulier
const fields = Array.isArray(this.formData.fields)
? this.formData.fields
: Object.entries(this.formData.fields).map(([id, field]) => ({ ...field, id }));
fields.forEach(field => {
const fieldId = field.id || field.name;
if (field.type === 'boolean') {
const currentValue = updatedValues[fieldId];
// Initialiseer als de waarde undefined, null, of een lege string is
if (currentValue === undefined || currentValue === null || currentValue === '') {
updatedValues[fieldId] = field.default === true ? true : false;
hasChanges = true;
} else if (typeof currentValue !== 'boolean') {
// Converteer andere waarden naar boolean
updatedValues[fieldId] = Boolean(currentValue);
hasChanges = true;
}
}
});
// Update alleen als er wijzigingen zijn
if (hasChanges) {
this.localFormValues = updatedValues;
}
},
updateFieldValue(fieldId, value) { updateFieldValue(fieldId, value) {
// Zoek het veld om het type te bepalen
let field = null;
if (Array.isArray(this.formData.fields)) {
field = this.formData.fields.find(f => (f.id || f.name) === fieldId);
} else {
field = this.formData.fields[fieldId];
}
// Type conversie voor boolean velden
let processedValue = value;
if (field && field.type === 'boolean') {
processedValue = Boolean(value);
}
// Update lokale waarde // Update lokale waarde
this.localFormValues = { this.localFormValues = {
...this.localFormValues, ...this.localFormValues,
[fieldId]: value [fieldId]: processedValue
}; };
// Na elke field update, controleer alle boolean velden
this.$nextTick(() => {
this.initializeBooleanFields();
});
}, },
handleSubmit() { handleSubmit() {
// Eerst proactief alle boolean velden corrigeren
this.initializeBooleanFields();
// Wacht tot updates zijn verwerkt, dan valideer en submit
this.$nextTick(() => {
// Basic validation // Basic validation
const missingFields = []; const missingFields = [];
@@ -221,24 +312,42 @@ export default {
const fieldId = field.id || field.name; const fieldId = field.id || field.name;
if (field.required) { if (field.required) {
const value = this.localFormValues[fieldId]; const value = this.localFormValues[fieldId];
// Voor boolean velden is false een geldige waarde
if (field.type === 'boolean') {
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
if (typeof value !== 'boolean') {
missingFields.push(field.name);
}
} else {
// Bestaande validatie voor andere veldtypen
if (value === undefined || value === null || if (value === undefined || value === null ||
(typeof value === 'string' && !value.trim()) || (typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)) { (Array.isArray(value) && value.length === 0)) {
missingFields.push(field.name); missingFields.push(field.name);
} }
} }
}
}); });
} else { } else {
// Valideer object-gebaseerde velden // Valideer object-gebaseerde velden
Object.entries(this.formData.fields).forEach(([fieldId, field]) => { Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
if (field.required) { if (field.required) {
const value = this.localFormValues[fieldId]; const value = this.localFormValues[fieldId];
// Voor boolean velden is false een geldige waarde
if (field.type === 'boolean') {
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
if (typeof value !== 'boolean') {
missingFields.push(field.name);
}
} else {
// Bestaande validatie voor andere veldtypen
if (value === undefined || value === null || if (value === undefined || value === null ||
(typeof value === 'string' && !value.trim()) || (typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)) { (Array.isArray(value) && value.length === 0)) {
missingFields.push(field.name); missingFields.push(field.name);
} }
} }
}
}); });
} }
@@ -249,6 +358,7 @@ export default {
// Emit submit event // Emit submit event
this.$emit('submit', this.localFormValues); this.$emit('submit', this.localFormValues);
});
}, },
handleCancel() { handleCancel() {
@@ -277,7 +387,7 @@ export default {
// Format different field types // Format different field types
if (field.type === 'boolean') { if (field.type === 'boolean') {
return value ? 'Ja' : 'Nee'; return value ? true : false;
} else if (field.type === 'enum' && !value && field.default) { } else if (field.type === 'enum' && !value && field.default) {
return field.default; return field.default;
} }

View File

@@ -199,9 +199,18 @@ export default {
return this.modelValue; return this.modelValue;
}, },
set(value) { set(value) {
// Type conversie voor boolean velden
let processedValue = value;
if (this.field.type === 'boolean') {
// Converteer alle mogelijke waarden naar echte boolean
console.log('FormField Boolean Value: ', value);
processedValue = Boolean(value);
console.log('FormField Boolean Processed Value: ', processedValue);
}
// Voorkom emit als de waarde niet is veranderd // Voorkom emit als de waarde niet is veranderd
if (JSON.stringify(value) !== JSON.stringify(this.modelValue)) { if (JSON.stringify(processedValue) !== JSON.stringify(this.modelValue)) {
this.$emit('update:modelValue', value); this.$emit('update:modelValue', processedValue);
} }
} }
}, },

View File

@@ -35,11 +35,13 @@ KO_CRITERIA_MET_MESSAGE = "We processed your answers with a positive result."
RQC_MESSAGE = "You are well suited for this job." RQC_MESSAGE = "You are well suited for this job."
CONTACT_DATA_QUESTION = ("Are you willing to provide us with your contact data, so we can contact you to continue " CONTACT_DATA_QUESTION = ("Are you willing to provide us with your contact data, so we can contact you to continue "
"the selection process?") "the selection process?")
CONTACT_DATA_GUIDING_MESSAGE = ("Thank you for trusting your contact data with us. Below you find a form to help you "
"to provide us the necessary information.")
NO_CONTACT_DATA_QUESTION = ("We are sorry to hear that. The only way to proceed with the selection process is " NO_CONTACT_DATA_QUESTION = ("We are sorry to hear that. The only way to proceed with the selection process is "
"to provide us with your contact data. Do you want to provide us with your contact data?" "to provide us with your contact data. Do you want to provide us with your contact data?"
"if not, we thank you, and we'll end the selection process.") "if not, we thank you, and we'll end the selection process.")
CONTACT_DATA_PROCESSED_MESSAGE = "We successfully processed your contact data." CONTACT_DATA_PROCESSED_MESSAGE = "Thank you for allowing us to contact you."
CONTACT_TIME_QUESTION = "When do you prefer us to contact you? Provide us with some preferred weekdays and times!" CONTACT_TIME_QUESTION = "When do you prefer us to contact you? You can select some options in the provided form"
NO_CONTACT_TIME_MESSAGE = ("We could not process your preferred contact time. Can you please provide us with your " NO_CONTACT_TIME_MESSAGE = ("We could not process your preferred contact time. Can you please provide us with your "
"preferred contact time?") "preferred contact time?")
CONTACT_TIME_PROCESSED_MESSAGE = ("We successfully processed your preferred contact time. We will contact you as soon " CONTACT_TIME_PROCESSED_MESSAGE = ("We successfully processed your preferred contact time. We will contact you as soon "
@@ -85,7 +87,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
self._add_state_result_relation("competency_questions") self._add_state_result_relation("competency_questions")
self._add_state_result_relation("competency_scores") self._add_state_result_relation("competency_scores")
self._add_state_result_relation("personal_contact_data") self._add_state_result_relation("personal_contact_data")
self._add_state_result_relation("contact_time") self._add_state_result_relation("contact_time_prefs")
def _instantiate_specialist(self): def _instantiate_specialist(self):
verbose = self.tuning verbose = self.tuning
@@ -264,11 +266,13 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0") contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0")
contact_form = TranslationServices.translate_config(self.tenant_id, contact_form, "fields", contact_form = TranslationServices.translate_config(self.tenant_id, contact_form, "fields",
arguments.language) arguments.language)
guiding_message = TranslationServices.translate(self.tenant_id, CONTACT_DATA_GUIDING_MESSAGE,
arguments.language)
rag_output = self._check_and_execute_rag(arguments, formatted_context, citations) rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
if rag_output: if rag_output:
answer = f"{rag_output.answer}" answer = f"{rag_output.answer}\n\n{guiding_message}"
else: else:
answer = "" answer = guiding_message
self.flow.state.answer = answer self.flow.state.answer = answer
self.flow.state.form_request = contact_form self.flow.state.form_request = contact_form
@@ -291,6 +295,9 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
answer = ( answer = (
f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_PROCESSED_MESSAGE, arguments.language)}\n" f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_PROCESSED_MESSAGE, arguments.language)}\n"
f"{TranslationServices.translate(self.tenant_id, CONTACT_TIME_QUESTION, arguments.language)}") f"{TranslationServices.translate(self.tenant_id, CONTACT_TIME_QUESTION, arguments.language)}")
time_pref_form = cache_manager.specialist_forms_config_cache.get_config("CONTACT_TIME_PREFERENCES_SIMPLE", "1.0")
time_pref_form = TranslationServices.translate_config(self.tenant_id, time_pref_form, "fields",
arguments.language)
rag_output = self._check_and_execute_rag(arguments, formatted_context, citations) rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
if rag_output: if rag_output:
@@ -299,6 +306,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
self.flow.state.answer = answer self.flow.state.answer = answer
self.flow.state.phase = "contact_time_evaluation" self.flow.state.phase = "contact_time_evaluation"
self.flow.state.personal_contact_data = arguments.form_values self.flow.state.personal_contact_data = arguments.form_values
self.flow.state.form_request = time_pref_form
results = SelectionResult.create_for_type(self.type, self.type_version,) results = SelectionResult.create_for_type(self.type, self.type_version,)
return results return results
@@ -306,29 +314,21 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
def execute_contact_time_evaluation_state(self, arguments: SpecialistArguments, formatted_context, citations) \ def execute_contact_time_evaluation_state(self, arguments: SpecialistArguments, formatted_context, citations) \
-> SpecialistResult: -> SpecialistResult:
self.log_tuning("Traicie Selection Specialist contact_time_evaluation started", {}) self.log_tuning("Traicie Selection Specialist contact_time_evaluation started", {})
contact_time_answer = HumanAnswerServices.get_answer_to_question(self.tenant_id, CONTACT_TIME_QUESTION,
arguments.question, arguments.language)
rag_output = self._check_and_execute_rag(arguments, formatted_context, citations) rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
if contact_time_answer == "No answer provided": message = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language)
answer = TranslationServices.translate(self.tenant_id, NO_CONTACT_TIME_MESSAGE, arguments.language)
if rag_output:
answer = f"{answer}\n\n{rag_output.answer}"
self.flow.state.answer = answer
self.flow.state.phase = "contact_time_evaluation"
results = SelectionResult.create_for_type(self.type, self.type_version,)
else:
answer = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language) answer = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language)
if rag_output: if rag_output:
answer = f"{answer}\n\n{rag_output.answer}" answer = f"{rag_output.answer}\n\n{message}"
self.flow.state.answer = answer self.flow.state.answer = answer
self.flow.state.phase = "candidate_selected" self.flow.state.phase = "candidate_selected"
self.flow.state.contact_time = contact_time_answer current_app.logger.debug(f"Contact time evaluation: {arguments.form_values}")
self.flow.state.contact_time_prefs = arguments.form_values
results = SelectionResult.create_for_type(self.type, self.type_version,) results = SelectionResult.create_for_type(self.type, self.type_version,)
current_app.logger.debug(f"Results: {results.model_dump()}")
return results return results
@@ -481,6 +481,14 @@ class PersonalContactData(BaseModel):
consent: bool = Field(..., description="Consent", alias="consent") consent: bool = Field(..., description="Consent", alias="consent")
class ContactTimePreferences(BaseModel):
early: Optional[bool] = Field(None, description="Early", alias="early")
late_morning: Optional[bool] = Field(None, description="Late Morning", alias="late_morning")
afternoon: Optional[bool] = Field(None, description="Afternoon", alias="afternoon")
evening: Optional[bool] = Field(None, description="Evening", alias="evening")
other: Optional[str] = Field(None, description="Other", alias="other")
class SelectionInput(BaseModel): class SelectionInput(BaseModel):
# RAG elements # RAG elements
language: Optional[str] = Field(None, alias="language") language: Optional[str] = Field(None, alias="language")
@@ -508,7 +516,7 @@ class SelectionFlowState(EveAIFlowState):
rag_output: Optional[RAGOutput] = None rag_output: Optional[RAGOutput] = None
ko_criteria_answers: Optional[Dict[str, str]] = None ko_criteria_answers: Optional[Dict[str, str]] = None
personal_contact_data: Optional[PersonalContactData] = None personal_contact_data: Optional[PersonalContactData] = None
contact_time: Optional[str] = None contact_time_prefs: Optional[ContactTimePreferences] = None
citations: Optional[List[Dict[str, Any]]] = None citations: Optional[List[Dict[str, Any]]] = None
@@ -516,7 +524,7 @@ class SelectionResult(SpecialistResult):
rag_output: Optional[RAGOutput] = Field(None, alias="rag_output") rag_output: Optional[RAGOutput] = Field(None, alias="rag_output")
ko_criteria_answers: Optional[Dict[str, str]] = Field(None, alias="ko_criteria_answers") ko_criteria_answers: Optional[Dict[str, str]] = Field(None, alias="ko_criteria_answers")
personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data") personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data")
contact_time: Optional[str] = None contact_time_prefs: Optional[ContactTimePreferences] = None
class SelectionFlow(EveAICrewAIFlow[SelectionFlowState]): class SelectionFlow(EveAICrewAIFlow[SelectionFlowState]):

407
test_boolean_field_fix.html Normal file
View File

@@ -0,0 +1,407 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Boolean Field Fix</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.test-result {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.form-values {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-family: monospace;
}
</style>
</head>
<body>
<div id="app">
<h1>Test Boolean Field Fix</h1>
<div class="test-section">
<h2>Test 1: Basic Boolean Field</h2>
<p>Test dat een niet-aangevinkte checkbox false retourneert in plaats van een lege string.</p>
<dynamic-form
:form-data="testForm1"
:form-values="formValues1"
@update:form-values="formValues1 = $event"
@submit="handleSubmit1"
@cancel="handleCancel"
/>
<div class="form-values">
<strong>Huidige waarden:</strong><br>
{{ JSON.stringify(formValues1, null, 2) }}
</div>
<div v-if="submitResult1" class="test-result" :class="submitResult1.success ? 'success' : 'error'">
{{ submitResult1.message }}
</div>
</div>
<div class="test-section">
<h2>Test 2: Required Boolean Field</h2>
<p>Test dat required boolean velden correct valideren (false is een geldige waarde).</p>
<dynamic-form
:form-data="testForm2"
:form-values="formValues2"
@update:form-values="formValues2 = $event"
@submit="handleSubmit2"
@cancel="handleCancel"
/>
<div class="form-values">
<strong>Huidige waarden:</strong><br>
{{ JSON.stringify(formValues2, null, 2) }}
</div>
<div v-if="submitResult2" class="test-result" :class="submitResult2.success ? 'success' : 'error'">
{{ submitResult2.message }}
</div>
</div>
<div class="test-section">
<h2>Test 3: Mixed Form with Boolean and Other Fields</h2>
<p>Test een formulier met zowel boolean als andere veldtypen.</p>
<dynamic-form
:form-data="testForm3"
:form-values="formValues3"
@update:form-values="formValues3 = $event"
@submit="handleSubmit3"
@cancel="handleCancel"
/>
<div class="form-values">
<strong>Huidige waarden:</strong><br>
{{ JSON.stringify(formValues3, null, 2) }}
</div>
<div v-if="submitResult3" class="test-result" :class="submitResult3.success ? 'success' : 'error'">
{{ submitResult3.message }}
</div>
</div>
</div>
<script type="module">
// Import components (in real scenario these would be imported from actual files)
// For this test, we'll create mock components that simulate the behavior
const FormField = {
name: 'FormField',
props: {
field: { type: Object, required: true },
fieldId: { type: String, required: true },
modelValue: { default: null }
},
emits: ['update:modelValue'],
computed: {
value: {
get() {
if (this.modelValue === undefined || this.modelValue === null) {
if (this.field.type === 'boolean') {
return this.field.default === true;
}
return this.field.default !== undefined ? this.field.default : '';
}
return this.modelValue;
},
set(value) {
// Type conversie voor boolean velden
let processedValue = value;
if (this.field.type === 'boolean') {
processedValue = Boolean(value);
}
if (JSON.stringify(processedValue) !== JSON.stringify(this.modelValue)) {
this.$emit('update:modelValue', processedValue);
}
}
},
fieldType() {
const typeMap = {
'boolean': 'checkbox',
'string': 'text',
'str': 'text'
};
return typeMap[this.field.type] || this.field.type;
}
},
template: `
<div class="form-field" style="margin-bottom: 15px;">
<label v-if="fieldType !== 'checkbox'" :for="fieldId">
{{ field.name }}
<span v-if="field.required" style="color: red;">*</span>
</label>
<input v-if="fieldType === 'text'"
:id="fieldId"
type="text"
v-model="value"
:required="field.required"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<div v-if="fieldType === 'checkbox'" style="display: flex; align-items: center;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox"
:id="fieldId"
v-model="value"
:required="field.required"
style="margin-right: 8px;">
<span>{{ field.name }}</span>
<span v-if="field.required" style="color: red; margin-left: 2px;">*</span>
</label>
</div>
</div>
`
};
const DynamicForm = {
name: 'DynamicForm',
components: { 'form-field': FormField },
props: {
formData: { type: Object, required: true },
formValues: { type: Object, default: () => ({}) }
},
emits: ['submit', 'cancel', 'update:formValues'],
data() {
return {
localFormValues: { ...this.formValues }
};
},
computed: {
isFormValid() {
const missingFields = [];
this.formData.fields.forEach(field => {
const fieldId = field.id || field.name;
if (field.required) {
const value = this.localFormValues[fieldId];
if (field.type === 'boolean') {
if (typeof value !== 'boolean') {
missingFields.push(field.name);
}
} else {
if (value === undefined || value === null ||
(typeof value === 'string' && !value.trim())) {
missingFields.push(field.name);
}
}
}
});
return missingFields.length === 0;
}
},
watch: {
formValues: {
handler(newValues) {
if (JSON.stringify(newValues) !== JSON.stringify(this.localFormValues)) {
this.localFormValues = JSON.parse(JSON.stringify(newValues));
}
},
deep: true
},
localFormValues: {
handler(newValues) {
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
this.$emit('update:formValues', JSON.parse(JSON.stringify(newValues)));
}
},
deep: true
}
},
methods: {
updateFieldValue(fieldId, value) {
const field = this.formData.fields.find(f => (f.id || f.name) === fieldId);
let processedValue = value;
if (field && field.type === 'boolean') {
processedValue = Boolean(value);
}
this.localFormValues = {
...this.localFormValues,
[fieldId]: processedValue
};
},
handleSubmit() {
this.$emit('submit', this.localFormValues);
},
handleCancel() {
this.$emit('cancel');
}
},
template: `
<div style="padding: 15px; border: 1px solid #ddd; border-radius: 8px;">
<h3>{{ formData.title }}</h3>
<div style="margin-bottom: 20px;">
<form-field
v-for="field in formData.fields"
:key="field.id || field.name"
:field="field"
:field-id="field.id || field.name"
:model-value="localFormValues[field.id || field.name]"
@update:model-value="updateFieldValue(field.id || field.name, $event)"
/>
</div>
<div>
<button type="button" @click="handleCancel" style="margin-right: 10px; padding: 8px 16px;">
Annuleren
</button>
<button type="button" @click="handleSubmit" :disabled="!isFormValid"
style="padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 4px;"
:style="{ opacity: isFormValid ? 1 : 0.5 }">
Versturen
</button>
</div>
</div>
`
};
const { createApp } = Vue;
createApp({
components: {
'dynamic-form': DynamicForm
},
data() {
return {
// Test 1: Basic boolean field
testForm1: {
title: 'Basic Boolean Test',
fields: [
{
name: 'Akkoord',
type: 'boolean',
required: false
}
]
},
formValues1: {},
submitResult1: null,
// Test 2: Required boolean field
testForm2: {
title: 'Required Boolean Test',
fields: [
{
name: 'Verplichte Checkbox',
type: 'boolean',
required: true
}
]
},
formValues2: {},
submitResult2: null,
// Test 3: Mixed form
testForm3: {
title: 'Mixed Form Test',
fields: [
{
name: 'Naam',
type: 'string',
required: true
},
{
name: 'Nieuwsbrief',
type: 'boolean',
required: false
},
{
name: 'Voorwaarden',
type: 'boolean',
required: true
}
]
},
formValues3: {},
submitResult3: null
};
},
methods: {
handleSubmit1(values) {
const akkoordValue = values['Akkoord'];
const isBoolean = typeof akkoordValue === 'boolean';
const isCorrectValue = akkoordValue === false || akkoordValue === true;
this.submitResult1 = {
success: isBoolean && isCorrectValue,
message: isBoolean && isCorrectValue
? `✅ SUCCESS: Boolean field retourneert correct ${akkoordValue} (type: ${typeof akkoordValue})`
: `❌ FAILED: Boolean field retourneert ${akkoordValue} (type: ${typeof akkoordValue})`
};
},
handleSubmit2(values) {
const checkboxValue = values['Verplichte Checkbox'];
const isBoolean = typeof checkboxValue === 'boolean';
this.submitResult2 = {
success: isBoolean,
message: isBoolean
? `✅ SUCCESS: Required boolean field retourneert correct ${checkboxValue} (type: ${typeof checkboxValue})`
: `❌ FAILED: Required boolean field retourneert ${checkboxValue} (type: ${typeof checkboxValue})`
};
},
handleSubmit3(values) {
const naam = values['Naam'];
const nieuwsbrief = values['Nieuwsbrief'];
const voorwaarden = values['Voorwaarden'];
const naamOk = typeof naam === 'string' && naam.length > 0;
const nieuwsbriefOk = typeof nieuwsbrief === 'boolean';
const voorwaardenOk = typeof voorwaarden === 'boolean';
const allOk = naamOk && nieuwsbriefOk && voorwaardenOk;
this.submitResult3 = {
success: allOk,
message: allOk
? `✅ SUCCESS: Alle velden correct - Naam: "${naam}", Nieuwsbrief: ${nieuwsbrief}, Voorwaarden: ${voorwaarden}`
: `❌ FAILED: Problemen met velden - Naam: ${naam} (${typeof naam}), Nieuwsbrief: ${nieuwsbrief} (${typeof nieuwsbrief}), Voorwaarden: ${voorwaarden} (${typeof voorwaarden})`
};
},
handleCancel() {
console.log('Form cancelled');
}
}
}).mount('#app');
</script>
</body>
</html>

80
verify_boolean_fix.js Normal file
View File

@@ -0,0 +1,80 @@
// Verification script to demonstrate the boolean field fix
// This simulates the behavior of the updated FormField component
console.log('=== Boolean Field Fix Verification ===\n');
// Simulate the type conversion logic from FormField.vue
function processFieldValue(value, fieldType) {
let processedValue = value;
if (fieldType === 'boolean') {
// This is the key fix: convert all values to proper boolean
processedValue = Boolean(value);
}
return processedValue;
}
// Test scenarios
const testCases = [
{ description: 'Unchecked checkbox (empty string)', value: '', fieldType: 'boolean' },
{ description: 'Checked checkbox (true)', value: true, fieldType: 'boolean' },
{ description: 'Unchecked checkbox (false)', value: false, fieldType: 'boolean' },
{ description: 'Null value', value: null, fieldType: 'boolean' },
{ description: 'Undefined value', value: undefined, fieldType: 'boolean' },
{ description: 'String field (should not be affected)', value: '', fieldType: 'string' },
{ description: 'String field with value', value: 'test', fieldType: 'string' }
];
console.log('Testing type conversion logic:\n');
testCases.forEach((testCase, index) => {
const result = processFieldValue(testCase.value, testCase.fieldType);
const originalType = typeof testCase.value;
const resultType = typeof result;
console.log(`Test ${index + 1}: ${testCase.description}`);
console.log(` Input: ${testCase.value} (${originalType})`);
console.log(` Output: ${result} (${resultType})`);
// Verify the fix
if (testCase.fieldType === 'boolean') {
const isCorrect = typeof result === 'boolean';
console.log(`${isCorrect ? 'PASS' : 'FAIL'}: Boolean field returns boolean type`);
// Special check for empty string (the main issue)
if (testCase.value === '') {
const isFixed = result === false;
console.log(`${isFixed ? 'PASS' : 'FAIL'}: Empty string converts to false`);
}
}
console.log('');
});
// Simulate validation logic from DynamicForm.vue
function validateRequiredBooleanField(value) {
// New validation logic: for boolean fields, check if it's actually a boolean
return typeof value === 'boolean';
}
console.log('Testing validation logic for required boolean fields:\n');
const validationTests = [
{ description: 'Required boolean field with false value', value: false },
{ description: 'Required boolean field with true value', value: true },
{ description: 'Required boolean field with empty string (should fail)', value: '' },
{ description: 'Required boolean field with null (should fail)', value: null }
];
validationTests.forEach((test, index) => {
const isValid = validateRequiredBooleanField(test.value);
console.log(`Validation Test ${index + 1}: ${test.description}`);
console.log(` Value: ${test.value} (${typeof test.value})`);
console.log(` Valid: ${isValid ? 'YES' : 'NO'}`);
console.log('');
});
console.log('=== Summary ===');
console.log('✅ Boolean fields now return proper boolean values (true/false)');
console.log('✅ Empty strings from unchecked checkboxes are converted to false');
console.log('✅ Required boolean field validation accepts false as valid');
console.log('✅ Type conversion is applied at both FormField and DynamicForm levels');
console.log('\nThe boolean field issue has been successfully resolved!');