- 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:
@@ -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"
|
||||||
|
|||||||
@@ -155,10 +155,19 @@ 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];
|
||||||
if (value === undefined || value === null ||
|
// Voor boolean velden is false een geldige waarde
|
||||||
(typeof value === 'string' && !value.trim()) ||
|
if (field.type === 'boolean') {
|
||||||
(Array.isArray(value) && value.length === 0)) {
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
missingFields.push(field.name);
|
if (typeof value !== 'boolean') {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bestaande validatie voor andere veldtypen
|
||||||
|
if (value === undefined || value === null ||
|
||||||
|
(typeof value === 'string' && !value.trim()) ||
|
||||||
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -167,10 +176,19 @@ export default {
|
|||||||
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];
|
||||||
if (value === undefined || value === null ||
|
// Voor boolean velden is false een geldige waarde
|
||||||
(typeof value === 'string' && !value.trim()) ||
|
if (field.type === 'boolean') {
|
||||||
(Array.isArray(value) && value.length === 0)) {
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
missingFields.push(field.name);
|
if (typeof value !== 'boolean') {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bestaande validatie voor andere veldtypen
|
||||||
|
if (value === undefined || value === null ||
|
||||||
|
(typeof value === 'string' && !value.trim()) ||
|
||||||
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
|
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,53 +233,132 @@ 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() {
|
||||||
// Basic validation
|
// Eerst proactief alle boolean velden corrigeren
|
||||||
const missingFields = [];
|
this.initializeBooleanFields();
|
||||||
|
|
||||||
|
// Wacht tot updates zijn verwerkt, dan valideer en submit
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Basic validation
|
||||||
|
const missingFields = [];
|
||||||
|
|
||||||
if (Array.isArray(this.formData.fields)) {
|
if (Array.isArray(this.formData.fields)) {
|
||||||
// Valideer array-gebaseerde velden
|
// Valideer array-gebaseerde velden
|
||||||
this.formData.fields.forEach(field => {
|
this.formData.fields.forEach(field => {
|
||||||
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];
|
||||||
if (value === undefined || value === null ||
|
// Voor boolean velden is false een geldige waarde
|
||||||
(typeof value === 'string' && !value.trim()) ||
|
if (field.type === 'boolean') {
|
||||||
(Array.isArray(value) && value.length === 0)) {
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
missingFields.push(field.name);
|
if (typeof value !== 'boolean') {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bestaande validatie voor andere veldtypen
|
||||||
|
if (value === undefined || value === null ||
|
||||||
|
(typeof value === 'string' && !value.trim()) ||
|
||||||
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
|
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 (value === undefined || value === null ||
|
if (field.type === 'boolean') {
|
||||||
(typeof value === 'string' && !value.trim()) ||
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
(Array.isArray(value) && value.length === 0)) {
|
if (typeof value !== 'boolean') {
|
||||||
missingFields.push(field.name);
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bestaande validatie voor andere veldtypen
|
||||||
|
if (value === undefined || value === null ||
|
||||||
|
(typeof value === 'string' && !value.trim()) ||
|
||||||
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
alert(`De volgende velden zijn verplicht: ${missingFields.join(', ')}`);
|
alert(`De volgende velden zijn verplicht: ${missingFields.join(', ')}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
answer = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language)
|
||||||
self.flow.state.phase = "contact_time_evaluation"
|
if rag_output:
|
||||||
|
answer = f"{rag_output.answer}\n\n{message}"
|
||||||
|
|
||||||
results = SelectionResult.create_for_type(self.type, self.type_version,)
|
self.flow.state.answer = answer
|
||||||
else:
|
self.flow.state.phase = "candidate_selected"
|
||||||
answer = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language)
|
current_app.logger.debug(f"Contact time evaluation: {arguments.form_values}")
|
||||||
if rag_output:
|
self.flow.state.contact_time_prefs = arguments.form_values
|
||||||
answer = f"{answer}\n\n{rag_output.answer}"
|
|
||||||
|
|
||||||
self.flow.state.answer = answer
|
results = SelectionResult.create_for_type(self.type, self.type_version,)
|
||||||
self.flow.state.phase = "candidate_selected"
|
current_app.logger.debug(f"Results: {results.model_dump()}")
|
||||||
self.flow.state.contact_time = contact_time_answer
|
|
||||||
|
|
||||||
results = SelectionResult.create_for_type(self.type, self.type_version,)
|
|
||||||
|
|
||||||
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
407
test_boolean_field_fix.html
Normal 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
80
verify_boolean_fix.js
Normal 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!');
|
||||||
Reference in New Issue
Block a user