mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
fix inaccuracy with using parseFloat instead of parseInt
This commit is contained in:
parent
da51b691ea
commit
86820b36fb
@ -4,13 +4,6 @@ import ContributionForm from './ContributionForm.vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
// Mock external components and dependencies
|
||||
vi.mock('@/components/Inputs/InputHour', () => ({
|
||||
default: {
|
||||
name: 'InputHour',
|
||||
template: '<input data-testid="input-hour" />',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/Inputs/InputAmount', () => ({
|
||||
default: {
|
||||
name: 'InputAmount',
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
:rules="validationSchema.fields.memo"
|
||||
@update:model-value="updateField"
|
||||
textarea="true"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<ValidatedInput
|
||||
name="hours"
|
||||
@ -102,10 +102,13 @@ const form = reactive({ ...props.modelValue })
|
||||
|
||||
// update local form if in parent form changed, it is necessary because the community page will reuse this form also for editing existing
|
||||
// contributions, and it will reusing a existing instance of this component
|
||||
watch(() => props.modelValue, (newValue) => Object.assign(form, newValue))
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => Object.assign(form, newValue),
|
||||
)
|
||||
|
||||
// use computed to make sure child input update if props from parent from this component change
|
||||
const amount = computed(() => form.hours ? (form.hours * 20).toFixed(2).toString() : '20')
|
||||
const amount = computed(() => form.amount)
|
||||
const date = computed(() => form.date)
|
||||
const hours = computed(() => form.hours)
|
||||
const memo = computed(() => form.memo)
|
||||
@ -114,50 +117,37 @@ const isThisMonth = computed(() => {
|
||||
const formDate = new Date(form.date)
|
||||
const now = new Date()
|
||||
return formDate.getMonth() === now.getMonth() && formDate.getFullYear() === now.getFullYear()
|
||||
});
|
||||
})
|
||||
|
||||
// reactive validation schema, because some boundaries depend on form input and existing data
|
||||
const validationSchema = computed(() => {
|
||||
const maxHours = Number((isThisMonth.value ? props.maxGddThisMonth : props.maxGddLastMonth) / 20)
|
||||
const maxAmounts = Number(isThisMonth.value ? parseFloat(props.maxGddThisMonth): parseFloat(props.maxGddLastMonth))
|
||||
const maxAmounts = Number(
|
||||
isThisMonth.value ? parseFloat(props.maxGddThisMonth) : parseFloat(props.maxGddLastMonth),
|
||||
)
|
||||
const maxHours = Number(maxAmounts / 20)
|
||||
|
||||
return object({
|
||||
// The date field is required and needs to be a valid date
|
||||
// contribution date
|
||||
date: dateSchema()
|
||||
.required('contribution.noDateSelected')
|
||||
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
|
||||
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
|
||||
.max(new Date().toISOString().slice(0, 10))
|
||||
.default(''), // date cannot be in the future
|
||||
memo: memoSchema,
|
||||
hours: number().transform((value, originalValue) =>
|
||||
originalValue === "" ? undefined : value
|
||||
)
|
||||
hours: number()
|
||||
.transform((value, originalValue) => (originalValue === '' ? undefined : value))
|
||||
.required('contribution.noHours')
|
||||
.min(0.01, ({min}) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
|
||||
.max(maxHours, ({max}) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
|
||||
.test(
|
||||
'decimal-places',
|
||||
'form.validation.gddCreationTime.decimal-places',
|
||||
(value) => {
|
||||
if (value === undefined || value === null) return true
|
||||
return /^\d+(\.\d{0,2})?$/.test(value.toString())
|
||||
}
|
||||
),
|
||||
amount: number().max(maxAmounts)
|
||||
.min(0.01, ({ min }) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
|
||||
.max(maxHours, ({ max }) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
|
||||
.test('decimal-places', 'form.validation.gddCreationTime.decimal-places', (value) => {
|
||||
if (value === undefined || value === null) return true
|
||||
return /^\d+(\.\d{0,2})?$/.test(value.toString())
|
||||
}),
|
||||
amount: number().max(maxAmounts),
|
||||
})
|
||||
})
|
||||
|
||||
const updateField = (newValue, name, valid) => {
|
||||
if (typeof name === 'string' && name.length) {
|
||||
form[name] = newValue
|
||||
if (name === 'hours') {
|
||||
form.amount = (newValue * 20).toFixed(2).toString()
|
||||
}
|
||||
}
|
||||
// console.log('update field', { newValue, name, form, amount: amount.value })
|
||||
emit('update:modelValue', form)
|
||||
}
|
||||
|
||||
const disabled = computed(() => !validationSchema.value.isValidSync(form))
|
||||
|
||||
const noOpenCreation = computed(() => {
|
||||
@ -175,6 +165,16 @@ const noOpenCreation = computed(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
const updateField = (newValue, name) => {
|
||||
if (typeof name === 'string' && name.length) {
|
||||
form[name] = newValue
|
||||
if (name === 'hours') {
|
||||
form.amount = form.hours ? (form.hours * 20).toFixed(2).toString() : '20'
|
||||
}
|
||||
}
|
||||
emit('update:modelValue', form)
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const dataToSave = { ...form }
|
||||
let emitOption = 'set-contribution'
|
||||
@ -188,6 +188,7 @@ function submit() {
|
||||
|
||||
function fullFormReset() {
|
||||
emit('update:modelValue', {
|
||||
id: undefined,
|
||||
date: null,
|
||||
memo: '',
|
||||
hours: '',
|
||||
|
||||
@ -33,6 +33,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
function afterComma(input) {
|
||||
return input.toFixed(2).toString()
|
||||
}
|
||||
export default {
|
||||
name: 'OpenCreationsAmount',
|
||||
props: {
|
||||
@ -42,16 +45,16 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
hoursSubmittedThisMonth() {
|
||||
return (1000 - this.maxGddThisMonth) / 20
|
||||
return afterComma((1000 - this.maxGddThisMonth) / 20)
|
||||
},
|
||||
hoursSubmittedLastMonth() {
|
||||
return (1000 - this.maxGddLastMonth) / 20
|
||||
return afterComma((1000 - this.maxGddLastMonth) / 20)
|
||||
},
|
||||
hoursAvailableThisMonth() {
|
||||
return this.maxGddThisMonth / 20
|
||||
return afterComma(this.maxGddThisMonth / 20)
|
||||
},
|
||||
hoursAvailableLastMonth() {
|
||||
return this.maxGddLastMonth / 20
|
||||
return afterComma(this.maxGddLastMonth / 20)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import InputHour from './InputHour.vue'
|
||||
import { useField } from 'vee-validate'
|
||||
import { BFormGroup, BFormInput, BFormInvalidFeedback } from 'bootstrap-vue-next'
|
||||
|
||||
// Mock vee-validate
|
||||
vi.mock('vee-validate', () => ({
|
||||
useField: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('InputHour', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (propsData = {}) => {
|
||||
return mount(InputHour, {
|
||||
props: {
|
||||
rules: {},
|
||||
name: 'input-field-name',
|
||||
label: 'input-field-label',
|
||||
placeholder: 'input-field-placeholder',
|
||||
validMaxTime: 25,
|
||||
...propsData,
|
||||
},
|
||||
global: {
|
||||
components: {
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BFormInvalidFeedback,
|
||||
},
|
||||
mocks: {
|
||||
$t: (t) => t,
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
$n: (n) => String(n),
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useField.mockReturnValue({
|
||||
value: ref(0),
|
||||
errorMessage: ref(''),
|
||||
meta: ref({ valid: true }),
|
||||
})
|
||||
wrapper = createWrapper()
|
||||
})
|
||||
|
||||
it('renders the component input-hour', () => {
|
||||
expect(wrapper.find('div.input-hour').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an input field', () => {
|
||||
expect(wrapper.findComponent({ name: 'BFormInput' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('properties', () => {
|
||||
it('passes correct props to BFormInput', () => {
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
expect(input.props()).toMatchObject({
|
||||
id: 'input-field-name-input-field',
|
||||
modelValue: 0,
|
||||
name: 'input-field-name',
|
||||
placeholder: 'input-field-placeholder',
|
||||
type: 'number',
|
||||
state: true,
|
||||
step: '0.25',
|
||||
min: '0',
|
||||
max: 25,
|
||||
})
|
||||
})
|
||||
|
||||
it('passes correct props to BFormGroup', () => {
|
||||
const formGroup = wrapper.findComponent({ name: 'BFormGroup' })
|
||||
expect(formGroup.props()).toMatchObject({
|
||||
label: 'input-field-label',
|
||||
labelFor: 'input-field-name-input-field',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('updates currentValue when input changes', async () => {
|
||||
await wrapper.findComponent({ name: 'BFormInput' }).vm.$emit('update:modelValue', 12)
|
||||
expect(wrapper.vm.currentValue).toBe(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('displays error message when present', async () => {
|
||||
useField.mockReturnValue({
|
||||
value: ref(0),
|
||||
errorMessage: ref('Error message'),
|
||||
meta: ref({ valid: false }),
|
||||
})
|
||||
wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.findComponent({ name: 'BFormInvalidFeedback' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'BFormInvalidFeedback' }).text()).toBe('Error message')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div class="input-hour">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormInput
|
||||
:id="labelFor"
|
||||
v-model="currentValue"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="number"
|
||||
:state="meta.valid"
|
||||
step="0.01"
|
||||
min="0"
|
||||
:max="validMaxTime"
|
||||
class="bg-248"
|
||||
/>
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</BFormInvalidFeedback>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useField } from 'vee-validate'
|
||||
|
||||
const props = defineProps({
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
validMaxTime: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
|
||||
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
@ -1,18 +1,16 @@
|
||||
<template>
|
||||
<div :class="wrapperClassName">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormTextarea v-if="textarea"
|
||||
<BFormTextarea
|
||||
v-if="textarea"
|
||||
v-bind="{ ...$attrs, id: labelFor, name }"
|
||||
v-model="model"
|
||||
trim
|
||||
:rows="4"
|
||||
:max-rows="4"
|
||||
no-resize
|
||||
/>
|
||||
<BFormInput v-else
|
||||
v-bind="{ ...$attrs, id: labelFor, name }"
|
||||
v-model="model"
|
||||
/>
|
||||
/>
|
||||
<BFormInput v-else v-bind="{ ...$attrs, id: labelFor, name }" v-model="model" />
|
||||
<slot></slot>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
@ -37,11 +35,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const model = defineModel()
|
||||
|
||||
const wrapperClassName = computed(() => props.name ? `input-${props.name}` : 'input')
|
||||
const wrapperClassName = computed(() => (props.name ? `input-${props.name}` : 'input'))
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LabeledInput
|
||||
<LabeledInput
|
||||
v-bind="$attrs"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
@ -11,9 +11,9 @@
|
||||
:name="name"
|
||||
:state="valid"
|
||||
@update:modelValue="updateValue"
|
||||
>
|
||||
>
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
{{ errorMessage }}
|
||||
</BFormInvalidFeedback>
|
||||
</LabeledInput>
|
||||
</template>
|
||||
@ -48,32 +48,35 @@ const valid = computed(() => props.rules.isValidSync(props.modelValue))
|
||||
const errorMessage = computed(() => {
|
||||
if (props.modelValue === undefined || props.modelValue === '' || props.modelValue === null) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
try {
|
||||
props.rules.validateSync(props.modelValue)
|
||||
return undefined
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return translateYupErrorString(e.message, t)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const updateValue = ((newValue) => {
|
||||
emit('update:modelValue', newValue, props.name, valid.value)
|
||||
})
|
||||
const updateValue = (newValue) => {
|
||||
emit('update:modelValue', newValue, props.name, valid.value)
|
||||
}
|
||||
|
||||
// update model and if value changed and model isn't null, check validation,
|
||||
// for loading Input with existing value and show correct validation state
|
||||
watch(() => props.modelValue, () => {
|
||||
model.value = props.modelValue
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
model.value = props.modelValue
|
||||
},
|
||||
)
|
||||
|
||||
// extract additional parameter like min and max from schema
|
||||
const schemaDescription = computed(() => props.rules.describe())
|
||||
const getTestParameter = (name) => schemaDescription.value?.tests?.find(t => t.name === name)?.params[name]
|
||||
const getTestParameter = (name) =>
|
||||
schemaDescription.value?.tests?.find((t) => t.name === name)?.params[name]
|
||||
const minValue = computed(() => getTestParameter('min'))
|
||||
const maxValue = computed(() => getTestParameter('max'))
|
||||
const resetValue = computed(() => schemaDescription.default)
|
||||
const isOptional = computed(() => schemaDescription.optional)
|
||||
const resetValue = computed(() => schemaDescription.value.default)
|
||||
const isOptional = computed(() => schemaDescription.value.optional)
|
||||
</script>
|
||||
|
||||
|
||||
@ -105,7 +105,7 @@ const minimalDate = computed(() => {
|
||||
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
||||
})
|
||||
|
||||
const amountToAdd = computed(() => (form.value.id ? parseInt(updateAmount.value) : 0))
|
||||
const amountToAdd = computed(() => (form.value.id ? parseFloat(updateAmount.value) : 0.0))
|
||||
|
||||
const maxForMonths = computed(() => {
|
||||
const originalDate = new Date(originalContributionDate.value)
|
||||
@ -115,9 +115,9 @@ const maxForMonths = computed(() => {
|
||||
creation.year === originalDate.getFullYear() &&
|
||||
creation.month === originalDate.getMonth()
|
||||
) {
|
||||
return parseInt(creation.amount) + amountToAdd.value
|
||||
return parseFloat(creation.amount)
|
||||
}
|
||||
return parseInt(creation.amount)
|
||||
return parseFloat(creation.amount)
|
||||
})
|
||||
}
|
||||
return [0, 0]
|
||||
@ -263,20 +263,20 @@ const handleUpdateListContributions = (pagination) => {
|
||||
}
|
||||
|
||||
const handleUpdateContributionForm = (item) => {
|
||||
/*Object.assign(form.value, {
|
||||
/* Object.assign(form.value, {
|
||||
id: item.id,
|
||||
date: new Date(item.contributionDate).toISOString().slice(0, 10),
|
||||
memo: item.memo,
|
||||
amount: item.amount,
|
||||
hours: item.amount / 20,
|
||||
})*/
|
||||
}) */
|
||||
form.value = {
|
||||
id: item.id,
|
||||
date: new Date(item.contributionDate).toISOString().slice(0, 10),
|
||||
memo: item.memo,
|
||||
amount: item.amount,
|
||||
hours: item.amount / 20,
|
||||
}//*/
|
||||
} //* /
|
||||
originalContributionDate.value = item.contributionDate
|
||||
updateAmount.value = item.amount
|
||||
tabIndex.value = 0
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { string } from 'yup'
|
||||
|
||||
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
|
||||
export const isLanguageKey = (str) => str.match(/^(?!\.)[a-z][a-zA-Z0-9-]*([.][a-z][a-zA-Z0-9-]*)*(?<!\.)$/)
|
||||
export const isLanguageKey = (str) =>
|
||||
str.match(/^(?!\.)[a-z][a-zA-Z0-9-]*([.][a-z][a-zA-Z0-9-]*)*(?<!\.)$/)
|
||||
|
||||
export const translateYupErrorString = (error, t) => {
|
||||
const type = typeof error
|
||||
@ -16,6 +17,5 @@ export const translateYupErrorString = (error, t) => {
|
||||
|
||||
export const memo = string()
|
||||
.required('contribution.yourActivity')
|
||||
.min(5, ({min}) => ({ key: 'form.validation.memo.min', values: { min } }))
|
||||
.max(255, ({max}) => ({ key: 'form.validation.memo.max', values: { max } }))
|
||||
|
||||
.min(5, ({ min }) => ({ key: 'form.validation.memo.min', values: { min } }))
|
||||
.max(255, ({ max }) => ({ key: 'form.validation.memo.max', values: { max } }))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user