mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3442 from gradido/frontend_refactor_contribution_form
refactor(frontend): contribution form refactor
This commit is contained in:
commit
e99638df0c
@ -4,13 +4,6 @@ import ContributionForm from './ContributionForm.vue'
|
|||||||
import { useForm } from 'vee-validate'
|
import { useForm } from 'vee-validate'
|
||||||
|
|
||||||
// Mock external components and dependencies
|
// Mock external components and dependencies
|
||||||
vi.mock('@/components/Inputs/InputHour', () => ({
|
|
||||||
default: {
|
|
||||||
name: 'InputHour',
|
|
||||||
template: '<input data-testid="input-hour" />',
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/components/Inputs/InputAmount', () => ({
|
vi.mock('@/components/Inputs/InputAmount', () => ({
|
||||||
default: {
|
default: {
|
||||||
name: 'InputAmount',
|
name: 'InputAmount',
|
||||||
@ -54,8 +47,6 @@ describe('ContributionForm', () => {
|
|||||||
hours: 2,
|
hours: 2,
|
||||||
amount: 40,
|
amount: 40,
|
||||||
},
|
},
|
||||||
isThisMonth: true,
|
|
||||||
minimalDate: new Date('2024-01-01'),
|
|
||||||
maxGddLastMonth: 100,
|
maxGddLastMonth: 100,
|
||||||
maxGddThisMonth: 200,
|
maxGddThisMonth: 200,
|
||||||
}
|
}
|
||||||
@ -73,44 +64,81 @@ describe('ContributionForm', () => {
|
|||||||
expect(wrapper.find('.contribution-form').exists()).toBe(true)
|
expect(wrapper.find('.contribution-form').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('computes showMessage correctly', async () => {
|
describe('compute isThisMonth', () => {
|
||||||
expect(wrapper.vm.showMessage).toBe(false)
|
it('return true', async () => {
|
||||||
|
await wrapper.setProps({
|
||||||
await wrapper.setProps({
|
modelValue: { date: new Date().toISOString() },
|
||||||
maxGddThisMonth: 0,
|
})
|
||||||
maxGddLastMonth: 0,
|
expect(wrapper.vm.isThisMonth).toBe(true)
|
||||||
})
|
})
|
||||||
|
it('return false', async () => {
|
||||||
|
const now = new Date()
|
||||||
|
const lastMonth = new Date(now.setMonth(now.getMonth() - 1, 1))
|
||||||
|
await wrapper.setProps({
|
||||||
|
modelValue: { date: lastMonth.toISOString() },
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.isThisMonth).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
expect(wrapper.vm.showMessage).toBe(true)
|
describe('noOpenCreations return correct translation key', () => {
|
||||||
|
it('if both max gdd are > 0', () => {
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||||
|
})
|
||||||
|
it('if max gdd for this month is 0, and form.date is in last month', async () => {
|
||||||
|
const now = new Date()
|
||||||
|
const lastMonth = new Date(now.setMonth(now.getMonth() - 1, 1))
|
||||||
|
await wrapper.setProps({
|
||||||
|
maxGddThisMonth: 0,
|
||||||
|
modelValue: { date: lastMonth.toISOString() },
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||||
|
})
|
||||||
|
it('if max gdd for last month is 0, and form.date is in this month', async () => {
|
||||||
|
await wrapper.setProps({
|
||||||
|
maxGddLastMonth: 0,
|
||||||
|
modelValue: { date: new Date().toISOString() },
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||||
|
})
|
||||||
|
it('if max gdd is 0 for both months', async () => {
|
||||||
|
await wrapper.setProps({
|
||||||
|
maxGddThisMonth: 0,
|
||||||
|
maxGddLastMonth: 0,
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBe('contribution.noOpenCreation.allMonth')
|
||||||
|
})
|
||||||
|
it('if max gdd this month is zero and form.date is inside this month', async () => {
|
||||||
|
await wrapper.setProps({
|
||||||
|
maxGddThisMonth: 0,
|
||||||
|
modelValue: { date: new Date().toISOString() },
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBe('contribution.noOpenCreation.thisMonth')
|
||||||
|
})
|
||||||
|
it('if max gdd last month is zero and form.date is inside last month', async () => {
|
||||||
|
const now = new Date()
|
||||||
|
const lastMonth = new Date(now.setMonth(now.getMonth() - 1, 1))
|
||||||
|
await wrapper.setProps({
|
||||||
|
maxGddLastMonth: 0,
|
||||||
|
modelValue: { date: lastMonth.toISOString() },
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.noOpenCreation).toBe('contribution.noOpenCreation.lastMonth')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('computes disabled correctly', async () => {
|
it('computes disabled correctly', async () => {
|
||||||
expect(wrapper.vm.disabled).toBe(false)
|
expect(wrapper.vm.disabled).toBe(true)
|
||||||
|
|
||||||
await wrapper.setProps({
|
await wrapper.setProps({
|
||||||
maxGddThisMonth: 30,
|
modelValue: { date: new Date().toISOString().slice(0, 10) },
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.form.amount = 100
|
wrapper.vm.form.amount = 100
|
||||||
|
|
||||||
expect(wrapper.vm.disabled).toBe(true)
|
expect(wrapper.vm.disabled).toBe(false)
|
||||||
})
|
|
||||||
|
|
||||||
it('computes validMaxGDD correctly', async () => {
|
|
||||||
expect(wrapper.vm.validMaxGDD).toBe(200)
|
|
||||||
|
|
||||||
await wrapper.setProps({ isThisMonth: false })
|
|
||||||
|
|
||||||
expect(wrapper.vm.validMaxGDD).toBe(100)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates amount when hours change', async () => {
|
it('updates amount when hours change', async () => {
|
||||||
const setFieldValueMock = vi.fn()
|
|
||||||
vi.mocked(useForm).mockReturnValue({
|
|
||||||
...vi.mocked(useForm)(),
|
|
||||||
setFieldValue: setFieldValueMock,
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapper = mount(ContributionForm, {
|
wrapper = mount(ContributionForm, {
|
||||||
props: defaultProps,
|
props: defaultProps,
|
||||||
global: {
|
global: {
|
||||||
@ -121,9 +149,9 @@ describe('ContributionForm', () => {
|
|||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
// Simulate changing hours
|
// Simulate changing hours
|
||||||
wrapper.vm.updateAmount(3)
|
wrapper.vm.updateField(3, 'hours')
|
||||||
|
|
||||||
expect(setFieldValueMock).toHaveBeenCalledWith('amount', '60.00')
|
expect(wrapper.vm.form.amount).toBe('60.00')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits update-contribution event on submit for existing contribution', async () => {
|
it('emits update-contribution event on submit for existing contribution', async () => {
|
||||||
@ -159,31 +187,4 @@ describe('ContributionForm', () => {
|
|||||||
|
|
||||||
expect(wrapper.emitted('set-contribution')).toBeTruthy()
|
expect(wrapper.emitted('set-contribution')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resets form on fullFormReset', () => {
|
|
||||||
const resetFormMock = vi.fn()
|
|
||||||
vi.mocked(useForm).mockReturnValue({
|
|
||||||
...vi.mocked(useForm)(),
|
|
||||||
resetForm: resetFormMock,
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapper = mount(ContributionForm, {
|
|
||||||
props: defaultProps,
|
|
||||||
global: {
|
|
||||||
stubs: ['BForm', 'BFormInput', 'BRow', 'BCol', 'BButton'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapper.vm.fullFormReset()
|
|
||||||
|
|
||||||
expect(resetFormMock).toHaveBeenCalledWith({
|
|
||||||
values: {
|
|
||||||
id: null,
|
|
||||||
date: '',
|
|
||||||
memo: '',
|
|
||||||
hours: 0,
|
|
||||||
amount: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,63 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contribution-form">
|
<div class="contribution-form">
|
||||||
<BForm
|
<BForm
|
||||||
ref="form"
|
|
||||||
class="form-style p-3 bg-white app-box-shadow gradido-border-radius"
|
class="form-style p-3 bg-white app-box-shadow gradido-border-radius"
|
||||||
@submit.prevent="submit"
|
@submit.prevent="submit"
|
||||||
>
|
>
|
||||||
<label>{{ $t('contribution.selectDate') }}</label>
|
<ValidatedInput
|
||||||
<BFormInput
|
|
||||||
id="contribution-date"
|
id="contribution-date"
|
||||||
:model-value="date"
|
:model-value="date"
|
||||||
:state="dataFieldMeta.valid"
|
name="date"
|
||||||
:locale="$i18n.locale"
|
:label="$t('contribution.selectDate')"
|
||||||
:max="getMaximalDate"
|
|
||||||
:min="minimalDate.toISOString().slice(0, 10)"
|
|
||||||
class="mb-4 bg-248"
|
|
||||||
reset-value=""
|
|
||||||
:label-no-date-selected="$t('contribution.noDateSelected')"
|
|
||||||
required
|
|
||||||
:no-flip="true"
|
:no-flip="true"
|
||||||
|
class="mb-4 bg-248"
|
||||||
type="date"
|
type="date"
|
||||||
@update:model-value="handleDateChange"
|
:rules="validationSchema.fields.date"
|
||||||
>
|
@update:model-value="updateField"
|
||||||
<template #nav-prev-year><span></span></template>
|
/>
|
||||||
<template #nav-next-year><span></span></template>
|
<div v-if="noOpenCreation" class="p-3" data-test="contribution-message">
|
||||||
</BFormInput>
|
|
||||||
|
|
||||||
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
|
|
||||||
{{ noOpenCreation }}
|
{{ noOpenCreation }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<input-textarea
|
<ValidatedInput
|
||||||
id="contribution-memo"
|
id="contribution-memo"
|
||||||
|
:model-value="memo"
|
||||||
name="memo"
|
name="memo"
|
||||||
:label="$t('contribution.activity')"
|
:label="$t('contribution.activity')"
|
||||||
:placeholder="$t('contribution.yourActivity')"
|
:placeholder="$t('contribution.yourActivity')"
|
||||||
:rules="{ required: true, min: 5, max: 255 }"
|
:rules="validationSchema.fields.memo"
|
||||||
|
textarea="true"
|
||||||
|
@update:model-value="updateField"
|
||||||
/>
|
/>
|
||||||
<input-hour
|
<ValidatedInput
|
||||||
name="hours"
|
name="hours"
|
||||||
|
:model-value="hours"
|
||||||
:label="$t('form.hours')"
|
:label="$t('form.hours')"
|
||||||
placeholder="0.25"
|
placeholder="0.01"
|
||||||
:rules="{
|
step="0.01"
|
||||||
required: true,
|
type="number"
|
||||||
min: 0.25,
|
:rules="validationSchema.fields.hours"
|
||||||
max: validMaxTime,
|
@update:model-value="updateField"
|
||||||
gddCreationTime: { min: 0.25, max: validMaxTime },
|
|
||||||
}"
|
|
||||||
:valid-max-time="validMaxTime"
|
|
||||||
/>
|
/>
|
||||||
<input-amount
|
<LabeledInput
|
||||||
id="contribution-amount"
|
id="contribution-amount"
|
||||||
|
:model-value="amount"
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
name="amount"
|
name="amount"
|
||||||
:label="$t('form.amount')"
|
:label="$t('form.amount')"
|
||||||
placeholder="20"
|
:placeholder="GDD_PER_HOUR"
|
||||||
:rules="{ required: true, gddSendAmount: { min: 20, max: validMaxGDD } }"
|
readonly
|
||||||
typ="ContributionForm"
|
type="text"
|
||||||
|
trim
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BRow class="mt-5">
|
<BRow class="mt-5">
|
||||||
<BCol>
|
<BCol>
|
||||||
<BButton
|
<BButton
|
||||||
@ -88,107 +80,104 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { reactive, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import InputHour from '@/components/Inputs/InputHour'
|
import ValidatedInput from '@/components/Inputs/ValidatedInput'
|
||||||
import InputAmount from '@/components/Inputs/InputAmount'
|
import LabeledInput from '@/components/Inputs/LabeledInput'
|
||||||
import InputTextarea from '@/components/Inputs/InputTextarea'
|
import { memo as memoSchema } from '@/validationSchemas'
|
||||||
import { useField, useForm } from 'vee-validate'
|
import { object, date as dateSchema, number } from 'yup'
|
||||||
|
import { GDD_PER_HOUR } from '../../constants'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Object, required: true },
|
modelValue: { type: Object, required: true },
|
||||||
isThisMonth: { type: Boolean, required: true },
|
|
||||||
minimalDate: { type: Date, required: true },
|
|
||||||
maxGddLastMonth: { type: Number, required: true },
|
maxGddLastMonth: { type: Number, required: true },
|
||||||
maxGddThisMonth: { type: Number, required: true },
|
maxGddThisMonth: { type: Number, required: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update-contribution', 'set-contribution'])
|
const emit = defineEmits(['update-contribution', 'set-contribution', 'update:modelValue'])
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const form = ref({ ...props.modelValue })
|
const form = reactive({ ...props.modelValue })
|
||||||
|
|
||||||
const {
|
// update local form if in parent form changed, it is necessary because the community page will reuse this form also for editing existing
|
||||||
values: formValues,
|
// contributions, and it will reusing a existing instance of this component
|
||||||
meta: formMeta,
|
watch(
|
||||||
resetForm,
|
() => props.modelValue,
|
||||||
defineField,
|
(newValue) => Object.assign(form, newValue),
|
||||||
setFieldValue,
|
)
|
||||||
} = useForm({
|
|
||||||
initialValues: {
|
// use computed to make sure child input update if props from parent from this component change
|
||||||
date: props.modelValue.date,
|
const amount = computed(() => form.amount)
|
||||||
memo: props.modelValue.memo,
|
const date = computed(() => form.date)
|
||||||
hours: props.modelValue.hours,
|
const hours = computed(() => form.hours)
|
||||||
amount: props.modelValue.amount,
|
const memo = computed(() => form.memo)
|
||||||
},
|
|
||||||
|
const isThisMonth = computed(() => {
|
||||||
|
const formDate = new Date(form.date)
|
||||||
|
const now = new Date()
|
||||||
|
return formDate.getMonth() === now.getMonth() && formDate.getFullYear() === now.getFullYear()
|
||||||
})
|
})
|
||||||
|
|
||||||
const [date, dateProps] = defineField('date')
|
// reactive validation schema, because some boundaries depend on form input and existing data
|
||||||
|
const validationSchema = computed(() => {
|
||||||
const { meta: dataFieldMeta } = useField('date', 'required')
|
const maxAmounts = Number(
|
||||||
|
isThisMonth.value ? parseFloat(props.maxGddThisMonth) : parseFloat(props.maxGddLastMonth),
|
||||||
const handleDateChange = (newDate) => {
|
|
||||||
date.value = newDate
|
|
||||||
emit('update:model-value', { ...props.modelValue, date: newDate })
|
|
||||||
}
|
|
||||||
|
|
||||||
const showMessage = computed(() => {
|
|
||||||
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) return true
|
|
||||||
if (props.modelValue.date)
|
|
||||||
return (
|
|
||||||
(props.isThisMonth && props.maxGddThisMonth <= 0) ||
|
|
||||||
(!props.isThisMonth && props.maxGddLastMonth <= 0)
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
const disabled = computed(() => {
|
|
||||||
return (
|
|
||||||
!formMeta.value.valid ||
|
|
||||||
(props.isThisMonth && parseInt(form.value.amount) > parseInt(props.maxGddThisMonth)) ||
|
|
||||||
(!props.isThisMonth && parseInt(form.value.amount) > parseInt(props.maxGddLastMonth))
|
|
||||||
)
|
)
|
||||||
|
const maxHours = parseFloat(Number(maxAmounts / GDD_PER_HOUR).toFixed(2))
|
||||||
|
|
||||||
|
return object({
|
||||||
|
// The date field is required and needs to be a valid date
|
||||||
|
// contribution date
|
||||||
|
date: dateSchema()
|
||||||
|
.required()
|
||||||
|
.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()
|
||||||
|
.required()
|
||||||
|
.transform((value, originalValue) => (originalValue === '' ? undefined : value))
|
||||||
|
.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().min(0.01).max(maxAmounts),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const validMaxGDD = computed(() => {
|
const disabled = computed(() => !validationSchema.value.isValidSync(form))
|
||||||
return Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth)
|
|
||||||
})
|
|
||||||
|
|
||||||
const validMaxTime = computed(() => {
|
|
||||||
return Number(validMaxGDD.value / 20)
|
|
||||||
})
|
|
||||||
|
|
||||||
const noOpenCreation = computed(() => {
|
const noOpenCreation = computed(() => {
|
||||||
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) {
|
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) {
|
||||||
return t('contribution.noOpenCreation.allMonth')
|
return t('contribution.noOpenCreation.allMonth')
|
||||||
}
|
}
|
||||||
if (props.isThisMonth && props.maxGddThisMonth <= 0) {
|
if (form.date) {
|
||||||
return t('contribution.noOpenCreation.thisMonth')
|
if (isThisMonth.value && props.maxGddThisMonth <= 0) {
|
||||||
|
return t('contribution.noOpenCreation.thisMonth')
|
||||||
|
}
|
||||||
|
if (!isThisMonth.value && props.maxGddLastMonth <= 0) {
|
||||||
|
return t('contribution.noOpenCreation.lastMonth')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!props.isThisMonth && props.maxGddLastMonth <= 0) {
|
return undefined
|
||||||
return t('contribution.noOpenCreation.lastMonth')
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getMaximalDate = computed(() => {
|
const updateField = (newValue, name) => {
|
||||||
return new Date().toISOString().slice(0, 10)
|
if (typeof name === 'string' && name.length) {
|
||||||
})
|
form[name] = newValue
|
||||||
|
if (name === 'hours') {
|
||||||
watch(
|
const amount = form.hours ? (form.hours * GDD_PER_HOUR).toFixed(2) : GDD_PER_HOUR
|
||||||
() => formValues.hours,
|
form.amount = amount.toString()
|
||||||
() => {
|
}
|
||||||
updateAmount(formValues.hours)
|
}
|
||||||
},
|
emit('update:modelValue', form)
|
||||||
)
|
|
||||||
|
|
||||||
function updateAmount(hours) {
|
|
||||||
setFieldValue('amount', (hours * 20).toFixed(2).toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const dataToSave = { ...formValues }
|
const dataToSave = { ...form }
|
||||||
let emitOption = 'set-contribution'
|
let emitOption = 'set-contribution'
|
||||||
if (props.modelValue.id) {
|
if (props.modelValue.id) {
|
||||||
dataToSave.id = props.modelValue.id
|
dataToSave.id = props.modelValue.id
|
||||||
@ -199,14 +188,12 @@ function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fullFormReset() {
|
function fullFormReset() {
|
||||||
resetForm({
|
emit('update:modelValue', {
|
||||||
values: {
|
id: undefined,
|
||||||
id: null,
|
date: null,
|
||||||
date: '',
|
memo: '',
|
||||||
memo: '',
|
hours: '',
|
||||||
hours: 0,
|
amount: undefined,
|
||||||
amount: '',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -99,12 +99,6 @@ describe('ContributionListItem', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('date', () => {
|
|
||||||
it('is equal to createdAt', () => {
|
|
||||||
expect(wrapper.vm.date).toBe(wrapper.vm.createdAt)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('delete contribution', () => {
|
describe('delete contribution', () => {
|
||||||
describe('edit contribution', () => {
|
describe('edit contribution', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
</BCol>
|
</BCol>
|
||||||
<BCol cols="9" lg="3" offset="3" offset-md="0" offset-lg="0">
|
<BCol cols="9" lg="3" offset="3" offset-md="0" offset-lg="0">
|
||||||
<div class="small">
|
<div class="small">
|
||||||
{{ $t('creation') }} {{ $t('(') }}{{ amount / 20 }} {{ $t('h') }}{{ $t(')') }}
|
{{ $t('creation') }} {{ $t('(') }}{{ hours }} {{ $t('h') }}{{ $t(')') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="status === 'DENIED' && allContribution" class="fw-bold">
|
<div v-if="status === 'DENIED' && allContribution" class="fw-bold">
|
||||||
<variant-icon icon="x-circle" variant="danger" />
|
<variant-icon icon="x-circle" variant="danger" />
|
||||||
@ -125,8 +125,9 @@ import ContributionMessagesList from '@/components/ContributionMessages/Contribu
|
|||||||
import { listContributionMessages } from '@/graphql/queries'
|
import { listContributionMessages } from '@/graphql/queries'
|
||||||
import { useAppToast } from '@/composables/useToast'
|
import { useAppToast } from '@/composables/useToast'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useLazyQuery, useQuery } from '@vue/apollo-composable'
|
import { useLazyQuery } from '@vue/apollo-composable'
|
||||||
import AppAvatar from '@/components/AppAvatar.vue'
|
import AppAvatar from '@/components/AppAvatar.vue'
|
||||||
|
import { GDD_PER_HOUR } from '../../constants'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@ -201,10 +202,9 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { toastError, toastSuccess } = useAppToast()
|
const { toastError } = useAppToast()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const inProcess = ref(true)
|
|
||||||
const messagesGet = ref([])
|
const messagesGet = ref([])
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
|
||||||
@ -224,8 +224,6 @@ const icon = computed(() => {
|
|||||||
return 'bell-fill'
|
return 'bell-fill'
|
||||||
})
|
})
|
||||||
|
|
||||||
const date = computed(() => props.createdAt)
|
|
||||||
|
|
||||||
const collapseId = computed(() => 'collapse' + String(props.id))
|
const collapseId = computed(() => 'collapse' + String(props.id))
|
||||||
|
|
||||||
const username = computed(() => ({
|
const username = computed(() => ({
|
||||||
@ -233,6 +231,8 @@ const username = computed(() => ({
|
|||||||
initials: `${props.firstName[0]}${props.lastName[0]}`,
|
initials: `${props.firstName[0]}${props.lastName[0]}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const hours = computed(() => parseFloat((props.amount / GDD_PER_HOUR).toFixed(2)))
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => visible.value,
|
() => visible.value,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@ -32,29 +32,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
export default {
|
<script setup>
|
||||||
name: 'OpenCreationsAmount',
|
import { computed } from 'vue'
|
||||||
props: {
|
import { GDD_PER_HOUR } from '../../constants'
|
||||||
minimalDate: { type: Date, required: true },
|
|
||||||
maxGddLastMonth: { type: Number, required: true },
|
const props = defineProps({
|
||||||
maxGddThisMonth: { type: Number, required: true },
|
minimalDate: { type: Date, required: true },
|
||||||
},
|
maxGddLastMonth: { type: Number, required: true },
|
||||||
computed: {
|
maxGddThisMonth: { type: Number, required: true },
|
||||||
hoursSubmittedThisMonth() {
|
})
|
||||||
return (1000 - this.maxGddThisMonth) / 20
|
|
||||||
},
|
function afterComma(input) {
|
||||||
hoursSubmittedLastMonth() {
|
return parseFloat(input.toFixed(2)).toString()
|
||||||
return (1000 - this.maxGddLastMonth) / 20
|
|
||||||
},
|
|
||||||
hoursAvailableThisMonth() {
|
|
||||||
return this.maxGddThisMonth / 20
|
|
||||||
},
|
|
||||||
hoursAvailableLastMonth() {
|
|
||||||
return this.maxGddLastMonth / 20
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hoursSubmittedThisMonth = computed(() =>
|
||||||
|
afterComma((1000 - props.maxGddThisMonth) / GDD_PER_HOUR),
|
||||||
|
)
|
||||||
|
const hoursSubmittedLastMonth = computed(() =>
|
||||||
|
afterComma((1000 - props.maxGddLastMonth) / GDD_PER_HOUR),
|
||||||
|
)
|
||||||
|
const hoursAvailableThisMonth = computed(() => afterComma(props.maxGddThisMonth / GDD_PER_HOUR))
|
||||||
|
const hoursAvailableLastMonth = computed(() => afterComma(props.maxGddLastMonth / GDD_PER_HOUR))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -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,54 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="input-hour">
|
|
||||||
<BFormGroup :label="label" :label-for="labelFor">
|
|
||||||
<BFormInput
|
|
||||||
:id="labelFor"
|
|
||||||
:model-value="currentValue"
|
|
||||||
:name="name"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
type="number"
|
|
||||||
:state="meta.valid"
|
|
||||||
step="0.25"
|
|
||||||
min="0"
|
|
||||||
:max="validMaxTime"
|
|
||||||
class="bg-248"
|
|
||||||
@update:model-value="currentValue = $event"
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
@ -8,6 +8,12 @@ vi.mock('vee-validate', () => ({
|
|||||||
useField: vi.fn(),
|
useField: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('InputTextarea', () => {
|
describe('InputTextarea', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
@update:modelValue="currentValue = $event"
|
@update:modelValue="currentValue = $event"
|
||||||
/>
|
/>
|
||||||
<BFormInvalidFeedback v-if="errorMessage">
|
<BFormInvalidFeedback v-if="errorMessage">
|
||||||
{{ errorMessage }}
|
{{ translatedErrorString }}
|
||||||
</BFormInvalidFeedback>
|
</BFormInvalidFeedback>
|
||||||
</BFormGroup>
|
</BFormGroup>
|
||||||
</div>
|
</div>
|
||||||
@ -25,6 +25,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useField } from 'vee-validate'
|
import { useField } from 'vee-validate'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { translateYupErrorString } from '@/validationSchemas'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rules: {
|
rules: {
|
||||||
@ -50,7 +52,8 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
|
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
|
||||||
|
const { t } = useI18n()
|
||||||
|
const translatedErrorString = computed(() => translateYupErrorString(errorMessage, t))
|
||||||
const labelFor = computed(() => `${props.name}-input-field`)
|
const labelFor = computed(() => `${props.name}-input-field`)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
45
frontend/src/components/Inputs/LabeledInput.vue
Normal file
45
frontend/src/components/Inputs/LabeledInput.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="wrapperClassName">
|
||||||
|
<BFormGroup :label="label" :label-for="labelFor">
|
||||||
|
<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" />
|
||||||
|
<slot></slot>
|
||||||
|
</BFormGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineOptions, defineModel } from 'vue'
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = defineModel()
|
||||||
|
|
||||||
|
const wrapperClassName = computed(() => (props.name ? `input-${props.name}` : 'input'))
|
||||||
|
const labelFor = computed(() => `${props.name}-input-field`)
|
||||||
|
</script>
|
||||||
82
frontend/src/components/Inputs/ValidatedInput.vue
Normal file
82
frontend/src/components/Inputs/ValidatedInput.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<LabeledInput
|
||||||
|
v-bind="$attrs"
|
||||||
|
:min="minValue"
|
||||||
|
:max="maxValue"
|
||||||
|
:model-value="model"
|
||||||
|
:reset-value="resetValue"
|
||||||
|
:locale="$i18n.locale"
|
||||||
|
:required="!isOptional"
|
||||||
|
:label="label"
|
||||||
|
:name="name"
|
||||||
|
:state="valid"
|
||||||
|
@update:modelValue="updateValue"
|
||||||
|
>
|
||||||
|
<BFormInvalidFeedback v-if="errorMessage">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</BFormInvalidFeedback>
|
||||||
|
</LabeledInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import LabeledInput from './LabeledInput'
|
||||||
|
import { translateYupErrorString } from '@/validationSchemas'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: [String, Number, Date],
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const model = ref(props.modelValue)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return translateYupErrorString(e.message, t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 minValue = computed(() => getTestParameter('min'))
|
||||||
|
const maxValue = computed(() => getTestParameter('max'))
|
||||||
|
const resetValue = computed(() => schemaDescription.value.default)
|
||||||
|
const isOptional = computed(() => schemaDescription.value.optional)
|
||||||
|
</script>
|
||||||
1
frontend/src/constants.js
Normal file
1
frontend/src/constants.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const GDD_PER_HOUR = 20
|
||||||
@ -76,7 +76,6 @@
|
|||||||
"allContributions": "Es wurden noch keine Beiträge eingereicht.",
|
"allContributions": "Es wurden noch keine Beiträge eingereicht.",
|
||||||
"myContributions": "Du hast noch keine Beiträge eingereicht."
|
"myContributions": "Du hast noch keine Beiträge eingereicht."
|
||||||
},
|
},
|
||||||
"noDateSelected": "Wähle irgendein Datum im Monat",
|
|
||||||
"noOpenCreation": {
|
"noOpenCreation": {
|
||||||
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
|
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
|
||||||
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
|
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
|
||||||
@ -185,9 +184,17 @@
|
|||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"username-placeholder": "Wähle deinen Benutzernamen",
|
"username-placeholder": "Wähle deinen Benutzernamen",
|
||||||
"validation": {
|
"validation": {
|
||||||
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
|
"gddCreationTime": {
|
||||||
|
"min": "Die Stunden sollten mindestens {min} groß sein",
|
||||||
|
"max": "Die Stunden sollten höchstens {max} groß sein",
|
||||||
|
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten"
|
||||||
|
},
|
||||||
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
|
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
|
||||||
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
|
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
|
||||||
|
"memo": {
|
||||||
|
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein",
|
||||||
|
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein"
|
||||||
|
},
|
||||||
"requiredField": "{fieldName} ist ein Pflichtfeld",
|
"requiredField": "{fieldName} ist ein Pflichtfeld",
|
||||||
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
|
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
|
||||||
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
|
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
|
||||||
|
|||||||
@ -76,7 +76,6 @@
|
|||||||
"allContributions": "No contributions have been submitted yet.",
|
"allContributions": "No contributions have been submitted yet.",
|
||||||
"myContributions": "You have not submitted any entries yet."
|
"myContributions": "You have not submitted any entries yet."
|
||||||
},
|
},
|
||||||
"noDateSelected": "Choose any date in the month",
|
|
||||||
"noOpenCreation": {
|
"noOpenCreation": {
|
||||||
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
|
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
|
||||||
"lastMonth": "The creation limit is reached for the selected month.",
|
"lastMonth": "The creation limit is reached for the selected month.",
|
||||||
@ -185,9 +184,17 @@
|
|||||||
"username": "Username",
|
"username": "Username",
|
||||||
"username-placeholder": "Choose your username",
|
"username-placeholder": "Choose your username",
|
||||||
"validation": {
|
"validation": {
|
||||||
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
|
"gddCreationTime": {
|
||||||
|
"min": "The hours should be at least {min} in size",
|
||||||
|
"max": "The hours should not be larger than {max}",
|
||||||
|
"decimal-places": "The hours should contain a maximum of two decimal places"
|
||||||
|
},
|
||||||
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
|
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
|
||||||
"is-not": "You cannot send Gradidos to yourself",
|
"is-not": "You cannot send Gradidos to yourself",
|
||||||
|
"memo": {
|
||||||
|
"min": "The job description should be at least {min} characters long",
|
||||||
|
"max": "The job description should not be longer than {max} characters"
|
||||||
|
},
|
||||||
"requiredField": "The {fieldName} field is required",
|
"requiredField": "The {fieldName} field is required",
|
||||||
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
|
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
|
||||||
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
|
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
|
||||||
|
|||||||
@ -10,9 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="mb-3"></div>
|
<div class="mb-3"></div>
|
||||||
<contribution-form
|
<contribution-form
|
||||||
:key="computedKeyFromForm"
|
|
||||||
v-model="form"
|
v-model="form"
|
||||||
:is-this-month="isThisMonth"
|
|
||||||
:minimal-date="minimalDate"
|
:minimal-date="minimalDate"
|
||||||
:max-gdd-last-month="maxForMonths[0]"
|
:max-gdd-last-month="maxForMonths[0]"
|
||||||
:max-gdd-this-month="maxForMonths[1]"
|
:max-gdd-this-month="maxForMonths[1]"
|
||||||
@ -70,6 +68,7 @@ import { createContribution, updateContribution, deleteContribution } from '@/gr
|
|||||||
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||||
import { useAppToast } from '@/composables/useToast'
|
import { useAppToast } from '@/composables/useToast'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { GDD_PER_HOUR } from '../constants'
|
||||||
|
|
||||||
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
|
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
|
||||||
|
|
||||||
@ -92,10 +91,10 @@ const contributionCount = ref(0)
|
|||||||
const contributionCountAll = ref(0)
|
const contributionCountAll = ref(0)
|
||||||
const form = ref({
|
const form = ref({
|
||||||
id: null,
|
id: null,
|
||||||
date: '',
|
date: undefined,
|
||||||
memo: '',
|
memo: '',
|
||||||
hours: 0,
|
hours: '',
|
||||||
amount: '',
|
amount: GDD_PER_HOUR,
|
||||||
})
|
})
|
||||||
const originalContributionDate = ref('')
|
const originalContributionDate = ref('')
|
||||||
const updateAmount = ref('')
|
const updateAmount = ref('')
|
||||||
@ -107,15 +106,7 @@ const minimalDate = computed(() => {
|
|||||||
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
||||||
})
|
})
|
||||||
|
|
||||||
const isThisMonth = computed(() => {
|
const amountToAdd = computed(() => (form.value.id ? parseFloat(updateAmount.value) : 0.0))
|
||||||
const formDate = new Date(form.value.date)
|
|
||||||
return (
|
|
||||||
formDate.getFullYear() === maximalDate.value.getFullYear() &&
|
|
||||||
formDate.getMonth() === maximalDate.value.getMonth()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const amountToAdd = computed(() => (form.value.id ? parseInt(updateAmount.value) : 0))
|
|
||||||
|
|
||||||
const maxForMonths = computed(() => {
|
const maxForMonths = computed(() => {
|
||||||
const originalDate = new Date(originalContributionDate.value)
|
const originalDate = new Date(originalContributionDate.value)
|
||||||
@ -125,18 +116,13 @@ const maxForMonths = computed(() => {
|
|||||||
creation.year === originalDate.getFullYear() &&
|
creation.year === originalDate.getFullYear() &&
|
||||||
creation.month === originalDate.getMonth()
|
creation.month === originalDate.getMonth()
|
||||||
) {
|
) {
|
||||||
return parseInt(creation.amount) + amountToAdd.value
|
return parseFloat(creation.amount) + amountToAdd.value
|
||||||
}
|
}
|
||||||
return parseInt(creation.amount)
|
return parseFloat(creation.amount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return [0, 0]
|
return [0, 0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const computedKeyFromForm = computed(() => {
|
|
||||||
return `${form.value.id}_${form.value.date}_${form.value.memo}_${form.value.amount}_${form.value.hours}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const { onResult: onOpenCreationsResult, refetch: refetchOpenCreations } = useQuery(
|
const { onResult: onOpenCreationsResult, refetch: refetchOpenCreations } = useQuery(
|
||||||
openCreations,
|
openCreations,
|
||||||
() => ({}),
|
() => ({}),
|
||||||
@ -279,7 +265,7 @@ const handleUpdateContributionForm = (item) => {
|
|||||||
memo: item.memo,
|
memo: item.memo,
|
||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
hours: item.amount / 20,
|
hours: item.amount / 20,
|
||||||
}
|
} //* /
|
||||||
originalContributionDate.value = item.contributionDate
|
originalContributionDate.value = item.contributionDate
|
||||||
updateAmount.value = item.amount
|
updateAmount.value = item.amount
|
||||||
tabIndex.value = 0
|
tabIndex.value = 0
|
||||||
|
|||||||
21
frontend/src/validationSchemas.js
Normal file
21
frontend/src/validationSchemas.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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 translateYupErrorString = (error, t) => {
|
||||||
|
const type = typeof error
|
||||||
|
if (type === 'object') {
|
||||||
|
return t(error.key, error.values)
|
||||||
|
} else if (type === 'string' && error.length > 0 && isLanguageKey(error)) {
|
||||||
|
return t(error)
|
||||||
|
} else {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } }))
|
||||||
@ -1658,13 +1658,12 @@
|
|||||||
vee-validate "4.14.7"
|
vee-validate "4.14.7"
|
||||||
|
|
||||||
"@vee-validate/yup@^4.14.1":
|
"@vee-validate/yup@^4.14.1":
|
||||||
version "4.14.7"
|
version "4.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vee-validate/yup/-/yup-4.14.7.tgz#a029151394ae4fbc7a038dbb49acc86f2ba78ddc"
|
resolved "https://registry.yarnpkg.com/@vee-validate/yup/-/yup-4.15.0.tgz#409f9b57414fadd5b86bc6ada18cd51a7ccd121c"
|
||||||
integrity sha512-sMLkSXbVWIFK0BE8gEp2Gcdd3aqpTggBjbkrYmcdgyHBeYoPmhBHhUpkXDFhmsckie2xv6lNTicGO5oJt71N1Q==
|
integrity sha512-paK2ZdxZJRrUGwqaqf7KMNC+n5C7UGs7DofK7wZCza/zKT/QtFSxVYgopGoYYrbAfd6DpVmNpf/ouBuRdPBthA==
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^4.8.3"
|
type-fest "^4.8.3"
|
||||||
vee-validate "4.14.7"
|
vee-validate "4.15.0"
|
||||||
yup "^1.3.2"
|
|
||||||
|
|
||||||
"@vitejs/plugin-vue@5.1.4":
|
"@vitejs/plugin-vue@5.1.4":
|
||||||
version "5.1.4"
|
version "5.1.4"
|
||||||
@ -7126,6 +7125,14 @@ vee-validate@4.14.7, vee-validate@^4.13.2:
|
|||||||
"@vue/devtools-api" "^7.5.2"
|
"@vue/devtools-api" "^7.5.2"
|
||||||
type-fest "^4.8.3"
|
type-fest "^4.8.3"
|
||||||
|
|
||||||
|
vee-validate@4.15.0:
|
||||||
|
version "4.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vee-validate/-/vee-validate-4.15.0.tgz#eb77a9c867669d34abbc33ca5e16f2a991eb7ad5"
|
||||||
|
integrity sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^7.5.2"
|
||||||
|
type-fest "^4.8.3"
|
||||||
|
|
||||||
vite-node@2.1.8:
|
vite-node@2.1.8:
|
||||||
version "2.1.8"
|
version "2.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5"
|
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5"
|
||||||
@ -7502,7 +7509,7 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
yup@^1.3.2, yup@^1.4.0:
|
yup@^1.4.0:
|
||||||
version "1.6.1"
|
version "1.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/yup/-/yup-1.6.1.tgz#8defcff9daaf9feac178029c0e13b616563ada4b"
|
resolved "https://registry.yarnpkg.com/yup/-/yup-1.6.1.tgz#8defcff9daaf9feac178029c0e13b616563ada4b"
|
||||||
integrity sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==
|
integrity sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user