mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
refactor contributions in frontend
This commit is contained in:
parent
2cabeb87bb
commit
c4238a897c
@ -34,8 +34,8 @@
|
||||
{{ t('help.transactionlist.confirmed') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t('transactionlist.status') }} {{ t('math.equals') }}
|
||||
{{ t('help.transactionlist.status') }}
|
||||
{{ t('transactionlist.contributionStatus') }} {{ t('math.equals') }}
|
||||
{{ t('help.transactionlist.contributionStatus') }}
|
||||
</div>
|
||||
</BCollapse>
|
||||
</div>
|
||||
|
||||
@ -25,7 +25,7 @@ query adminListContributions(
|
||||
confirmedBy
|
||||
updatedAt
|
||||
updatedBy
|
||||
status
|
||||
contributionStatus
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
@ -54,7 +54,7 @@ query adminListContributionsShort(
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
status
|
||||
contributionStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,7 +224,7 @@ const fields = computed(
|
||||
],
|
||||
// all contributions
|
||||
[
|
||||
{ key: 'status', label: t('status') },
|
||||
{ key: 'contributionStatus', label: t('status') },
|
||||
baseFields.firstName,
|
||||
baseFields.lastName,
|
||||
baseFields.amount,
|
||||
@ -425,7 +425,7 @@ const updateStatus = (id) => {
|
||||
const target = items.value.find((obj) => obj.id === id)
|
||||
if (target) {
|
||||
target.messagesCount++
|
||||
target.status = 'IN_PROGRESS'
|
||||
target.contributionStatus = 'IN_PROGRESS'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -20,5 +20,5 @@ export class ContributionArgs {
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
creationDate: string
|
||||
contributionDate: string
|
||||
}
|
||||
|
||||
@ -1,64 +1,27 @@
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { ContributionMessage as dbContributionMessage } from '@entity/ContributionMessage'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { Field, Int, ObjectType } from 'type-graphql'
|
||||
|
||||
import { ContributionMessage } from './ContributionMessage'
|
||||
import { User } from './User'
|
||||
import { UnconfirmedContribution } from './UnconfirmedContribution'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
export class Contribution extends UnconfirmedContribution {
|
||||
constructor(contribution: dbContribution) {
|
||||
const user = contribution.user
|
||||
this.id = contribution.id
|
||||
this.firstName = user?.firstName ?? null
|
||||
this.lastName = user?.lastName ?? null
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
super(contribution)
|
||||
this.createdAt = contribution.createdAt
|
||||
this.confirmedAt = contribution.confirmedAt
|
||||
this.confirmedBy = contribution.confirmedBy
|
||||
this.contributionDate = contribution.contributionDate
|
||||
this.status = contribution.contributionStatus
|
||||
this.messagesCount = contribution.messages?.length ?? 0
|
||||
|
||||
this.deniedAt = contribution.deniedAt
|
||||
this.deniedBy = contribution.deniedBy
|
||||
this.deletedAt = contribution.deletedAt
|
||||
this.deletedBy = contribution.deletedBy
|
||||
this.updatedAt = contribution.updatedAt
|
||||
this.updatedBy = contribution.updatedBy
|
||||
this.moderatorId = contribution.moderatorId
|
||||
this.userId = contribution.userId
|
||||
this.resubmissionAt = contribution.resubmissionAt
|
||||
this.user = user ? new User(user) : null
|
||||
this.messages = contribution.messages
|
||||
? contribution.messages.map(
|
||||
(message: dbContributionMessage) => new ContributionMessage(message),
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId: number | null
|
||||
|
||||
@Field(() => User, { nullable: true })
|
||||
user: User | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName: string | null
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@ -87,19 +50,7 @@ export class Contribution {
|
||||
updatedBy: number | null
|
||||
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
|
||||
@Field(() => Int)
|
||||
messagesCount: number
|
||||
|
||||
@Field(() => [ContributionMessage], { nullable: true })
|
||||
messages: ContributionMessage[] | null
|
||||
|
||||
@Field(() => String)
|
||||
status: string
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
moderatorId: number | null
|
||||
contributionDate: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
resubmissionAt: Date | null
|
||||
|
||||
@ -1,42 +1,44 @@
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
import { ContributionMessage as dbContributionMessage } from '@entity/ContributionMessage'
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Field, Int, ObjectType } from 'type-graphql'
|
||||
|
||||
import { ContributionMessage } from './ContributionMessage'
|
||||
import { User } from './User'
|
||||
|
||||
@ObjectType()
|
||||
export class UnconfirmedContribution {
|
||||
constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) {
|
||||
constructor(contribution: Contribution) {
|
||||
const user = contribution.user
|
||||
this.id = contribution.id
|
||||
this.userId = contribution.userId
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.date = contribution.contributionDate
|
||||
this.firstName = user ? user.firstName : ''
|
||||
this.lastName = user ? user.lastName : ''
|
||||
this.email = user ? user.emailContact.email : ''
|
||||
this.moderator = contribution.moderatorId
|
||||
this.creation = creations
|
||||
this.status = contribution.contributionStatus
|
||||
this.messageCount = contribution.messages ? contribution.messages.length : 0
|
||||
}
|
||||
this.contributionDate = contribution.contributionDate
|
||||
this.user = user ? new User(user) : null
|
||||
this.moderatorId = contribution.moderatorId
|
||||
this.contributionStatus = contribution.contributionStatus
|
||||
this.messagesCount = contribution.messages ? contribution.messages.length : 0
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
this.messages = contribution.messages
|
||||
? contribution.messages.map(
|
||||
(message: dbContributionMessage) => new ContributionMessage(message),
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId: number | null
|
||||
|
||||
@Field(() => Int)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
@Field(() => User, { nullable: true })
|
||||
user: User | null
|
||||
|
||||
@Field(() => Date)
|
||||
date: Date
|
||||
contributionDate: Date
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
@ -45,14 +47,14 @@ export class UnconfirmedContribution {
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
moderator: number | null
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
moderatorId: number | null
|
||||
|
||||
@Field(() => String)
|
||||
status: string
|
||||
contributionStatus: string
|
||||
|
||||
@Field(() => Int)
|
||||
messageCount: number
|
||||
messagesCount: number
|
||||
|
||||
@Field(() => [ContributionMessage], { nullable: true })
|
||||
messages: ContributionMessage[] | null
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ export class ContributionResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||
@Mutation(() => UnconfirmedContribution)
|
||||
async createContribution(
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Args() { amount, memo, contributionDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
@ -93,14 +93,14 @@ export class ContributionResolver {
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
validateContribution(creations, amount, creationDateObj, clientTimezoneOffset)
|
||||
const contributionDateObj = new Date(contributionDate)
|
||||
validateContribution(creations, amount, contributionDateObj, clientTimezoneOffset)
|
||||
|
||||
const contribution = DbContribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.contributionDate = contributionDateObj
|
||||
contribution.memo = memo
|
||||
contribution.contributionType = ContributionType.USER
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
@ -109,7 +109,7 @@ export class ContributionResolver {
|
||||
await DbContribution.save(contribution)
|
||||
await EVENT_CONTRIBUTION_CREATE(user, contribution, amount)
|
||||
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
return new UnconfirmedContribution(contribution)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_CONTRIBUTION])
|
||||
@ -144,12 +144,12 @@ export class ContributionResolver {
|
||||
@Query(() => ContributionListResult)
|
||||
async listContributions(
|
||||
@Ctx() context: Context,
|
||||
@Args()
|
||||
paginated: Paginated,
|
||||
@Arg('pagination') pagination: Paginated,
|
||||
@Arg('messagePagination') messagePagination: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const user = getUser(context)
|
||||
const [dbContributions, count] = await loadUserContributions(user.id, paginated)
|
||||
|
||||
const [dbContributions, count] = await loadUserContributions(user.id, pagination, messagePagination)
|
||||
|
||||
// show contributions in progress first
|
||||
const inProgressContributions = dbContributions.filter(
|
||||
(contribution) => contribution.contributionStatus === ContributionStatus.IN_PROGRESS,
|
||||
@ -163,10 +163,10 @@ export class ContributionResolver {
|
||||
[...inProgressContributions, ...notInProgressContributions].map((contribution) => {
|
||||
// we currently expect not much contribution messages for needing pagination
|
||||
// but if we get more than expected, we should get warned
|
||||
if ((contribution.messages?.length || 0) > 10) {
|
||||
if ((contribution.messages?.length || 0) > messagePagination.pageSize) {
|
||||
logger.warn('more contribution messages as expected, consider pagination', {
|
||||
contributionId: contribution.id,
|
||||
expected: 10,
|
||||
expected: messagePagination.pageSize,
|
||||
actual: contribution.messages?.length || 0,
|
||||
})
|
||||
}
|
||||
@ -191,10 +191,9 @@ export class ContributionResolver {
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listAllContributions(
|
||||
@Args()
|
||||
paginated: Paginated,
|
||||
@Arg('pagination') pagination: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const [dbContributions, count] = await loadAllContributions(paginated)
|
||||
const [dbContributions, count] = await loadAllContributions(pagination)
|
||||
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
@ -215,8 +214,7 @@ export class ContributionResolver {
|
||||
contributionArgs,
|
||||
context,
|
||||
)
|
||||
const { contribution, contributionMessage, availableCreationSums } =
|
||||
await updateUnconfirmedContributionContext.run()
|
||||
const { contribution, contributionMessage } = await updateUnconfirmedContributionContext.run()
|
||||
await getConnection().transaction(async (transactionalEntityManager: EntityManager) => {
|
||||
await transactionalEntityManager.save(contribution)
|
||||
if (contributionMessage) {
|
||||
@ -226,7 +224,7 @@ export class ContributionResolver {
|
||||
const user = getUser(context)
|
||||
await EVENT_CONTRIBUTION_UPDATE(user, contribution, contributionArgs.amount)
|
||||
|
||||
return new UnconfirmedContribution(contribution, user, availableCreationSums)
|
||||
return new UnconfirmedContribution(contribution)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION])
|
||||
@ -629,25 +627,28 @@ export class ContributionResolver {
|
||||
|
||||
// Field resolvers
|
||||
@Authorized([RIGHTS.USER])
|
||||
@FieldResolver(() => User)
|
||||
@FieldResolver(() => User, { nullable: true })
|
||||
async user(
|
||||
@Root() contribution: DbContribution,
|
||||
@Info() info: GraphQLResolveInfo,
|
||||
): Promise<User> {
|
||||
let user = contribution.user
|
||||
): Promise<User | null> {
|
||||
let user: DbUser | null = contribution.user
|
||||
if (!user) {
|
||||
const queryBuilder = DbUser.createQueryBuilder('user')
|
||||
queryBuilder.where('user.id = :userId', { userId: contribution.userId })
|
||||
extractGraphQLFieldsForSelect(info, queryBuilder, 'user')
|
||||
user = await queryBuilder.getOneOrFail()
|
||||
user = await queryBuilder.getOne()
|
||||
}
|
||||
return new User(user)
|
||||
if (user) {
|
||||
return new User(user)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
|
||||
@FieldResolver(() => [ContributionMessage], { nullable: true })
|
||||
async messages(
|
||||
@Root() contribution: Contribution,
|
||||
@Root() contribution: UnconfirmedContribution,
|
||||
@Arg('pagination', () => Paginated) pagination: Paginated,
|
||||
): Promise<ContributionMessage[] | null> {
|
||||
if (contribution.messagesCount === 0) {
|
||||
|
||||
@ -73,6 +73,7 @@ export const getUserCreations = async (
|
||||
await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany()
|
||||
|
||||
logger.trace(sumAmountContributionPerUserAndLast3Month)
|
||||
console.log(JSON.stringify(sumAmountContributionPerUserAndLast3Month, null, 2))
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
|
||||
@ -3,9 +3,8 @@ import { FindManyOptions } from '@dbTools/typeorm'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
|
||||
function buildBaseOptions(paginated: Paginated): FindManyOptions<DbContribution> {
|
||||
const { order, currentPage, pageSize } = paginated
|
||||
const { currentPage, pageSize } = paginated
|
||||
return {
|
||||
order: { createdAt: order, id: order },
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}
|
||||
@ -19,10 +18,15 @@ function buildBaseOptions(paginated: Paginated): FindManyOptions<DbContribution>
|
||||
export const loadUserContributions = async (
|
||||
userId: number,
|
||||
paginated: Paginated,
|
||||
messagePagination: Paginated,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const { order } = paginated
|
||||
const { order: messageOrder } = messagePagination
|
||||
return DbContribution.findAndCount({
|
||||
where: { userId },
|
||||
withDeleted: true,
|
||||
relations: { messages: { user: true } },
|
||||
order: { createdAt: order, id: order, messages: { createdAt: messageOrder } },
|
||||
...buildBaseOptions(paginated),
|
||||
})
|
||||
}
|
||||
@ -34,8 +38,10 @@ export const loadUserContributions = async (
|
||||
export const loadAllContributions = async (
|
||||
paginated: Paginated,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const { order } = paginated
|
||||
return DbContribution.findAndCount({
|
||||
relations: { user: true },
|
||||
relations: { user: { emailContact: true } },
|
||||
order: { createdAt: order, id: order},
|
||||
...buildBaseOptions(paginated),
|
||||
})
|
||||
}
|
||||
|
||||
@ -18,14 +18,14 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu
|
||||
contribution: Contribution,
|
||||
private updateData: ContributionArgs,
|
||||
) {
|
||||
super(contribution, updateData.amount, new Date(updateData.creationDate))
|
||||
super(contribution, updateData.amount, new Date(updateData.contributionDate))
|
||||
logger.debug('use UnconfirmedContributionUserRole')
|
||||
}
|
||||
|
||||
protected update(): void {
|
||||
this.self.amount = this.updateData.amount
|
||||
this.self.memo = this.updateData.memo
|
||||
this.self.contributionDate = new Date(this.updateData.creationDate)
|
||||
this.self.contributionDate = new Date(this.updateData.contributionDate)
|
||||
this.self.contributionStatus = ContributionStatus.PENDING
|
||||
this.self.updatedAt = new Date()
|
||||
// null because updated by user them self
|
||||
|
||||
@ -39,11 +39,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'get-list-contribution-messages',
|
||||
'update-status',
|
||||
'add-contribution-message',
|
||||
])
|
||||
const emit = defineEmits(['get-list-contribution-messages', 'add-contribution-message'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
@ -68,7 +64,6 @@ async function onSubmit() {
|
||||
|
||||
// emit('get-list-contribution-messages', false)
|
||||
formText.value = ''
|
||||
emit('update-status', props.contributionId)
|
||||
emit('add-contribution-message', result.data.createContributionMessage)
|
||||
toastSuccess(t('message.reply'))
|
||||
} catch (error) {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center pointer clearboth">
|
||||
<BButton variant="outline-primary" block @click="$emit('close-all-open-collapse')">
|
||||
<BButton variant="outline-primary" block @click="$emit('close-messages-list')">
|
||||
<IBiArrowUpShort />
|
||||
{{ $t('form.close') }}
|
||||
</BButton>
|
||||
|
||||
@ -38,12 +38,6 @@
|
||||
<parse-message v-bind="message" data-test="message"></parse-message>
|
||||
</BCol>
|
||||
<BCol cols="2">
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :name="storeName.username"-->
|
||||
<!-- :initials="storeName.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="storeName.username"
|
||||
@ -55,12 +49,6 @@
|
||||
<div v-else>
|
||||
<BRow class="mb-3 p-2 is-moderator">
|
||||
<BCol cols="2">
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :name="moderationName.username"-->
|
||||
<!-- :initials="moderationName.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar :name="moderationName.username" :initials="moderationName.initials" />
|
||||
</BCol>
|
||||
<BCol cols="10">
|
||||
@ -70,7 +58,6 @@
|
||||
{{ $t('community.moderator') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small" data-test="date">{{ $d(new Date(message.createdAt), 'short') }}</div>
|
||||
<parse-message v-bind="message" data-test="message"></parse-message>
|
||||
</BCol>
|
||||
|
||||
56
frontend/src/components/Contributions/ContributionCreate.vue
Normal file
56
frontend/src/components/Contributions/ContributionCreate.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<contribution-form
|
||||
v-if="maxForMonths"
|
||||
:model-value="form"
|
||||
:max-gdd-last-month="parseFloat(maxForMonths.openCreations[1].amount)"
|
||||
:max-gdd-this-month="parseFloat(maxForMonths.openCreations[2].amount)"
|
||||
@upsert-contribution="handleCreateContribution"
|
||||
@reset-form="resetForm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
|
||||
import { GDD_PER_HOUR } from '@/constants'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { openCreationsAmounts, createContribution } from '@/graphql/contributions.graphql'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { result: maxForMonths, refetch } = useQuery(
|
||||
openCreationsAmounts,
|
||||
{},
|
||||
{ fetchPolicy: 'no-cache' },
|
||||
)
|
||||
const { mutate: createContributionMutation } = useMutation(createContribution)
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
contributionDate: undefined,
|
||||
memo: '',
|
||||
hours: '',
|
||||
amount: GDD_PER_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateContribution(contribution) {
|
||||
try {
|
||||
await createContributionMutation({ ...contribution })
|
||||
toastSuccess(t('contribution.submitted'))
|
||||
resetForm()
|
||||
refetch()
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = emptyForm()
|
||||
}
|
||||
</script>
|
||||
56
frontend/src/components/Contributions/ContributionEdit.vue
Normal file
56
frontend/src/components/Contributions/ContributionEdit.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<contribution-form
|
||||
:model-value="modelValue"
|
||||
:max-gdd-last-month="maxForMonths[0]"
|
||||
:max-gdd-this-month="maxForMonths[1]"
|
||||
@upsert-contribution="handleUpdateContribution"
|
||||
@reset-form="emit('reset-form')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { openCreations, updateContribution } from '@/graphql/contributions.graphql'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits(['contribution-updated', 'reset-form'])
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const { result: openCreationsResult } = useQuery(openCreations, {}, { fetchPolicy: 'no-cache' })
|
||||
const { mutate: updateContributionMutation } = useMutation(updateContribution)
|
||||
|
||||
const maxForMonths = computed(() => {
|
||||
const originalDate = new Date(props.modelValue.contributionDate)
|
||||
if (openCreationsResult.value && openCreationsResult.value.openCreations.length) {
|
||||
return openCreationsResult.value.openCreations.slice(1).map((creation) => {
|
||||
if (
|
||||
creation.year === originalDate.getFullYear() &&
|
||||
creation.month === originalDate.getMonth()
|
||||
) {
|
||||
return parseFloat(creation.amount) + parseFloat(props.modelValue.amount)
|
||||
}
|
||||
return parseFloat(creation.amount)
|
||||
})
|
||||
}
|
||||
return [0, 0]
|
||||
})
|
||||
|
||||
async function handleUpdateContribution(contribution) {
|
||||
try {
|
||||
await updateContributionMutation({ contributionId: props.modelValue.id, ...contribution })
|
||||
toastSuccess(t('contribution.updated'))
|
||||
emit('contribution-updated')
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,4 +1,10 @@
|
||||
<template>
|
||||
<open-creations-amount
|
||||
:minimal-date="minimalDate"
|
||||
:max-gdd-last-month="maxGddLastMonth"
|
||||
:max-gdd-this-month="maxGddThisMonth"
|
||||
/>
|
||||
<div class="mb-3"></div>
|
||||
<div class="contribution-form">
|
||||
<BForm
|
||||
class="form-style p-3 bg-white app-box-shadow gradido-border-radius"
|
||||
@ -6,13 +12,13 @@
|
||||
>
|
||||
<ValidatedInput
|
||||
id="contribution-date"
|
||||
:model-value="date"
|
||||
name="date"
|
||||
:model-value="form.contributionDate"
|
||||
name="contributionDate"
|
||||
:label="$t('contribution.selectDate')"
|
||||
:no-flip="true"
|
||||
class="mb-4 bg-248"
|
||||
type="date"
|
||||
:rules="validationSchema.fields.date"
|
||||
:rules="validationSchema.fields.contributionDate"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<div v-if="noOpenCreation" class="p-3" data-test="contribution-message">
|
||||
@ -21,7 +27,7 @@
|
||||
<div v-else>
|
||||
<ValidatedInput
|
||||
id="contribution-memo"
|
||||
:model-value="memo"
|
||||
:model-value="form.memo"
|
||||
name="memo"
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
@ -31,7 +37,7 @@
|
||||
/>
|
||||
<ValidatedInput
|
||||
name="hours"
|
||||
:model-value="hours"
|
||||
:model-value="form.hours"
|
||||
:label="$t('form.hours')"
|
||||
placeholder="0.01"
|
||||
step="0.01"
|
||||
@ -41,7 +47,7 @@
|
||||
/>
|
||||
<LabeledInput
|
||||
id="contribution-amount"
|
||||
:model-value="amount"
|
||||
:model-value="form.amount"
|
||||
class="mt-3"
|
||||
name="amount"
|
||||
:label="$t('form.amount')"
|
||||
@ -57,7 +63,7 @@
|
||||
type="reset"
|
||||
variant="secondary"
|
||||
data-test="button-cancel"
|
||||
@click="fullFormReset"
|
||||
@click="emit('reset-form')"
|
||||
>
|
||||
{{ $t('form.cancel') }}
|
||||
</BButton>
|
||||
@ -80,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import { reactive, computed, watch, ref, onMounted, onUnmounted, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ValidatedInput from '@/components/Inputs/ValidatedInput'
|
||||
import LabeledInput from '@/components/Inputs/LabeledInput'
|
||||
@ -88,35 +94,42 @@ import { memo as memoSchema } from '@/validationSchemas'
|
||||
import { object, date as dateSchema, number } from 'yup'
|
||||
import { GDD_PER_HOUR } from '../../constants'
|
||||
|
||||
const amountToHours = (amount) => parseFloat(amount / GDD_PER_HOUR).toFixed(2)
|
||||
const hoursToAmount = (hours) => parseFloat(hours * GDD_PER_HOUR).toFixed(2)
|
||||
const entityDataToForm = (entityData) => ({
|
||||
...entityData,
|
||||
hours: entityData.hours !== undefined ? entityData.hours : amountToHours(entityData.amount),
|
||||
contributionDate: entityData.contributionDate
|
||||
? new Date(entityData.contributionDate).toISOString().slice(0, 10)
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
maxGddLastMonth: { type: Number, required: true },
|
||||
maxGddThisMonth: { type: Number, required: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update-contribution', 'set-contribution', 'update:modelValue'])
|
||||
const emit = defineEmits(['upsert-contribution', 'update:modelValue', 'reset-form'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const form = reactive({ ...props.modelValue })
|
||||
const form = reactive(entityDataToForm(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),
|
||||
)
|
||||
|
||||
// use computed to make sure child input update if props from parent from this component change
|
||||
const amount = computed(() => form.amount)
|
||||
const date = computed(() => form.date)
|
||||
const hours = computed(() => form.hours)
|
||||
const memo = computed(() => form.memo)
|
||||
const now = ref(new Date()) // checked every minute, updated if day, month or year changed
|
||||
|
||||
const isThisMonth = computed(() => {
|
||||
const formDate = new Date(form.date)
|
||||
const now = new Date()
|
||||
return formDate.getMonth() === now.getMonth() && formDate.getFullYear() === now.getFullYear()
|
||||
const formContributionDate = new Date(form.contributionDate)
|
||||
return (
|
||||
formContributionDate.getMonth() === now.value.getMonth() &&
|
||||
formContributionDate.getFullYear() === now.value.getFullYear()
|
||||
)
|
||||
})
|
||||
|
||||
const minimalDate = computed(() => {
|
||||
const minimalDate = new Date(now.value)
|
||||
minimalDate.setMonth(now.value.getMonth() - 1, 1)
|
||||
return minimalDate
|
||||
})
|
||||
|
||||
// reactive validation schema, because some boundaries depend on form input and existing data
|
||||
@ -129,11 +142,10 @@ const validationSchema = computed(() => {
|
||||
return object({
|
||||
// The date field is required and needs to be a valid date
|
||||
// contribution date
|
||||
date: dateSchema()
|
||||
contributionDate: 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
|
||||
.min(minimalDate.value.toISOString().slice(0, 10)) // min date is first day of last month
|
||||
.max(now.value.toISOString().slice(0, 10)), // date cannot be in the future
|
||||
memo: memoSchema,
|
||||
hours: number()
|
||||
.required()
|
||||
@ -150,11 +162,12 @@ const validationSchema = computed(() => {
|
||||
|
||||
const disabled = computed(() => !validationSchema.value.isValidSync(form))
|
||||
|
||||
// decide message if no open creation exists
|
||||
const noOpenCreation = computed(() => {
|
||||
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) {
|
||||
return t('contribution.noOpenCreation.allMonth')
|
||||
}
|
||||
if (form.date) {
|
||||
if (form.contributionDate) {
|
||||
if (isThisMonth.value && props.maxGddThisMonth <= 0) {
|
||||
return t('contribution.noOpenCreation.thisMonth')
|
||||
}
|
||||
@ -165,36 +178,36 @@ const noOpenCreation = computed(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
// make sure, that base date for min and max date is up to date, even if user work at midnight
|
||||
onMounted(() => {
|
||||
const interval = setInterval(() => {
|
||||
const localNow = new Date()
|
||||
if (
|
||||
localNow.getDate() !== now.value.getDate() ||
|
||||
localNow.getMonth() !== now.value.getMonth() ||
|
||||
localNow.getFullYear() !== now.value.getFullYear()
|
||||
) {
|
||||
now.value = localNow
|
||||
}
|
||||
}, 60 * 1000) // check every minute
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
})
|
||||
|
||||
const updateField = (newValue, name) => {
|
||||
if (typeof name === 'string' && name.length) {
|
||||
form[name] = newValue
|
||||
if (name === 'hours') {
|
||||
const amount = form.hours ? (form.hours * GDD_PER_HOUR).toFixed(2) : GDD_PER_HOUR
|
||||
const amount = form.hours ? hoursToAmount(form.hours) : GDD_PER_HOUR
|
||||
form.amount = amount.toString()
|
||||
}
|
||||
}
|
||||
emit('update:modelValue', form)
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const dataToSave = { ...form }
|
||||
let emitOption = 'set-contribution'
|
||||
if (props.modelValue.id) {
|
||||
dataToSave.id = props.modelValue.id
|
||||
emitOption = 'update-contribution'
|
||||
}
|
||||
emit(emitOption, dataToSave)
|
||||
fullFormReset()
|
||||
}
|
||||
|
||||
function fullFormReset() {
|
||||
emit('update:modelValue', {
|
||||
id: undefined,
|
||||
date: null,
|
||||
memo: '',
|
||||
hours: '',
|
||||
amount: undefined,
|
||||
})
|
||||
emit('upsert-contribution', toRaw(form))
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { listAllContributions, listContributions } from '@/graphql/contributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import ContributionList from './ContributionList'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ContributionList from './ContributionList'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@ -15,6 +17,10 @@ vi.mock('@/components/Contributions/ContributionListItem.vue', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useQuery: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ContributionList', () => {
|
||||
let wrapper
|
||||
|
||||
@ -30,10 +36,38 @@ describe('ContributionList', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const contributionsList = {
|
||||
contributionCount: 3,
|
||||
contributionList: [
|
||||
{
|
||||
id: 0,
|
||||
date: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
status: 'IN_PROGRESS',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: '06/22/2022',
|
||||
memo: 'Ich habe 30 Stunden Frau Müller beim Einkaufen und im Haushalt geholfen.',
|
||||
amount: '600',
|
||||
status: 'CONFIRMED',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '05/04/2022',
|
||||
memo: 'Ich habe 50 Stunden den Nachbarkindern bei ihren Hausaufgaben geholfen und Nachhilfeunterricht gegeben.',
|
||||
amount: '1000',
|
||||
status: 'DENIED',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionCount: 3,
|
||||
showPagination: true,
|
||||
pageSize: 25,
|
||||
allContribution: false,
|
||||
items: [
|
||||
{
|
||||
id: 0,
|
||||
@ -45,7 +79,7 @@ describe('ContributionList', () => {
|
||||
{
|
||||
id: 1,
|
||||
date: '06/22/2022',
|
||||
memo: 'Ich habe 30 Stunden Frau Müller beim EInkaufen und im Haushalt geholfen.',
|
||||
memo: 'Ich habe 30 Stunden Frau Müller beim Einkaufen und im Haushalt geholfen.',
|
||||
amount: '600',
|
||||
status: 'CONFIRMED',
|
||||
},
|
||||
@ -67,10 +101,44 @@ describe('ContributionList', () => {
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
const mockListContributionsQuery = vi.fn()
|
||||
const mockListAllContributionsQuery = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useQuery).mockImplementation((query) => {
|
||||
if (query === listContributions) {
|
||||
return { onResult: mockListContributionsQuery, refetch: vi.fn() }
|
||||
}
|
||||
if (query === listAllContributions) {
|
||||
return { onResult: mockListAllContributionsQuery, refetch: vi.fn() }
|
||||
}
|
||||
})
|
||||
|
||||
wrapper = mountWrapper()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('mount as user contributions list', () => {
|
||||
beforeEach(() => {
|
||||
propsData.allContribution = false
|
||||
})
|
||||
it('fetches initial data', () => {
|
||||
expect(mockListContributionsQuery).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mount as all contributions list', () => {
|
||||
beforeEach(() => {
|
||||
propsData.allContribution = true
|
||||
})
|
||||
it('fetches initial data', () => {
|
||||
expect(mockListAllContributionsQuery).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list', () => {
|
||||
expect(wrapper.find('div.contribution-list').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="items.length === 0 && !loading.value">
|
||||
<div v-if="items.length === 0 && !loading">
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
<div v-else class="contribution-list">
|
||||
@ -8,9 +8,10 @@
|
||||
v-bind="item"
|
||||
:contribution-id="item.id"
|
||||
:all-contribution="allContribution"
|
||||
@close-all-open-collapse="$emit('close-all-open-collapse')"
|
||||
:messages-visible="openMessagesListId === item.id"
|
||||
@toggle-messages-visible="toggleMessagesVisible(item.id)"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
@contribution-changed="refetch()"
|
||||
/>
|
||||
</div>
|
||||
<BPagination
|
||||
@ -33,6 +34,8 @@ import ContributionListItem from '@/components/Contributions/ContributionListIte
|
||||
import { listContributions, listAllContributions } from '@/graphql/contributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { PAGE_SIZE } from '@/constants'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
allContribution: {
|
||||
@ -47,45 +50,65 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'close-all-open-collapse',
|
||||
'update-contribution-form',
|
||||
'delete-contribution',
|
||||
])
|
||||
// composables
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
// constants
|
||||
const pageSize = PAGE_SIZE
|
||||
|
||||
// refs
|
||||
const currentPage = ref(1)
|
||||
const openMessagesListId = ref(null)
|
||||
|
||||
// queries
|
||||
const { result, loading, refetch } = useQuery(
|
||||
props.allContribution ? listAllContributions : listContributions,
|
||||
() => ({
|
||||
pagination: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize,
|
||||
order: 'DESC',
|
||||
},
|
||||
messagePagination: !props.allContribution
|
||||
? {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
order: 'ASC',
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
{ fetchPolicy: 'cache-and-network' },
|
||||
)
|
||||
|
||||
// events
|
||||
const emit = defineEmits(['update-contribution-form'])
|
||||
|
||||
// computed
|
||||
const contributionListResult = computed(() => {
|
||||
return props.allContribution
|
||||
? result.value?.listAllContributions
|
||||
: result.value?.listContributions
|
||||
})
|
||||
|
||||
const contributionCount = computed(() => {
|
||||
return contributionListResult.value?.contributionCount || 0
|
||||
})
|
||||
const items = computed(() => {
|
||||
return contributionListResult.value?.contributionList || []
|
||||
})
|
||||
|
||||
const isPaginationVisible = computed(() => {
|
||||
return PAGE_SIZE < contributionCount.value
|
||||
return contributionCount.value > pageSize
|
||||
})
|
||||
const toggleMessagesVisible = (contributionId) => {
|
||||
if (openMessagesListId.value === contributionId) {
|
||||
openMessagesListId.value = 0
|
||||
} else {
|
||||
openMessagesListId.value = contributionId
|
||||
}
|
||||
}
|
||||
|
||||
const { result, loading } = useQuery(
|
||||
props.allContribution ? listAllContributions : listContributions,
|
||||
() => ({
|
||||
currentPage: currentPage.value,
|
||||
pageSize: PAGE_SIZE,
|
||||
}),
|
||||
{ fetchPolicy: 'cache-and-network' },
|
||||
)
|
||||
|
||||
// methods
|
||||
const updateContributionForm = (item) => {
|
||||
emit('update-contribution-form', item)
|
||||
}
|
||||
|
||||
const deleteContribution = (item) => {
|
||||
emit('delete-contribution', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import ContributionListItem from './ContributionListItem'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ContributionListItem from './ContributionListItem'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@ -169,5 +169,14 @@ describe('ContributionListItem', () => {
|
||||
expect(wrapper.emitted('close-all-open-collapse')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
describe('updateStatus', () => {
|
||||
it('updates status of a contribution', async () => {
|
||||
wrapper.vm.items[0] = { id: 1, status: 'IN_PROGRESS' }
|
||||
|
||||
wrapper.vm.updateStatus(1)
|
||||
|
||||
expect(wrapper.vm.items[0].status).toBe('PENDING')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,16 +6,8 @@
|
||||
>
|
||||
<BRow>
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<!-- <avatar-->
|
||||
<!-- v-if="firstName"-->
|
||||
<!-- :name="username.username"-->
|
||||
<!-- :initials="username.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- color="#fff"-->
|
||||
<!-- class="vue3-avatar fw-bold"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
v-if="firstName"
|
||||
v-if="username.username"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
color="#fff"
|
||||
@ -26,8 +18,8 @@
|
||||
</BAvatar>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<div v-if="firstName" class="me-3 fw-bold">
|
||||
{{ firstName }} {{ lastName }}
|
||||
<div v-if="username.username" class="me-3 fw-bold">
|
||||
{{ username.username }}
|
||||
<variant-icon :icon="icon" :variant="variant" />
|
||||
</div>
|
||||
<div class="small">
|
||||
@ -41,7 +33,7 @@
|
||||
<div
|
||||
v-if="localStatus === 'IN_PROGRESS' && !allContribution"
|
||||
class="text-danger pointer hover-font-bold"
|
||||
@click="visible = !visible"
|
||||
@click="emit('toggle-messages-visible')"
|
||||
>
|
||||
{{ $t('contribution.alert.answerQuestion') }}
|
||||
</div>
|
||||
@ -60,8 +52,8 @@
|
||||
<div v-else class="fw-bold">{{ $filters.GDD(amount) }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="1" lg="1" class="text-end align-items-center">
|
||||
<div v-if="messagesCount > 0 && !moderatorId" @click="visible = !visible">
|
||||
<collapse-icon class="text-end" :visible="visible" />
|
||||
<div v-if="messagesCount > 0 && !moderatorId" @click="emit('toggle-messages-visible')">
|
||||
<collapse-icon class="text-end" :visible="messagesVisible" />
|
||||
</div>
|
||||
</BCol>
|
||||
</BRow>
|
||||
@ -77,7 +69,7 @@
|
||||
!['CONFIRMED', 'DELETED'].includes(localStatus) && !allContribution && !moderatorId
|
||||
"
|
||||
class="test-delete-contribution pointer me-3"
|
||||
@click="deleteContribution({ id })"
|
||||
@click="processDeleteContribution({ id })"
|
||||
>
|
||||
<IBiTrash />
|
||||
|
||||
@ -92,10 +84,10 @@
|
||||
class="test-edit-contribution pointer me-3"
|
||||
@click="
|
||||
$emit('update-contribution-form', {
|
||||
id: id,
|
||||
contributionDate: contributionDate,
|
||||
memo: memo,
|
||||
amount: amount,
|
||||
id,
|
||||
contributionDate,
|
||||
memo,
|
||||
amount,
|
||||
})
|
||||
"
|
||||
>
|
||||
@ -104,20 +96,23 @@
|
||||
</div>
|
||||
</BCol>
|
||||
<BCol cols="6" class="text-center">
|
||||
<div v-if="messagesCount > 0 && !moderatorId" class="pointer" @click="visible = !visible">
|
||||
<div
|
||||
v-if="messagesCount > 0 && !moderatorId"
|
||||
class="pointer"
|
||||
@click="emit('toggle-messages-visible')"
|
||||
>
|
||||
<IBiChatDots />
|
||||
<div>{{ $t('moderatorChat') }}</div>
|
||||
</div>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BCollapse :model-value="visible">
|
||||
<BCollapse :model-value="messagesVisible">
|
||||
<contribution-messages-list
|
||||
:messages="messagesGet"
|
||||
v-if="messagesCount > 0"
|
||||
:messages="localMessages"
|
||||
:status="localStatus"
|
||||
:contribution-id="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-status="updateStatus"
|
||||
@close-all-open-collapse="visible = false"
|
||||
@close-messages-list="emit('toggle-messages-visible')"
|
||||
@add-contribution-message="addContributionMessage"
|
||||
/>
|
||||
</BCollapse>
|
||||
@ -130,12 +125,12 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import ContributionMessagesList from '@/components/ContributionMessages/ContributionMessagesList'
|
||||
import { listContributionMessages } from '@/graphql/queries'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLazyQuery } from '@vue/apollo-composable'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
import { GDD_PER_HOUR } from '../../constants'
|
||||
import { deleteContribution } from '@/graphql/contributions.graphql'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@ -150,13 +145,10 @@ const props = defineProps({
|
||||
messages: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
@ -189,7 +181,7 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
status: {
|
||||
contributionStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
@ -212,103 +204,89 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
messagesVisible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const messagesGet = ref([])
|
||||
const visible = ref(false)
|
||||
const localStatus = ref(props.status)
|
||||
const { mutate: deleteContributionMutation } = useMutation(deleteContribution)
|
||||
|
||||
const localMessages = ref([])
|
||||
const localStatus = ref(props.contributionStatus)
|
||||
|
||||
// if parent reload messages, update local messages copy
|
||||
watch(
|
||||
() => props.messages,
|
||||
() => {
|
||||
messagesGet.value = props.messages
|
||||
if (props.messages?.length > 0) {
|
||||
localMessages.value = [...props.messages]
|
||||
}
|
||||
},
|
||||
// parent is loading messages already
|
||||
{ immediate: false },
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const statusMapping = {
|
||||
CONFIRMED: { variant: 'success', icon: 'check' },
|
||||
DELETED: { variant: 'danger', icon: 'trash' },
|
||||
DENIED: { variant: 'warning', icon: 'x-circle' },
|
||||
IN_PROGRESS: { variant: '205', icon: 'question' },
|
||||
default: { variant: 'primary', icon: 'bell-fill' },
|
||||
}
|
||||
|
||||
const variant = computed(() => {
|
||||
if (props.deletedAt) return 'danger'
|
||||
if (props.deniedAt) return 'warning'
|
||||
if (props.confirmedAt) return 'success'
|
||||
if (props.status === 'IN_PROGRESS') return '205'
|
||||
return 'primary'
|
||||
return (statusMapping[localStatus.value] || statusMapping.default).variant
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.deletedAt) return 'trash'
|
||||
if (props.deniedAt) return 'x-circle'
|
||||
if (props.confirmedAt) return 'check'
|
||||
if (props.status === 'IN_PROGRESS') return 'question'
|
||||
return 'bell-fill'
|
||||
return (statusMapping[localStatus.value] || statusMapping.default).icon
|
||||
})
|
||||
|
||||
const collapseId = computed(() => 'collapse' + String(props.id))
|
||||
|
||||
const username = computed(() => ({
|
||||
username: `${props.firstName} ${props.lastName}`,
|
||||
initials: `${props.firstName[0]}${props.lastName[0]}`,
|
||||
}))
|
||||
const username = computed(() => {
|
||||
if (!props.user) return {}
|
||||
return {
|
||||
username: props.user.alias
|
||||
? props.user.alias
|
||||
: `${props.user.firstName} ${props.user.lastName}`,
|
||||
initials: `${props.user.firstName[0]}${props.user.lastName[0]}`,
|
||||
}
|
||||
})
|
||||
|
||||
const hours = computed(() => parseFloat((props.amount / GDD_PER_HOUR).toFixed(2)))
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
() => {
|
||||
if (visible.value) getListContributionMessages()
|
||||
},
|
||||
)
|
||||
|
||||
function deleteContribution(item) {
|
||||
async function processDeleteContribution(item) {
|
||||
if (props.allContribution) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('tried to delete contribution from all contributions')
|
||||
return
|
||||
}
|
||||
if (window.confirm(t('contribution.delete'))) {
|
||||
emit('delete-contribution', item)
|
||||
}
|
||||
}
|
||||
|
||||
const { onResult, onError, load, refetch } = useLazyQuery(listContributionMessages, {
|
||||
contributionId: props.contributionId,
|
||||
})
|
||||
|
||||
function getListContributionMessages(closeCollapse = true) {
|
||||
if (closeCollapse) {
|
||||
emit('close-all-open-collapse')
|
||||
}
|
||||
const variables = {
|
||||
contributionId: props.contributionId,
|
||||
}
|
||||
// load works only once and return false on second call
|
||||
if (!load(listContributionMessages, variables)) {
|
||||
// update list data every time getListContributionMessages is called
|
||||
// because it could be added new messages
|
||||
refetch(variables)
|
||||
try {
|
||||
await deleteContributionMutation(item)
|
||||
toastSuccess(t('contribution.deleted'))
|
||||
localStatus.value = 'DELETED'
|
||||
emit('contribution-changed')
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addContributionMessage(message) {
|
||||
messagesGet.value.push(message)
|
||||
}
|
||||
|
||||
onResult((resultValue) => {
|
||||
if (resultValue.data) {
|
||||
messagesGet.value.length = 0
|
||||
resultValue.data.listContributionMessages.messages.forEach((message) => {
|
||||
messagesGet.value.push(message)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onError((err) => {
|
||||
toastError(err.message)
|
||||
})
|
||||
|
||||
const updateStatus = (id) => {
|
||||
localMessages.value.push(message)
|
||||
localStatus.value = 'PENDING'
|
||||
emit('contribution-changed')
|
||||
}
|
||||
|
||||
const emit = defineEmits(['delete-contribution', 'close-all-open-collapse'])
|
||||
const emit = defineEmits([
|
||||
'toggle-messages-visible',
|
||||
'update-contribution-form',
|
||||
'contribution-changed',
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -60,9 +60,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
const { value, meta, errorMessage } = useField(props.name, props.rules)
|
||||
|
||||
const amountFocused = ref(false)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div :class="wrapperClassName">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormTextarea
|
||||
v-if="textarea"
|
||||
v-if="textarea === 'true'"
|
||||
v-bind="{ ...$attrs, id: labelFor, name }"
|
||||
v-model="model"
|
||||
trim
|
||||
@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineOptions, defineModel } from 'vue'
|
||||
import { computed, defineOptions, defineModel, watch } from 'vue'
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
@ -32,9 +32,9 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Boolean,
|
||||
type: String,
|
||||
required: false,
|
||||
default: false,
|
||||
default: 'false',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
#import './user.graphql'
|
||||
|
||||
fragment contributionFields on Contribution {
|
||||
fragment unconfirmedContributionFields on Contribution {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
contributionStatus
|
||||
messagesCount
|
||||
}
|
||||
|
||||
fragment contributionFields on Contribution {
|
||||
...unconfirmedContributionFields
|
||||
createdAt
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
updatedBy
|
||||
@ -26,34 +31,77 @@ fragment contributionMessageFields on ContributionMessage {
|
||||
userId
|
||||
}
|
||||
|
||||
query listContributions ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
query listContributions ($pagination: Paginated!, $messagePagination: Paginated!) {
|
||||
listContributions(pagination: $pagination, messagePagination: $messagePagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
...contributionFields
|
||||
deletedAt
|
||||
moderatorId
|
||||
messages(pagination: { currentPage: 1, pageSize: 10, order: DESC }) {
|
||||
messages(pagination: $messagePagination) {
|
||||
...contributionMessageFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query listAllContributions ($pagination: Paginated!) {
|
||||
listAllContributions(pagination: $pagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
user {
|
||||
...userFields
|
||||
}
|
||||
...contributionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query countContributionsInProgress {
|
||||
countContributionsInProgress
|
||||
}
|
||||
|
||||
|
||||
query listAllContributions ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
firstName
|
||||
lastName
|
||||
...contributionFields
|
||||
}
|
||||
query openCreations {
|
||||
openCreations {
|
||||
year
|
||||
month
|
||||
amount
|
||||
}
|
||||
}
|
||||
|
||||
query openCreationsAmounts {
|
||||
openCreations {
|
||||
amount
|
||||
}
|
||||
}
|
||||
|
||||
# return unconfirmedContributionFields
|
||||
mutation createContribution ($amount: Decimal!, $memo: String!, $contributionDate: String!) {
|
||||
createContribution(amount: $amount, memo: $memo, contributionDate: $contributionDate) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
# return unconfirmedContributionFields
|
||||
mutation updateContribution (
|
||||
$contributionId: Int!,
|
||||
$amount: Decimal!,
|
||||
$memo: String!,
|
||||
$contributionDate: String!
|
||||
) {
|
||||
updateContribution(
|
||||
contributionId: $contributionId,
|
||||
amount: $amount,
|
||||
memo: $memo,
|
||||
contributionDate: $contributionDate
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
mutation deleteContribution($id: Int!) {
|
||||
deleteContribution(id: $id)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -134,36 +134,6 @@ export const redeemTransactionLink = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const createContribution = gql`
|
||||
mutation ($creationDate: String!, $memo: String!, $amount: Decimal!) {
|
||||
createContribution(creationDate: $creationDate, memo: $memo, amount: $amount) {
|
||||
amount
|
||||
memo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateContribution = gql`
|
||||
mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
updateContribution(
|
||||
contributionId: $contributionId
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
) {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
deleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const createContributionMessage = gql`
|
||||
mutation ($contributionId: Int!, $message: String!) {
|
||||
createContributionMessage(contributionId: $contributionId, message: $message) {
|
||||
|
||||
@ -187,56 +187,6 @@ export const listContributionLinks = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const listContributions = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
deletedAt
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
updatedBy
|
||||
updatedAt
|
||||
moderatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const listAllContributions = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
updatedBy
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const communityStatistics = gql`
|
||||
query {
|
||||
communityStatistics {
|
||||
@ -281,16 +231,6 @@ export const listContributionMessages = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const openCreations = gql`
|
||||
query {
|
||||
openCreations {
|
||||
year
|
||||
month
|
||||
amount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const user = gql`
|
||||
query ($identifier: String!, $communityIdentifier: String!) {
|
||||
user(identifier: $identifier, communityIdentifier: $communityIdentifier) {
|
||||
|
||||
6
frontend/src/graphql/user.graphql
Normal file
6
frontend/src/graphql/user.graphql
Normal file
@ -0,0 +1,6 @@
|
||||
fragment userFields on User {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
alias
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import Community from './Community'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { countContributionsInProgress } from '@/graphql/contributions.graphql'
|
||||
import { createContribution, deleteContribution, updateContribution } from '@/graphql/mutations'
|
||||
import { listAllContributions, listContributions, openCreations } from '@/graphql/queries'
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { BTab, BTabs } from 'bootstrap-vue-next'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Community from './Community'
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock('vue-router', () => ({
|
||||
@ -80,9 +81,8 @@ describe('Community', () => {
|
||||
let mockRouter
|
||||
let mockToast
|
||||
|
||||
const mockCountContributionsInProgress = vi.fn()
|
||||
const mockOpenCreationsQuery = vi.fn()
|
||||
const mockListContributionsQuery = vi.fn()
|
||||
const mockListAllContributionsQuery = vi.fn()
|
||||
const mockCreateContributionMutation = vi.fn()
|
||||
const mockUpdateContributionMutation = vi.fn()
|
||||
const mockDeleteContributionMutation = vi.fn()
|
||||
@ -99,17 +99,34 @@ describe('Community', () => {
|
||||
vi.mocked(useAppToast).mockReturnValue(mockToast)
|
||||
|
||||
vi.mocked(useQuery).mockImplementation((query) => {
|
||||
if (query === openCreations) return { onResult: mockOpenCreationsQuery, refetch: vi.fn() }
|
||||
if (query === listContributions)
|
||||
return { onResult: mockListContributionsQuery, refetch: vi.fn() }
|
||||
if (query === listAllContributions)
|
||||
return { onResult: mockListAllContributionsQuery, refetch: vi.fn() }
|
||||
if (query === openCreations) {
|
||||
return {
|
||||
onResult: mockOpenCreationsQuery,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
if (query === countContributionsInProgress) {
|
||||
return { onResult: mockCountContributionsInProgress }
|
||||
}
|
||||
})
|
||||
|
||||
vi.mocked(useMutation).mockImplementation((mutation) => {
|
||||
if (mutation === createContribution) return { mutate: mockCreateContributionMutation }
|
||||
if (mutation === updateContribution) return { mutate: mockUpdateContributionMutation }
|
||||
if (mutation === deleteContribution) return { mutate: mockDeleteContributionMutation }
|
||||
if (mutation === createContribution) {
|
||||
return {
|
||||
mutate: mockCreateContributionMutation,
|
||||
}
|
||||
}
|
||||
if (mutation === updateContribution) {
|
||||
return {
|
||||
mutate: mockUpdateContributionMutation,
|
||||
}
|
||||
}
|
||||
if (mutation === deleteContribution) {
|
||||
return {
|
||||
mutate: mockDeleteContributionMutation,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { defineRule } = require('vee-validate')
|
||||
@ -138,14 +155,11 @@ describe('Community', () => {
|
||||
describe('mount', () => {
|
||||
it('initializes with correct data', () => {
|
||||
expect(wrapper.vm.tabIndex).toBe(0)
|
||||
expect(wrapper.vm.items).toEqual([])
|
||||
expect(wrapper.vm.itemsAll).toEqual([])
|
||||
})
|
||||
|
||||
it('fetches initial data', () => {
|
||||
expect(mockOpenCreationsQuery).toHaveBeenCalled()
|
||||
expect(mockListContributionsQuery).toHaveBeenCalled()
|
||||
expect(mockListAllContributionsQuery).toHaveBeenCalled()
|
||||
expect(mockCountContributionsInProgress).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -266,14 +280,4 @@ describe('Community', () => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith({ params: { tab: 'contribute' } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('updates status of a contribution', async () => {
|
||||
wrapper.vm.items[0] = { id: 1, status: 'IN_PROGRESS' }
|
||||
|
||||
wrapper.vm.updateStatus(1)
|
||||
|
||||
expect(wrapper.vm.items[0].status).toBe('PENDING')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,26 +3,18 @@
|
||||
<div>
|
||||
<BTabs :model-value="tabIndex" no-nav-style borderless align="center">
|
||||
<BTab no-body lazy>
|
||||
<open-creations-amount
|
||||
:minimal-date="minimalDate"
|
||||
:max-gdd-last-month="maxForMonths[0]"
|
||||
:max-gdd-this-month="maxForMonths[1]"
|
||||
/>
|
||||
<div class="mb-3"></div>
|
||||
<contribution-form
|
||||
v-model="form"
|
||||
:minimal-date="minimalDate"
|
||||
:max-gdd-last-month="maxForMonths[0]"
|
||||
:max-gdd-this-month="maxForMonths[1]"
|
||||
@set-contribution="handleSaveContribution"
|
||||
@update-contribution="handleUpdateContribution"
|
||||
<contribution-edit
|
||||
v-if="itemToEdit"
|
||||
v-model="itemToEdit"
|
||||
@contribution-updated="handleContributionUpdated"
|
||||
@reset-form="itemToEdit = null"
|
||||
/>
|
||||
<contribution-create v-else />
|
||||
</BTab>
|
||||
<BTab no-body lazy>
|
||||
<contribution-list
|
||||
:empty-text="$t('contribution.noContributions.myContributions')"
|
||||
@update-contribution-form="handleUpdateContributionForm"
|
||||
@delete-contribution="handleDeleteContribution"
|
||||
/>
|
||||
</BTab>
|
||||
<BTab no-body lazy>
|
||||
@ -39,12 +31,11 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import OpenCreationsAmount from '@/components/Contributions/OpenCreationsAmount'
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm'
|
||||
import ContributionEdit from '@/components/Contributions/ContributionEdit'
|
||||
import ContributionCreate from '@/components/Contributions/ContributionCreate'
|
||||
import ContributionList from '@/components/Contributions/ContributionList'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { openCreations } from '@/graphql/queries'
|
||||
import { countContributionsInProgress } from '@/graphql/contributions.graphql'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@ -52,8 +43,6 @@ import { GDD_PER_HOUR } from '../constants'
|
||||
|
||||
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
|
||||
|
||||
const emit = defineEmits(['update-transactions'])
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@ -61,59 +50,8 @@ const { toastError, toastSuccess, toastInfo } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const items = ref([])
|
||||
const itemsAll = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(25)
|
||||
const currentPageAll = ref(1)
|
||||
const pageSizeAll = ref(25)
|
||||
const contributionCount = ref(0)
|
||||
const contributionCountAll = ref(0)
|
||||
const form = ref({
|
||||
id: null,
|
||||
date: undefined,
|
||||
memo: '',
|
||||
hours: '',
|
||||
amount: GDD_PER_HOUR,
|
||||
})
|
||||
const originalContributionDate = ref('')
|
||||
const updateAmount = ref('')
|
||||
const maximalDate = ref(new Date())
|
||||
const openCreationsData = ref([])
|
||||
|
||||
const minimalDate = computed(() => {
|
||||
const date = new Date(maximalDate.value)
|
||||
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
||||
})
|
||||
|
||||
const amountToAdd = computed(() => (form.value.id ? parseFloat(updateAmount.value) : 0.0))
|
||||
|
||||
const maxForMonths = computed(() => {
|
||||
const originalDate = new Date(originalContributionDate.value)
|
||||
if (openCreationsData.value && openCreationsData.value.length) {
|
||||
return openCreationsData.value.slice(1).map((creation) => {
|
||||
if (
|
||||
creation.year === originalDate.getFullYear() &&
|
||||
creation.month === originalDate.getMonth()
|
||||
) {
|
||||
return parseFloat(creation.amount) + amountToAdd.value
|
||||
}
|
||||
return parseFloat(creation.amount)
|
||||
})
|
||||
}
|
||||
return [0, 0]
|
||||
})
|
||||
const { onResult: onOpenCreationsResult, refetch: refetchOpenCreations } = useQuery(
|
||||
openCreations,
|
||||
() => ({}),
|
||||
{
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
)
|
||||
|
||||
const { mutate: createContributionMutation } = useMutation(createContribution)
|
||||
const { mutate: updateContributionMutation } = useMutation(updateContribution)
|
||||
const { mutate: deleteContributionMutation } = useMutation(deleteContribution)
|
||||
const itemToEdit = ref(null)
|
||||
|
||||
const { onResult: handleInProgressContributionFound } = useQuery(
|
||||
countContributionsInProgress,
|
||||
@ -122,18 +60,10 @@ const { onResult: handleInProgressContributionFound } = useQuery(
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
)
|
||||
|
||||
onOpenCreationsResult(({ data }) => {
|
||||
if (data) {
|
||||
openCreationsData.value = data.openCreations
|
||||
}
|
||||
})
|
||||
|
||||
// jump to my contributions if in progress contribution found
|
||||
handleInProgressContributionFound(({ data }) => {
|
||||
if (data) {
|
||||
const count = data.countContributionsInProgress
|
||||
if (count > 0) {
|
||||
if (data.countContributionsInProgress > 0) {
|
||||
tabIndex.value = 1
|
||||
if (route.params.tab !== 'contributions') {
|
||||
router.push({ params: { tab: 'contributions' } })
|
||||
@ -147,63 +77,15 @@ const updateTabIndex = () => {
|
||||
const index = COMMUNITY_TABS.indexOf(route.params.tab)
|
||||
tabIndex.value = index > -1 ? index : 0
|
||||
}
|
||||
|
||||
const refetchData = () => {
|
||||
refetchOpenCreations()
|
||||
handleInProgressContributionFound()
|
||||
// after a edit contribution was saved, jump to contributions tab
|
||||
function handleContributionUpdated() {
|
||||
itemToEdit.value = null
|
||||
tabIndex.value = 1
|
||||
router.push({ params: { tab: 'contributions' } })
|
||||
}
|
||||
|
||||
const handleSaveContribution = async (data) => {
|
||||
try {
|
||||
await createContributionMutation({
|
||||
creationDate: data.date,
|
||||
memo: data.memo,
|
||||
amount: data.amount,
|
||||
})
|
||||
toastSuccess(t('contribution.submitted'))
|
||||
refetchData()
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateContribution = async (data) => {
|
||||
try {
|
||||
await updateContributionMutation({
|
||||
contributionId: data.id,
|
||||
creationDate: data.date,
|
||||
memo: data.memo,
|
||||
amount: data.amount,
|
||||
})
|
||||
toastSuccess(t('contribution.updated'))
|
||||
refetchData()
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteContribution = async (data) => {
|
||||
try {
|
||||
await deleteContributionMutation({
|
||||
id: data.id,
|
||||
})
|
||||
toastSuccess(t('contribution.deleted'))
|
||||
refetchData()
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// if user clicks on edit contribution in contributions tab, jump to contribute tab and populate form with contribution data
|
||||
const handleUpdateContributionForm = (item) => {
|
||||
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
|
||||
itemToEdit.value = item
|
||||
tabIndex.value = 0
|
||||
router.push({ params: { tab: 'contribute' } })
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user