diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js index f19459ce9..3638f5180 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js +++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js @@ -14,6 +14,7 @@ describe('ContributionMessagesFormular', () => { const propsData = { contributionId: 42, contributionMemo: 'It is a test memo', + hideResubmission: true, } const mocks = { @@ -95,13 +96,14 @@ describe('ContributionMessagesFormular', () => { await wrapper.find('button[data-test="submit-dialog"]').trigger('click') }) - it('moderatorMesage has `DIALOG`', () => { + it('moderatorMessage has `DIALOG`', () => { expect(apolloMutateMock).toBeCalledWith({ mutation: adminCreateContributionMessage, variables: { contributionId: 42, message: 'text form message', messageType: 'DIALOG', + resubmissionAt: null, }, }) }) @@ -128,6 +130,7 @@ describe('ContributionMessagesFormular', () => { contributionId: 42, message: 'text form message', messageType: 'MODERATOR', + resubmissionAt: null, }, }) }) @@ -137,6 +140,53 @@ describe('ContributionMessagesFormular', () => { }) }) + describe('send resubmission contribution message with success', () => { + const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days in milliseconds + + beforeEach(async () => { + await wrapper.setData({ + form: { + text: 'text form message', + }, + showResubmissionDate: true, + resubmissionDate: futureDate, + resubmissionTime: '08:46', + }) + await wrapper.find('button[data-test="submit-moderator"]').trigger('click') + }) + + it('graphql payload contain resubmission date', () => { + const futureDateExactTime = futureDate + futureDateExactTime.setHours(8) + futureDateExactTime.setMinutes(46) + expect(apolloMutateMock).toBeCalledWith({ + mutation: adminCreateContributionMessage, + variables: { + contributionId: 42, + message: 'text form message', + messageType: 'MODERATOR', + resubmissionAt: futureDateExactTime.toString(), + }, + }) + }) + + it('toasts an success message', () => { + expect(toastSuccessSpy).toBeCalledWith('message.request') + }) + }) + + describe('set memo', () => { + beforeEach(async () => { + await wrapper.setData({ + chatOrMemo: 0, + }) + await wrapper.find('button[data-test="submit-memo"]').trigger('click') + }) + it('check chatOrMemo value is 1', () => { + expect(wrapper.vm.chatOrMemo).toBe(1) + }) + }) + describe('update contribution memo from moderator for user created contributions', () => { beforeEach(async () => { await wrapper.setData({ diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue index 1e395c183..9b27f34a8 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -4,6 +4,15 @@ + + + {{ $t('moderator.show-submission-form') }} + + + + + + import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage' import { adminUpdateContribution } from '@/graphql/adminUpdateContribution' +import TimePicker from '@/components/input/TimePicker' export default { + components: { + TimePicker, + }, name: 'ContributionMessagesFormular', props: { contributionId: { @@ -77,6 +90,10 @@ export default { type: String, required: true, }, + hideResubmission: { + type: Boolean, + required: true, + }, }, data() { return { @@ -85,6 +102,9 @@ export default { memo: this.contributionMemo, }, loading: false, + resubmissionDate: null, + resubmissionTime: '00:00', + showResubmissionDate: false, chatOrMemo: 0, // 0 = Chat, 1 = Memo messageType: { DIALOG: 'DIALOG', @@ -93,6 +113,13 @@ export default { } }, methods: { + combineResubmissionDateAndTime() { + const formattedDate = new Date(this.resubmissionDate) + const [hours, minutes] = this.resubmissionTime.split(':') + formattedDate.setHours(parseInt(hours)) + formattedDate.setMinutes(parseInt(minutes)) + return formattedDate + }, onSubmit(mType) { this.loading = true if (this.chatOrMemo === 0) { @@ -103,12 +130,23 @@ export default { contributionId: this.contributionId, message: this.form.text, messageType: mType, + resubmissionAt: this.showResubmissionDate + ? this.combineResubmissionDateAndTime().toString() + : null, }, }) .then((result) => { - this.$emit('get-list-contribution-messages', this.contributionId) - this.$emit('update-status', this.contributionId) - this.form.text = '' + if ( + this.hideResubmission && + this.showResubmissionDate && + this.combineResubmissionDateAndTime() > new Date() + ) { + this.$emit('update-contributions') + } else { + this.$emit('get-list-contribution-messages', this.contributionId) + this.$emit('update-status', this.contributionId) + } + this.onReset() this.toastSuccess(this.$t('message.request')) this.loading = false }) @@ -141,6 +179,9 @@ export default { onReset(event) { this.form.text = '' this.form.memo = this.contributionMemo + this.showResubmissionDate = false + this.resubmissionDate = null + this.resubmissionTime = '00:00' }, enableMemo() { this.chatOrMemo = 1 @@ -151,7 +192,8 @@ export default { return ( (this.chatOrMemo === 0 && this.form.text === '') || this.loading || - (this.chatOrMemo === 1 && this.form.memo.length < 5) + (this.chatOrMemo === 1 && this.form.memo.length < 5) || + (this.showResubmissionDate && !this.resubmissionDate) ) }, moderatorDisabled() { diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js index b38c4e7d4..fe91abe6c 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js +++ b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js @@ -89,6 +89,7 @@ describe('ContributionMessagesList', () => { contributionMemo: 'test memo', contributionUserId: 108, contributionStatus: 'PENDING', + hideResubmission: true, } const mocks = { @@ -155,5 +156,15 @@ describe('ContributionMessagesList', () => { expect(wrapper.emitted('reload-contribution')[0]).toEqual([3]) }) }) + + describe('test update-contributions', () => { + beforeEach(() => { + wrapper.vm.updateContributions() + }) + + it('emits update-contributions', () => { + expect(wrapper.emitted('update-contributions')).toBeTruthy() + }) + }) }) }) diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.vue b/admin/src/components/ContributionMessages/ContributionMessagesList.vue index c6bed086d..229fe6e04 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesList.vue +++ b/admin/src/components/ContributionMessages/ContributionMessagesList.vue @@ -12,9 +12,11 @@ @@ -47,6 +49,10 @@ export default { type: Number, required: true, }, + hideResubmission: { + type: Boolean, + required: true, + }, }, data() { return { @@ -79,6 +85,9 @@ export default { reloadContribution(id) { this.$emit('reload-contribution', id) }, + updateContributions() { + this.$emit('update-contributions') + }, }, } diff --git a/admin/src/components/Tables/OpenCreationsTable.spec.js b/admin/src/components/Tables/OpenCreationsTable.spec.js index 054ef9067..6babe9956 100644 --- a/admin/src/components/Tables/OpenCreationsTable.spec.js +++ b/admin/src/components/Tables/OpenCreationsTable.spec.js @@ -70,6 +70,7 @@ const propsData = { { key: 'confirm', label: 'save' }, ], toggleDetails: false, + hideResubmission: true, } const mocks = { diff --git a/admin/src/components/Tables/OpenCreationsTable.vue b/admin/src/components/Tables/OpenCreationsTable.vue index f351fe228..747ef572b 100644 --- a/admin/src/components/Tables/OpenCreationsTable.vue +++ b/admin/src/components/Tables/OpenCreationsTable.vue @@ -112,8 +112,10 @@ :contributionStatus="row.item.status" :contributionUserId="row.item.userId" :contributionMemo="row.item.memo" + :hideResubmission="hideResubmission" @update-status="updateStatus" @reload-contribution="reloadContribution" + @update-contributions="updateContributions" /> @@ -154,6 +156,10 @@ export default { type: Array, required: true, }, + hideResubmission: { + type: Boolean, + required: true, + }, }, methods: { myself(item) { @@ -176,6 +182,9 @@ export default { reloadContribution(id) { this.$emit('reload-contribution', id) }, + updateContributions() { + this.$emit('update-contributions') + }, }, } diff --git a/admin/src/components/input/TimePicker.spec.js b/admin/src/components/input/TimePicker.spec.js new file mode 100644 index 000000000..82f51a969 --- /dev/null +++ b/admin/src/components/input/TimePicker.spec.js @@ -0,0 +1,63 @@ +import { mount } from '@vue/test-utils' +import TimePicker from './TimePicker.vue' + +describe('TimePicker', () => { + it('updates timeValue on input and emits input event', async () => { + const wrapper = mount(TimePicker, { + propsData: { + value: '12:34', // Set an initial value for testing + }, + }) + + const input = wrapper.find('input[type="text"]') + + // Simulate user input + await input.setValue('23:45') + + // Check if timeValue is updated + expect(wrapper.vm.timeValue).toBe('23:45') + + // Check if input event is emitted with updated value + expect(wrapper.emitted().input).toBeTruthy() + expect(wrapper.emitted().input[0]).toEqual(['23:45']) + }) + + it('validates and corrects time format on blur', async () => { + const wrapper = mount(TimePicker) + + const input = wrapper.find('input[type="text"]') + + // Simulate user input + await input.setValue('99:99') + expect(wrapper.emitted().input).toBeTruthy() + expect(wrapper.emitted().input[0]).toEqual(['99:99']) + + // Trigger blur event + await input.trigger('blur') + + // Check if timeValue is corrected to valid format + expect(wrapper.vm.timeValue).toBe('23:59') // Maximum allowed value for hours and minutes + + // Check if input event is emitted with corrected value + expect(wrapper.emitted().input).toBeTruthy() + expect(wrapper.emitted().input[1]).toEqual(['23:59']) + }) + + it('check handling of empty input', async () => { + const wrapper = mount(TimePicker) + const input = wrapper.find('input[type="text"]') + + // Simulate user input with non-numeric characters + await input.setValue('') + + // Trigger blur event + await input.trigger('blur') + + // Check if non-numeric characters are filtered out + expect(wrapper.vm.timeValue).toBe('00:00') + + // Check if input event is emitted with filtered value + expect(wrapper.emitted().input).toBeTruthy() + expect(wrapper.emitted().input[1]).toEqual(['00:00']) + }) +}) diff --git a/admin/src/components/input/TimePicker.vue b/admin/src/components/input/TimePicker.vue new file mode 100644 index 000000000..e8fb416ad --- /dev/null +++ b/admin/src/components/input/TimePicker.vue @@ -0,0 +1,48 @@ + + + diff --git a/admin/src/graphql/adminCreateContributionMessage.js b/admin/src/graphql/adminCreateContributionMessage.js index 66750b833..df7ca5458 100644 --- a/admin/src/graphql/adminCreateContributionMessage.js +++ b/admin/src/graphql/adminCreateContributionMessage.js @@ -1,11 +1,17 @@ import gql from 'graphql-tag' export const adminCreateContributionMessage = gql` - mutation ($contributionId: Int!, $message: String!, $messageType: ContributionMessageType) { + mutation ( + $contributionId: Int! + $message: String! + $messageType: ContributionMessageType + $resubmissionAt: String + ) { adminCreateContributionMessage( contributionId: $contributionId message: $message messageType: $messageType + resubmissionAt: $resubmissionAt ) { id message diff --git a/admin/src/graphql/adminListContributions.js b/admin/src/graphql/adminListContributions.js index e11ebfa05..5daa742b5 100644 --- a/admin/src/graphql/adminListContributions.js +++ b/admin/src/graphql/adminListContributions.js @@ -9,6 +9,7 @@ export const adminListContributions = gql` $userId: Int $query: String $noHashtag: Boolean + $hideResubmission: Boolean ) { adminListContributions( currentPage: $currentPage @@ -18,6 +19,7 @@ export const adminListContributions = gql` userId: $userId query: $query noHashtag: $noHashtag + hideResubmission: $hideResubmission ) { contributionCount contributionList { diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 264029cc6..5b6fefc47 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -99,6 +99,8 @@ } }, "hide_details": "Details verbergen", + "hide_resubmission": "Wiedervorlage verbergen", + "hide_resubmission_tooltip": "Verbirgt alle Schöpfungen für die ein Moderator ein Erinnerungsdatum festgelegt hat.", "lastname": "Nachname", "math": { "equals": "=", @@ -111,6 +113,7 @@ "moderator": { "chat": "Chat", "history": "Die Daten wurden geändert. Dies sind die alten Daten.", + "show-submission-form": "warten auf Erinnerung?", "moderator": "Moderator", "notice": "Moderator Notiz", "memo": "Memo", diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index dbd831bb9..57d375c77 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -99,6 +99,8 @@ } }, "hide_details": "Hide details", + "hide_resubmission": "Hide resubmission", + "hide_resubmission_tooltip": "Hides all creations for which a moderator has set a reminder date.", "lastname": "Lastname", "math": { "equals": "=", @@ -111,6 +113,7 @@ "moderator": { "chat": "Chat", "history": "The data has been changed. This is the old data.", + "show-submission-form": "wait for reminder?", "moderator": "Moderator", "notice": "Moderator note", "memo": "Memo", diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js index 4842c8b3b..cba169655 100644 --- a/admin/src/pages/CreationConfirm.spec.js +++ b/admin/src/pages/CreationConfirm.spec.js @@ -347,6 +347,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: false, noHashtag: null, order: 'DESC', pageSize: 25, @@ -364,6 +365,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: true, noHashtag: null, order: 'DESC', pageSize: 25, @@ -382,6 +384,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: false, noHashtag: null, order: 'DESC', pageSize: 25, @@ -400,6 +403,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: false, noHashtag: null, order: 'DESC', pageSize: 25, @@ -418,6 +422,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: false, noHashtag: null, order: 'DESC', pageSize: 25, @@ -440,6 +445,7 @@ describe('CreationConfirm', () => { it('calls the API again', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 2, + hideResubmission: false, noHashtag: null, order: 'DESC', pageSize: 25, @@ -457,6 +463,7 @@ describe('CreationConfirm', () => { it('refetches contributions with proper filter and current page = 1', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: true, noHashtag: null, order: 'DESC', pageSize: 25, @@ -480,6 +487,7 @@ describe('CreationConfirm', () => { it('calls the API with query', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: true, noHashtag: null, order: 'DESC', pageSize: 25, @@ -496,6 +504,7 @@ describe('CreationConfirm', () => { it('calls the API with empty query', () => { expect(adminListContributionsMock).toBeCalledWith({ currentPage: 1, + hideResubmission: true, noHashtag: null, order: 'DESC', pageSize: 25, diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue index 3ca382c43..a0cd9a8a3 100644 --- a/admin/src/pages/CreationConfirm.vue +++ b/admin/src/pages/CreationConfirm.vue @@ -2,10 +2,16 @@