mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into grass_instead_of_sass
This commit is contained in:
commit
d41d4a5875
58
.github/workflows/publish.yml
vendored
58
.github/workflows/publish.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
##########################################################################
|
||||
- name: Frontend | Build `production` image
|
||||
run: |
|
||||
docker build --target production -t "gradido/frontend:latest" -t "gradido/frontend:production" -t "gradido/frontend:${VERSION}" -t "gradido/frontend:${BUILD_VERSION}" frontend/
|
||||
docker build -f ./frontend/Dockerfile --target production -t "gradido/frontend:latest" -t "gradido/frontend:production" -t "gradido/frontend:${VERSION}" -t "gradido/frontend:${BUILD_VERSION}" .
|
||||
docker save "gradido/frontend" > /tmp/frontend.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@ -43,6 +43,43 @@ jobs:
|
||||
name: docker-frontend-production
|
||||
path: /tmp/frontend.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD PRODUCTION ADMIN #########################################
|
||||
##############################################################################
|
||||
build_production_admin:
|
||||
name: Docker Build Production - Admin
|
||||
runs-on: ubuntu-latest
|
||||
#needs: [nothing]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
##########################################################################
|
||||
# SET ENVS ###############################################################
|
||||
##########################################################################
|
||||
- name: ENV - VERSION
|
||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_DATE
|
||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_VERSION
|
||||
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_COMMIT
|
||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||
##########################################################################
|
||||
# ADMIN ##################################################################
|
||||
##########################################################################
|
||||
- name: Admin | Build `production` image
|
||||
run: |
|
||||
docker build -f ./admin/Dockerfile --target production -t "gradido/admin:latest" -t "gradido/admin:production" -t "gradido/admin:${VERSION}" -t "gradido/admin:${BUILD_VERSION}" .
|
||||
docker save "gradido/admin" > /tmp/admin.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-admin-production
|
||||
path: /tmp/admin.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD PRODUCTION BACKEND #######################################
|
||||
##############################################################################
|
||||
@ -170,14 +207,14 @@ jobs:
|
||||
##########################################################################
|
||||
# DATABASE UP ############################################################
|
||||
##########################################################################
|
||||
- name: Database | Build `production_up` image
|
||||
- name: Database | Build `up` image
|
||||
run: |
|
||||
docker build --target production_up -t "gradido/database:production_up" database/
|
||||
docker save "gradido/database:production_up" > /tmp/database_up.tar
|
||||
docker build --target up -t "gradido/database:up" database/
|
||||
docker save "gradido/database:up" > /tmp/database_up.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-database-production_up
|
||||
name: docker-database-up
|
||||
path: /tmp/database_up.tar
|
||||
|
||||
##############################################################################
|
||||
@ -243,6 +280,13 @@ jobs:
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/frontend.tar
|
||||
- name: Download Docker Image (Admin)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-admin-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/admin.tar
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@ -267,7 +311,7 @@ jobs:
|
||||
- name: Download Docker Image (Database)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-database-production_up
|
||||
name: docker-database-up
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/database_up.tar
|
||||
@ -287,6 +331,8 @@ jobs:
|
||||
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
- name: Push frontend
|
||||
run: docker push --all-tags gradido/frontend
|
||||
- name: Push admin
|
||||
run: docker push --all-tags gradido/admin
|
||||
- name: Push backend
|
||||
run: docker push --all-tags gradido/backend
|
||||
- name: Push dht-node
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
*.log
|
||||
*.bak
|
||||
.turbo
|
||||
vite.config.mjs.timestamp-*
|
||||
/node_modules/*
|
||||
messages.pot
|
||||
nbproject
|
||||
|
||||
@ -14,9 +14,9 @@ ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||
ARG BUILD_VERSION
|
||||
ENV BUILD_VERSION=${BUILD_VERSION:-'broken'}
|
||||
ARG BUILD_COMMIT
|
||||
ENV BUILD_COMMIT=${BUILD_COMMIT:-'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'}
|
||||
ENV BUILD_COMMIT=${BUILD_COMMIT:-'decafcabdecafcabdecafcabdecafcabdecafcab'}
|
||||
ARG BUILD_COMMIT_SHORT
|
||||
ENV BUILD_COMMIT_SHORT=${BUILD_COMMIT_SHORT:-'deadbeef'}
|
||||
ENV BUILD_COMMIT_SHORT=${BUILD_COMMIT_SHORT:-'decafcab'}
|
||||
## SET NODE_ENV
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
@ -11,12 +11,15 @@
|
||||
"start": "vite preview",
|
||||
"postbuild": "uname | grep -q Linux && find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} + || echo 'Skip precompress on non-Linux'",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||
"lint:fix": "eslint --max-warnings=0 --ext .js,.vue,.json . --fix",
|
||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||
"stylelint:fix": "stylelint --max-warnings=0 '**/*.{scss,vue}' --fix",
|
||||
"test": "cross-env TZ=UTC vitest run",
|
||||
"test:coverage": "cross-env TZ=UTC vitest run --coverage",
|
||||
"test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs",
|
||||
"test:watch": "cross-env TZ=UTC vitest",
|
||||
"locales": "scripts/sort.sh"
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/json": "^2.2.228",
|
||||
|
||||
@ -108,7 +108,7 @@ describe('ContributionMessagesList', () => {
|
||||
id: 42,
|
||||
memo: 'test memo',
|
||||
userId: 108,
|
||||
status: 'PENDING',
|
||||
contributionStatus: 'PENDING',
|
||||
user: defaultUser,
|
||||
},
|
||||
hideResubmission: true,
|
||||
@ -153,7 +153,7 @@ describe('ContributionMessagesList', () => {
|
||||
it('does not render the ContributionMessagesFormular when status is not PENDING or IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({
|
||||
contribution: {
|
||||
status: 'COMPLETED',
|
||||
contributionStatus: 'COMPLETED',
|
||||
user: defaultUser,
|
||||
},
|
||||
})
|
||||
|
||||
@ -41,7 +41,12 @@
|
||||
/>
|
||||
</div>
|
||||
</BContainer>
|
||||
<div v-if="contribution.status === 'PENDING' || contribution.status === 'IN_PROGRESS'">
|
||||
<div
|
||||
v-if="
|
||||
contribution.contributionStatus === 'PENDING' ||
|
||||
contribution.contributionStatus === 'IN_PROGRESS'
|
||||
"
|
||||
>
|
||||
<contribution-messages-formular
|
||||
:contribution-id="contribution.id"
|
||||
:contribution-memo="contribution.memo"
|
||||
|
||||
@ -92,7 +92,7 @@ describe('CreationTransactionList', () => {
|
||||
expect(fields[0].key).toBe('createdAt')
|
||||
expect(fields[1].key).toBe('contributionDate')
|
||||
expect(fields[2].key).toBe('confirmedAt')
|
||||
expect(fields[3].key).toBe('status')
|
||||
expect(fields[3].key).toBe('contributionStatus')
|
||||
expect(fields[4].key).toBe('amount')
|
||||
expect(fields[5].key).toBe('memo')
|
||||
})
|
||||
|
||||
@ -80,7 +80,7 @@ const fields = [
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
key: 'contributionStatus',
|
||||
label: t('transactionlist.status'),
|
||||
},
|
||||
{
|
||||
|
||||
@ -19,9 +19,13 @@ describe('Overlay', () => {
|
||||
amount: '100',
|
||||
contributionDate: '2023-01-15T00:00:00.000Z',
|
||||
memo: 'Test memo',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
user: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
emailContact: {
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -23,11 +23,11 @@
|
||||
</BRow>
|
||||
<BRow class="mt-3">
|
||||
<BCol class="col-3">{{ $t('name') }}</BCol>
|
||||
<BCol>{{ item.firstName }} {{ item.lastName }}</BCol>
|
||||
<BCol>{{ item.user.firstName }} {{ item.user.lastName }}</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="col-3">{{ $t('e_mail') }}</BCol>
|
||||
<BCol>{{ item.email }}</BCol>
|
||||
<BCol>{{ item.user.emailContact.email }}</BCol>
|
||||
</BRow>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
{{ $t('hide_details') }} {{ row.item.firstName }} {{ row.item.lastName }}
|
||||
{{ $t('hide_details') }} {{ row.item.user.firstName }} {{ row.item.user.lastName }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
@ -14,12 +14,12 @@ describe('OpenCreationsTable', () => {
|
||||
let store
|
||||
|
||||
const mockItems = [
|
||||
{ id: 1, status: 'PENDING', userId: 2, moderatorId: null, messagesCount: 0 },
|
||||
{ id: 2, status: 'CONFIRMED', userId: 3, moderatorId: 1, messagesCount: 2 },
|
||||
{ id: 1, contributionStatus: 'PENDING', userId: 2, moderatorId: null, messagesCount: 0 },
|
||||
{ id: 2, contributionStatus: 'CONFIRMED', userId: 3, moderatorId: 1, messagesCount: 2 },
|
||||
]
|
||||
|
||||
const mockFields = [
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'contributionStatus', label: 'Status' },
|
||||
{ key: 'bookmark', label: 'Bookmark' },
|
||||
{ key: 'memo', label: 'Memo' },
|
||||
{ key: 'editCreation', label: 'Edit' },
|
||||
@ -72,7 +72,7 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it('applies correct row class based on status', () => {
|
||||
const rowClass = wrapper.vm.rowClass({ status: 'CONFIRMED' }, 'row')
|
||||
const rowClass = wrapper.vm.rowClass({ contributionStatus: 'CONFIRMED' }, 'row')
|
||||
expect(rowClass).toBe('table-success')
|
||||
})
|
||||
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
stacked="md"
|
||||
:tbody-tr-class="rowClass"
|
||||
>
|
||||
<template #cell(status)="row">
|
||||
<IBiQuestionSquare v-if="row.item.status === 'IN_PROGRESS'" />
|
||||
<IBiBellFill v-else-if="row.item.status === 'PENDING'" />
|
||||
<IBiCheck v-else-if="row.item.status === 'CONFIRMED'" />
|
||||
<IBiXCircle v-else-if="row.item.status === 'DENIED'" />
|
||||
<IBiTrash v-else-if="row.item.status === 'DELETED'" />
|
||||
<template #cell(contributionStatus)="row">
|
||||
<IBiQuestionSquare v-if="row.item.contributionStatus === 'IN_PROGRESS'" />
|
||||
<IBiBellFill v-else-if="row.item.contributionStatus === 'PENDING'" />
|
||||
<IBiCheck v-else-if="row.item.contributionStatus === 'CONFIRMED'" />
|
||||
<IBiXCircle v-else-if="row.item.contributionStatus === 'DENIED'" />
|
||||
<IBiTrash v-else-if="row.item.contributionStatus === 'DELETED'" />
|
||||
</template>
|
||||
<template #cell(bookmark)="row">
|
||||
<div v-if="!myself(row.item)">
|
||||
@ -51,11 +51,11 @@
|
||||
<BButton v-else @click="rowToggleDetails(row, 0)">
|
||||
<IBiChatDots />
|
||||
<IBiExclamationCircleFill
|
||||
v-if="row.item.status === 'PENDING' && row.item.messagesCount > 0"
|
||||
v-if="row.item.contributionStatus === 'PENDING' && row.item.messagesCount > 0"
|
||||
style="color: #ffc107"
|
||||
/>
|
||||
<IBiQuestionDiamond
|
||||
v-if="row.item.status === 'IN_PROGRESS' && row.item.messagesCount > 0"
|
||||
v-if="row.item.contributionStatus === 'IN_PROGRESS' && row.item.messagesCount > 0"
|
||||
variant="warning"
|
||||
style="color: #ffc107"
|
||||
class="ps-1"
|
||||
@ -197,11 +197,11 @@ export default {
|
||||
},
|
||||
rowClass(item, type) {
|
||||
if (!item || type !== 'row') return
|
||||
if (item.status === 'CONFIRMED') return 'table-success'
|
||||
if (item.status === 'DENIED') return 'table-warning'
|
||||
if (item.status === 'DELETED') return 'table-danger'
|
||||
if (item.status === 'IN_PROGRESS') return 'table-primary'
|
||||
if (item.status === 'PENDING') return 'table-primary'
|
||||
if (item.contributionStatus === 'CONFIRMED') return 'table-success'
|
||||
if (item.contributionStatus === 'DENIED') return 'table-warning'
|
||||
if (item.contributionStatus === 'DELETED') return 'table-danger'
|
||||
if (item.contributionStatus === 'IN_PROGRESS') return 'table-primary'
|
||||
if (item.contributionStatus === 'PENDING') return 'table-primary'
|
||||
},
|
||||
updateStatus(id) {
|
||||
this.$emit('update-status', id)
|
||||
|
||||
@ -116,7 +116,7 @@ describe('TransactionLinkList', () => {
|
||||
})
|
||||
|
||||
it('formats status correctly for different scenarios', () => {
|
||||
const statusField = wrapper.vm.fields.find((f) => f.key === 'status')
|
||||
const statusField = wrapper.vm.fields.find((f) => f.key === 'contributionStatus')
|
||||
|
||||
// Open transaction
|
||||
expect(statusField.formatter(null, null, mockLinks[0])).toBe('open')
|
||||
|
||||
@ -54,7 +54,7 @@ const fields = computed(() => [
|
||||
formatter: (value) => d(new Date(value)),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
key: 'contributionStatus',
|
||||
label: 'status',
|
||||
formatter: (value, key, item) => {
|
||||
if (item.deletedAt) return `${t('deleted')}: ${d(new Date(item.deletedAt))}`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -129,7 +129,7 @@ const fields = computed(() => [
|
||||
// { key: 'confirm_mail', label: t('confirmed') },
|
||||
// { key: 'has_elopage', label: 'elopage' },
|
||||
// { key: 'transactions_list', label: t('transaction') },
|
||||
{ key: 'status', label: t('status') },
|
||||
{ key: 'contributionStatus', label: t('status') },
|
||||
])
|
||||
|
||||
watch(
|
||||
|
||||
@ -5,6 +5,11 @@
|
||||
"locales": {},
|
||||
"lint": {
|
||||
"dependsOn": ["stylelint", "locales"]
|
||||
},
|
||||
"stylelint:fix": {},
|
||||
"locales:fix": {},
|
||||
"lint:fix": {
|
||||
"dependsOn": ["stylelint:fix", "locales:fix"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"lint:fix:unsafe": "biome check --fix --unsafe",
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix",
|
||||
"start": "cross-env TZ=UTC NODE_ENV=production node build/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
|
||||
@ -20,5 +20,5 @@ export class ContributionArgs {
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
creationDate: string
|
||||
contributionDate: string
|
||||
}
|
||||
|
||||
@ -1,58 +1,26 @@
|
||||
import { User as DbUser, Contribution as dbContribution } from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Contribution as DbContribution } from 'database'
|
||||
import { Field, Int, ObjectType } from 'type-graphql'
|
||||
|
||||
import { User } from './User'
|
||||
import { UnconfirmedContribution } from './UnconfirmedContribution'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
constructor(contribution: dbContribution, user?: DbUser | null) {
|
||||
this.id = contribution.id
|
||||
this.firstName = user?.firstName ?? null
|
||||
this.lastName = user?.lastName ?? null
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.createdAt = contribution.createdAt
|
||||
this.confirmedAt = contribution.confirmedAt
|
||||
this.confirmedBy = contribution.confirmedBy
|
||||
this.contributionDate = contribution.contributionDate
|
||||
this.status = contribution.contributionStatus
|
||||
this.messagesCount = contribution.messages ? 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
|
||||
if (user) {
|
||||
this.user = new User(user)
|
||||
}
|
||||
export class Contribution extends UnconfirmedContribution {
|
||||
constructor(dbContribution: DbContribution) {
|
||||
super(dbContribution)
|
||||
this.createdAt = dbContribution.createdAt
|
||||
this.confirmedAt = dbContribution.confirmedAt
|
||||
this.confirmedBy = dbContribution.confirmedBy
|
||||
this.contributionDate = dbContribution.contributionDate
|
||||
|
||||
this.deniedAt = dbContribution.deniedAt
|
||||
this.deniedBy = dbContribution.deniedBy
|
||||
this.deletedAt = dbContribution.deletedAt
|
||||
this.deletedBy = dbContribution.deletedBy
|
||||
this.updatedAt = dbContribution.updatedAt
|
||||
this.updatedBy = dbContribution.updatedBy
|
||||
this.resubmissionAt = dbContribution.resubmissionAt
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@ -83,24 +51,17 @@ export class Contribution {
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
|
||||
@Field(() => Int)
|
||||
messagesCount: number
|
||||
|
||||
@Field(() => String)
|
||||
status: string
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
moderatorId: number | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
resubmissionAt: Date | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionListResult {
|
||||
constructor(count: number, list: Contribution[]) {
|
||||
constructor(count: number, list: DbContribution[]) {
|
||||
this.contributionCount = count
|
||||
this.contributionList = list
|
||||
this.contributionList = list.map(
|
||||
(dbContribution: DbContribution) => new Contribution(dbContribution),
|
||||
)
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { ContributionMessage as DbContributionMessage, User } from 'database'
|
||||
import { ContributionMessage as DbContributionMessage } from 'database'
|
||||
import { Field, Int, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionMessage {
|
||||
constructor(contributionMessage: DbContributionMessage, user: User) {
|
||||
this.id = contributionMessage.id
|
||||
this.message = contributionMessage.message
|
||||
this.createdAt = contributionMessage.createdAt
|
||||
this.updatedAt = contributionMessage.updatedAt
|
||||
this.type = contributionMessage.type
|
||||
this.userFirstName = user.firstName
|
||||
this.userLastName = user.lastName
|
||||
this.userId = user.id
|
||||
this.isModerator = contributionMessage.isModerator
|
||||
constructor(dbContributionMessage: DbContributionMessage) {
|
||||
const user = dbContributionMessage.user
|
||||
this.id = dbContributionMessage.id
|
||||
this.message = dbContributionMessage.message
|
||||
this.createdAt = dbContributionMessage.createdAt
|
||||
this.updatedAt = dbContributionMessage.updatedAt
|
||||
this.type = dbContributionMessage.type
|
||||
this.userFirstName = user?.firstName ?? null
|
||||
this.userLastName = user?.lastName ?? null
|
||||
this.userId = user?.id ?? null
|
||||
this.isModerator = dbContributionMessage.isModerator
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
|
||||
@ -1,41 +1,45 @@
|
||||
import { Contribution, User } from 'database'
|
||||
import {
|
||||
Contribution as DbContribution,
|
||||
ContributionMessage as DbContributionMessage,
|
||||
} from 'database'
|
||||
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[]) {
|
||||
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
|
||||
}
|
||||
constructor(dbContribution: DbContribution) {
|
||||
const user = dbContribution.user
|
||||
this.id = dbContribution.id
|
||||
this.userId = dbContribution.userId
|
||||
this.amount = dbContribution.amount
|
||||
this.memo = dbContribution.memo
|
||||
this.contributionDate = dbContribution.contributionDate
|
||||
this.user = user ? new User(user) : null
|
||||
this.moderatorId = dbContribution.moderatorId
|
||||
this.contributionStatus = dbContribution.contributionStatus
|
||||
this.messagesCount = dbContribution.messages ? dbContribution.messages.length : 0
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
this.messages = dbContribution.messages
|
||||
? dbContribution.messages.map(
|
||||
(dbMessage: DbContributionMessage) => new ContributionMessage(dbMessage),
|
||||
)
|
||||
: 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
|
||||
@ -44,14 +48,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 @@ describe('ContributionMessageResolver', () => {
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
contributionDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
@ -138,7 +138,7 @@ describe('ContributionMessageResolver', () => {
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
contributionDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
const mutationResult = await mutate({
|
||||
|
||||
@ -77,7 +77,7 @@ export class ContributionMessageResolver {
|
||||
{ id: contributionId } as DbContribution,
|
||||
finalContributionMessage,
|
||||
)
|
||||
return new ContributionMessage(finalContributionMessage, user)
|
||||
return new ContributionMessage(finalContributionMessage)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
|
||||
@ -89,16 +89,12 @@ export class ContributionMessageResolver {
|
||||
): Promise<ContributionMessageListResult> {
|
||||
const [contributionMessages, count] = await findContributionMessages({
|
||||
contributionId,
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
pagination: { currentPage, pageSize, order },
|
||||
})
|
||||
|
||||
return {
|
||||
count,
|
||||
messages: contributionMessages.map(
|
||||
(message) => new ContributionMessage(message, message.user),
|
||||
),
|
||||
messages: contributionMessages.map((message) => new ContributionMessage(message)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,17 +107,13 @@ export class ContributionMessageResolver {
|
||||
): Promise<ContributionMessageListResult> {
|
||||
const [contributionMessages, count] = await findContributionMessages({
|
||||
contributionId,
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
pagination: { currentPage, pageSize, order },
|
||||
showModeratorType: true,
|
||||
})
|
||||
|
||||
return {
|
||||
count,
|
||||
messages: contributionMessages.map(
|
||||
(message) => new ContributionMessage(message, message.user),
|
||||
),
|
||||
messages: contributionMessages.map((message) => new ContributionMessage(message)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,6 +188,6 @@ export class ContributionMessageResolver {
|
||||
finalContribution,
|
||||
finalContributionMessage,
|
||||
)
|
||||
return new ContributionMessage(finalContributionMessage, moderator)
|
||||
return new ContributionMessage(finalContributionMessage)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,19 +6,7 @@ import {
|
||||
} from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import {
|
||||
Arg,
|
||||
Args,
|
||||
Authorized,
|
||||
Ctx,
|
||||
FieldResolver,
|
||||
Info,
|
||||
Int,
|
||||
Mutation,
|
||||
Query,
|
||||
Resolver,
|
||||
Root,
|
||||
} from 'type-graphql'
|
||||
import { Arg, Args, Authorized, Ctx, Info, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { EntityManager, IsNull, getConnection } from 'typeorm'
|
||||
|
||||
import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
|
||||
@ -26,7 +14,6 @@ import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
import { ContributionArgs } from '@arg/ContributionArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { ContributionType } from '@enum/ContributionType'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
@ -35,7 +22,6 @@ import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { Decay } from '@model/Decay'
|
||||
import { OpenCreation } from '@model/OpenCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { User } from '@model/User'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import {
|
||||
@ -62,9 +48,11 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { fullName } from '@/util/utilities'
|
||||
|
||||
import { findContribution } from './util/contributions'
|
||||
import { start } from 'repl'
|
||||
import { ContributionMessageType } from '../enum/ContributionMessageType'
|
||||
import { loadAllContributions, loadUserContributions } from './util/contributions'
|
||||
import { getOpenCreations, getUserCreation, validateContribution } from './util/creations'
|
||||
import { extractGraphQLFields, extractGraphQLFieldsForSelect } from './util/extractGraphQLFields'
|
||||
import { extractGraphQLFields } from './util/extractGraphQLFields'
|
||||
import { findContributions } from './util/findContributions'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
@ -74,17 +62,17 @@ export class ContributionResolver {
|
||||
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
|
||||
@Query(() => Contribution)
|
||||
async contribution(@Arg('id', () => Int) id: number): Promise<Contribution> {
|
||||
const contribution = await findContribution(id)
|
||||
if (!contribution) {
|
||||
const dbContribution = await DbContribution.findOne({ where: { id } })
|
||||
if (!dbContribution) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
return new Contribution(contribution)
|
||||
return new Contribution(dbContribution)
|
||||
}
|
||||
|
||||
@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)
|
||||
@ -92,14 +80,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
|
||||
@ -108,7 +96,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])
|
||||
@ -138,54 +126,55 @@ export class ContributionResolver {
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listContributions(
|
||||
@Ctx() context: Context,
|
||||
@Args()
|
||||
paginated: Paginated,
|
||||
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
||||
statusFilter?: ContributionStatus[] | null,
|
||||
@Arg('pagination') pagination: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const user = getUser(context)
|
||||
const filter = new SearchContributionsFilterArgs()
|
||||
filter.statusFilter = statusFilter
|
||||
filter.userId = user.id
|
||||
const [dbContributions, count] = await findContributions(paginated, filter, true, {
|
||||
messages: true,
|
||||
})
|
||||
const [dbContributions, count] = await loadUserContributions(user.id, pagination)
|
||||
|
||||
return new ContributionListResult(
|
||||
// show contributions in progress first
|
||||
const inProgressContributions = dbContributions.filter(
|
||||
(contribution) => contribution.contributionStatus === ContributionStatus.IN_PROGRESS,
|
||||
)
|
||||
const notInProgressContributions = dbContributions.filter(
|
||||
(contribution) => contribution.contributionStatus !== ContributionStatus.IN_PROGRESS,
|
||||
)
|
||||
|
||||
const result = new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map((contribution) => {
|
||||
[...inProgressContributions, ...notInProgressContributions].map((contribution) => {
|
||||
// filter out moderator messages for this call
|
||||
contribution.messages = contribution.messages?.filter(
|
||||
(m) => (m.type as ContributionMessageType) !== ContributionMessageType.MODERATOR,
|
||||
(message) =>
|
||||
(message.type as ContributionMessageType) !== ContributionMessageType.MODERATOR,
|
||||
)
|
||||
return new Contribution(contribution, user)
|
||||
return contribution
|
||||
}),
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
|
||||
@Query(() => Int)
|
||||
async countContributionsInProgress(@Ctx() context: Context): Promise<number> {
|
||||
const user = getUser(context)
|
||||
const count = await DbContribution.count({
|
||||
select: { id: true },
|
||||
where: { userId: user.id, contributionStatus: ContributionStatus.IN_PROGRESS },
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listAllContributions(
|
||||
@Args()
|
||||
paginated: Paginated,
|
||||
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
||||
statusFilter?: ContributionStatus[] | null,
|
||||
@Arg('pagination') pagination: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const filter = new SearchContributionsFilterArgs()
|
||||
filter.statusFilter = statusFilter
|
||||
const [dbContributions, count] = await findContributions(paginated, filter, false, {
|
||||
user: true,
|
||||
})
|
||||
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||
)
|
||||
const [dbContributions, count] = await loadAllContributions(pagination)
|
||||
return new ContributionListResult(count, dbContributions)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UPDATE_CONTRIBUTION])
|
||||
@ -201,8 +190,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) {
|
||||
@ -212,7 +200,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])
|
||||
@ -370,10 +358,7 @@ export class ContributionResolver {
|
||||
countOnly,
|
||||
)
|
||||
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||
)
|
||||
return new ContributionListResult(count, dbContributions)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||
@ -612,21 +597,4 @@ export class ContributionResolver {
|
||||
|
||||
return !!res
|
||||
}
|
||||
|
||||
// Field resolvers
|
||||
@Authorized([RIGHTS.USER])
|
||||
@FieldResolver(() => User)
|
||||
async user(
|
||||
@Root() contribution: DbContribution,
|
||||
@Info() info: GraphQLResolveInfo,
|
||||
): Promise<User> {
|
||||
let user = 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()
|
||||
}
|
||||
return new User(user)
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,7 +469,7 @@ describe('TransactionLinkResolver', () => {
|
||||
variables: {
|
||||
amount: new Decimal(1000),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
contributionDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
contribution = result.data.createContribution
|
||||
@ -512,7 +512,7 @@ describe('TransactionLinkResolver', () => {
|
||||
contributionId: contribution ? contribution.id : -1,
|
||||
amount: new Decimal(800),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
contributionDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -344,7 +344,7 @@ describe('send coins', () => {
|
||||
// create contribution as user bob
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||
variables: { amount: 1000, memo: 'testing', contributionDate: new Date().toISOString() },
|
||||
})
|
||||
|
||||
// login as admin
|
||||
|
||||
@ -458,7 +458,11 @@ describe('UserResolver', () => {
|
||||
// create contribution as user bob
|
||||
contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||
variables: {
|
||||
amount: 1000,
|
||||
memo: 'testing',
|
||||
contributionDate: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
// login as admin
|
||||
|
||||
@ -72,14 +72,14 @@ describe('semaphore', () => {
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
await creationFactory(testEnv, {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
await mutate({
|
||||
@ -121,7 +121,7 @@ describe('semaphore', () => {
|
||||
} = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: contributionDateFormatter(new Date()),
|
||||
contributionDate: contributionDateFormatter(new Date()),
|
||||
amount: 200,
|
||||
memo: 'Bobs Contribution',
|
||||
},
|
||||
@ -144,7 +144,7 @@ describe('semaphore', () => {
|
||||
} = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: contributionDateFormatter(new Date()),
|
||||
contributionDate: contributionDateFormatter(new Date()),
|
||||
amount: 200,
|
||||
memo: 'Bibis Contribution',
|
||||
},
|
||||
|
||||
@ -1,5 +1,93 @@
|
||||
import { Contribution } from 'database'
|
||||
import { Order } from '@/graphql/enum/Order'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { Contribution as DbContribution } from 'database'
|
||||
import { FindManyOptions, In } from 'typeorm'
|
||||
|
||||
export const findContribution = async (id: number): Promise<Contribution | null> => {
|
||||
return Contribution.findOne({ where: { id } })
|
||||
// TODO: combine with Pagination class for all queries to use
|
||||
function buildPaginationOptions(paginated: Paginated): FindManyOptions<DbContribution> {
|
||||
const { currentPage, pageSize } = paginated
|
||||
return {
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load user contributions with messages
|
||||
* @param userId if userId is set, load all contributions of the user, with messages
|
||||
* @param paginated pagination, see {@link Paginated}
|
||||
*/
|
||||
export const loadUserContributions = async (
|
||||
userId: number,
|
||||
paginated: Paginated,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const { order } = paginated
|
||||
// manual, faster and simpler queries as auto generated from typeorm
|
||||
const countPromise = DbContribution.count({
|
||||
select: { id: true },
|
||||
where: { userId },
|
||||
withDeleted: true,
|
||||
})
|
||||
// we collect all contributions, ignoring if user exist or not
|
||||
const contributionIds = await DbContribution.find({
|
||||
select: { id: true },
|
||||
where: { userId },
|
||||
withDeleted: true,
|
||||
order: { createdAt: order, id: order },
|
||||
...buildPaginationOptions(paginated),
|
||||
})
|
||||
const contributionsPromise = DbContribution.find({
|
||||
relations: { messages: { user: true } },
|
||||
withDeleted: true,
|
||||
order: { createdAt: order, id: order, messages: { createdAt: Order.ASC } },
|
||||
where: { id: In(contributionIds.map((c) => c.id)) },
|
||||
})
|
||||
return [await contributionsPromise, await countPromise]
|
||||
|
||||
// original code
|
||||
// note: typeorm will create similar queries as above, but more complex and slower
|
||||
/*
|
||||
return DbContribution.findAndCount({
|
||||
where: { userId },
|
||||
withDeleted: true,
|
||||
relations: { messages: { user: true } },
|
||||
order: { createdAt: order, id: order, messages: { createdAt: Order.ASC } },
|
||||
...buildPaginationOptions(paginated),
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
* Load all contributions
|
||||
* @param paginated pagination, see {@link Paginated}
|
||||
*/
|
||||
export const loadAllContributions = async (
|
||||
paginated: Paginated,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const { order } = paginated
|
||||
// manual, faster queries as auto generated from typeorm
|
||||
const countPromise = DbContribution.count({ select: { id: true } })
|
||||
// console.log('loadAllContributions', { count })
|
||||
|
||||
const contributionIds = await DbContribution.find({
|
||||
select: { id: true },
|
||||
order: { createdAt: order, id: order },
|
||||
...buildPaginationOptions(paginated),
|
||||
})
|
||||
const contributionsPromise = DbContribution.find({
|
||||
relations: { user: { emailContact: true } },
|
||||
order: { createdAt: order, id: order },
|
||||
where: { id: In(contributionIds.map((c) => c.id)) },
|
||||
})
|
||||
return [await contributionsPromise, await countPromise]
|
||||
|
||||
// original code
|
||||
// note: typeorm will create similar queries as above, but more complex and slower
|
||||
/*
|
||||
return DbContribution.findAndCount({
|
||||
relations: { user: { emailContact: true } },
|
||||
order: { createdAt: order, id: order },
|
||||
...buildPaginationOptions(paginated),
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ describe('util/creation', () => {
|
||||
variables: {
|
||||
amount: 400.0,
|
||||
memo: 'Contribution for this month',
|
||||
creationDate: contributionDateFormatter(now),
|
||||
contributionDate: contributionDateFormatter(now),
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
@ -105,7 +105,7 @@ describe('util/creation', () => {
|
||||
variables: {
|
||||
amount: 500.0,
|
||||
memo: 'Contribution for the last month',
|
||||
creationDate: contributionDateFormatter(
|
||||
contributionDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
},
|
||||
|
||||
@ -1,28 +1,26 @@
|
||||
import { ContributionMessage as DbContributionMessage } from 'database'
|
||||
import { In } from 'typeorm'
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
interface FindContributionMessagesOptions {
|
||||
contributionId: number
|
||||
pageSize: number
|
||||
currentPage: number
|
||||
order: Order
|
||||
pagination: Paginated
|
||||
showModeratorType?: boolean
|
||||
}
|
||||
|
||||
export const findContributionMessages = async (
|
||||
options: FindContributionMessagesOptions,
|
||||
): Promise<[DbContributionMessage[], number]> => {
|
||||
const { contributionId, pageSize, currentPage, order, showModeratorType } = options
|
||||
const { contributionId, pagination, showModeratorType } = options
|
||||
|
||||
const messageTypes = [ContributionMessageType.DIALOG, ContributionMessageType.HISTORY]
|
||||
|
||||
if (showModeratorType) {
|
||||
messageTypes.push(ContributionMessageType.MODERATOR)
|
||||
}
|
||||
|
||||
const { currentPage, pageSize, order } = pagination
|
||||
return DbContributionMessage.findAndCount({
|
||||
where: {
|
||||
contributionId,
|
||||
|
||||
@ -17,14 +17,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
|
||||
|
||||
@ -2,7 +2,7 @@ export interface CreationInterface {
|
||||
email: string
|
||||
amount: number
|
||||
memo: string
|
||||
creationDate: string
|
||||
contributionDate: string
|
||||
confirmed?: boolean
|
||||
// number of months to move the confirmed creation to the past
|
||||
moveCreationDate?: number
|
||||
|
||||
@ -122,7 +122,7 @@ bobsSendings.forEach((sending) => {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: sending.amount,
|
||||
memo: sending.memo,
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
})
|
||||
@ -132,7 +132,7 @@ export const creations: CreationInterface[] = [
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
moveCreationDate: 12,
|
||||
},
|
||||
@ -140,7 +140,7 @@ export const creations: CreationInterface[] = [
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: '#Hexen',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
moveCreationDate: 12,
|
||||
},
|
||||
@ -149,7 +149,7 @@ export const creations: CreationInterface[] = [
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
|
||||
@ -33,7 +33,7 @@ export const creationFactory = async (
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
where: { userId: user.id, creationDate: new Date(creation.contributionDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
|
||||
|
||||
@ -265,8 +265,8 @@ export const deleteContributionLink = gql`
|
||||
`
|
||||
|
||||
export const createContribution = gql`
|
||||
mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) {
|
||||
mutation ($amount: Decimal!, $memo: String!, $contributionDate: String!) {
|
||||
createContribution(amount: $amount, memo: $memo, contributionDate: $contributionDate) {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
@ -276,12 +276,12 @@ export const createContribution = gql`
|
||||
`
|
||||
|
||||
export const updateContribution = gql`
|
||||
mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $contributionDate: String!) {
|
||||
updateContribution(
|
||||
contributionId: $contributionId
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
contributionDate: $contributionDate
|
||||
) {
|
||||
id
|
||||
amount
|
||||
|
||||
@ -244,52 +244,47 @@ export const queryTransactionLink = gql`
|
||||
`
|
||||
|
||||
export const listContributions = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 5
|
||||
$order: Order
|
||||
$statusFilter: [ContributionStatus!]
|
||||
) {
|
||||
listContributions(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
statusFilter: $statusFilter
|
||||
) {
|
||||
query ($pagination: Paginated!) {
|
||||
listContributions(pagination: $pagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
contributionStatus
|
||||
messagesCount
|
||||
createdAt
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
deletedAt
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
updatedBy
|
||||
updatedAt
|
||||
deletedAt
|
||||
moderatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const listAllContributions = `
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusFilter: [ContributionStatus!]) {
|
||||
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order, statusFilter: $statusFilter) {
|
||||
query ($pagination: Paginated!) {
|
||||
listAllContributions(pagination: $pagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
user {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
contributionDate
|
||||
status
|
||||
contributionStatus
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
@ -318,7 +313,7 @@ export const adminListContributions = gql`
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
contributionDate
|
||||
status
|
||||
contributionStatus
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
|
||||
@ -38,7 +38,8 @@ export class Connection {
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
// workaround to let previous path working, because with esbuild the script root path has changed
|
||||
logPath: (CONFIG.PRODUCTION ? '../' : '') + CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
|
||||
@ -6,9 +6,13 @@
|
||||
"cache": false
|
||||
},
|
||||
"locales": {},
|
||||
"locales:fix": {},
|
||||
"lint": {
|
||||
"dependsOn": ["locales", "database#build"]
|
||||
},
|
||||
"lint:fix": {
|
||||
"dependsOn": ["locales:fix", "database#build"]
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["database#build", "config-schema#build"]
|
||||
},
|
||||
|
||||
@ -107,8 +107,16 @@ exec > >(tee -a $UPDATE_HTML) 2>&1
|
||||
|
||||
# configure nginx for the update-page
|
||||
echo 'Configuring nginx to serve the update-page'
|
||||
nginx_restart() {
|
||||
sudo /etc/init.d/nginx restart || {
|
||||
echo -e "\e[33mwarn: nginx restart failed, will try to fix with 'sudo systemctl reset-failed nginx' and 'sudo systemctl start nginx'\e[0m" > /dev/tty
|
||||
sudo systemctl reset-failed nginx
|
||||
sudo systemctl start nginx
|
||||
}
|
||||
}
|
||||
nginx_restart
|
||||
ln -sf $SCRIPT_DIR/nginx/sites-available/update-page.conf $SCRIPT_DIR/nginx/sites-enabled/default
|
||||
sudo /etc/init.d/nginx restart
|
||||
|
||||
|
||||
# helper functions
|
||||
log_step() {
|
||||
@ -141,7 +149,7 @@ onError() {
|
||||
log_error "( x.x ) Exit Code: $exit_code"
|
||||
log_error " > < Offending command: '$BASH_COMMAND'"
|
||||
log_error ""
|
||||
exit 1
|
||||
exit 1
|
||||
}
|
||||
trap onError ERR
|
||||
|
||||
@ -314,8 +322,7 @@ done
|
||||
# let nginx showing gradido
|
||||
log_step 'Configuring nginx to serve gradido again'
|
||||
ln -sf $SCRIPT_DIR/nginx/sites-available/gradido.conf $SCRIPT_DIR/nginx/sites-enabled/default
|
||||
sudo nginx -t
|
||||
sudo /etc/init.d/nginx restart
|
||||
nginx_restart
|
||||
|
||||
# keep the update log
|
||||
cat $UPDATE_HTML >> $GRADIDO_LOG_PATH/update.$TODAY.log
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { CONFIG } from '@/config'
|
||||
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||
// We cannot use our connection here, but must use the external typeorm installation
|
||||
import { entities } from 'database'
|
||||
import { Connection, FileLogger, createConnection } from 'typeorm'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
export const connection = async (): Promise<Connection | null> => {
|
||||
try {
|
||||
return createConnection({
|
||||
@ -19,7 +18,8 @@ export const connection = async (): Promise<Connection | null> => {
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
// workaround to let previous path working, because with esbuild the script root path has changed
|
||||
logPath: '../' + CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
|
||||
@ -18,7 +18,8 @@ const connection = async (): Promise<Connection | null> => {
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
// workaround to let previous path working, because with esbuild the script root path has changed
|
||||
logPath: '../' + CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
|
||||
@ -13,9 +13,9 @@ ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||
ARG BUILD_VERSION
|
||||
ENV BUILD_VERSION=${BUILD_VERSION:-'broken'}
|
||||
ARG BUILD_COMMIT
|
||||
ENV BUILD_COMMIT=${BUILD_COMMIT:-'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'}
|
||||
ENV BUILD_COMMIT=${BUILD_COMMIT:-'decafcabdecafcabdecafcabdecafcabdecafcab'}
|
||||
ARG BUILD_COMMIT_SHORT
|
||||
ENV BUILD_COMMIT_SHORT=${BUILD_COMMIT_SHORT:-'deadbeef'}
|
||||
ENV BUILD_COMMIT_SHORT=${BUILD_COMMIT_SHORT:-'decafcab'}
|
||||
## SET NODE_ENV
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
@ -9,12 +9,15 @@
|
||||
"postbuild": "uname | grep -q Linux && find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} + || echo 'Skip precompress on non-Linux'",
|
||||
"analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||
"lint:fix": "eslint --max-warnings=0 --ext .js,.vue,.json . --fix",
|
||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||
"stylelint:fix": "stylelint --max-warnings=0 '**/*.{scss,vue}' --fix",
|
||||
"test": "cross-env TZ=UTC vitest run",
|
||||
"test:coverage": "cross-env TZ=UTC vitest run --coverage",
|
||||
"test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs",
|
||||
"test:watch": "cross-env TZ=UTC vitest",
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix",
|
||||
"compile-scss": "node ./scripts/scss.mjs compile",
|
||||
"watch-scss": "node ./scripts/scss.mjs watch",
|
||||
"compile-scss-sass": "node ./scripts/scss.mjs compile sass",
|
||||
|
||||
@ -52,7 +52,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockMutate.mockResolvedValue({})
|
||||
mockMutate.mockResolvedValue({ data: { createContributionMessage: {} } })
|
||||
wrapper = createWrapper()
|
||||
})
|
||||
|
||||
@ -66,7 +66,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
|
||||
it('resets the form on reset event', async () => {
|
||||
await wrapper.find('form').trigger('reset')
|
||||
expect(wrapper.vm.form.text).toBe('')
|
||||
expect(wrapper.vm.formText).toBe('')
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
@ -82,19 +82,14 @@ describe('ContributionMessagesFormular', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('emits get-list-contribution-messages event', async () => {
|
||||
it('emits add-contribution-message event', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('get-list-contribution-messages')).toEqual([[false]])
|
||||
})
|
||||
|
||||
it('emits update-status event', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('update-status')).toEqual([[42]])
|
||||
expect(wrapper.emitted('add-contribution-message')).toEqual([[{}]])
|
||||
})
|
||||
|
||||
it('resets the form text', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.vm.form.text).toBe('')
|
||||
expect(wrapper.vm.formText).toBe('')
|
||||
})
|
||||
|
||||
it('shows success toast', async () => {
|
||||
|
||||
@ -3,13 +3,7 @@
|
||||
<small class="ps-2 pt-3">{{ $t('form.reply') }}</small>
|
||||
<div>
|
||||
<BForm @submit.prevent="onSubmit" @reset="onReset">
|
||||
<BFormTextarea
|
||||
id="textarea"
|
||||
:model-value="form.text"
|
||||
:placeholder="$t('form.memo')"
|
||||
:rows="3"
|
||||
@update:model-value="form.text = $event"
|
||||
/>
|
||||
<BFormTextarea id="textarea" v-model="formText" :placeholder="$t('form.memo')" :rows="3" />
|
||||
<BRow class="mt-4 mb-4">
|
||||
<BCol>
|
||||
<BButton type="reset" variant="secondary">{{ $t('form.cancel') }}</BButton>
|
||||
@ -39,35 +33,31 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['get-list-contribution-messages', 'update-status'])
|
||||
const emit = defineEmits(['add-contribution-message'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
|
||||
const { mutate: createContributionMessageMutation } = useMutation(createContributionMessage)
|
||||
|
||||
const form = ref({
|
||||
text: '',
|
||||
})
|
||||
const formText = ref('')
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const disabled = computed(() => {
|
||||
return form.value.text === '' || isSubmitting.value
|
||||
return formText.value === '' || isSubmitting.value
|
||||
})
|
||||
|
||||
async function onSubmit() {
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
await createContributionMessageMutation({
|
||||
const result = await createContributionMessageMutation({
|
||||
contributionId: props.contributionId,
|
||||
message: form.value.text,
|
||||
message: formText.value,
|
||||
})
|
||||
|
||||
emit('get-list-contribution-messages', false)
|
||||
emit('update-status', props.contributionId)
|
||||
form.value.text = ''
|
||||
formText.value = ''
|
||||
emit('add-contribution-message', result.data.createContributionMessage)
|
||||
toastSuccess(t('message.reply'))
|
||||
} catch (error) {
|
||||
toastError(error.message)
|
||||
@ -77,6 +67,6 @@ async function onSubmit() {
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
form.value.text = ''
|
||||
formText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -77,16 +77,4 @@ describe('ContributionMessagesList', () => {
|
||||
expect(wrapper.find('button').text()).toContain('form.close')
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper()
|
||||
})
|
||||
|
||||
it('emits update-status event when updateStatus method is called', async () => {
|
||||
await wrapper.vm.updateStatus(42)
|
||||
expect(wrapper.emitted('update-status')).toBeTruthy()
|
||||
expect(wrapper.emitted('update-status')[0]).toEqual([42])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,12 +10,11 @@
|
||||
v-if="['PENDING', 'IN_PROGRESS'].includes(status)"
|
||||
:contribution-id="contributionId"
|
||||
v-bind="$attrs"
|
||||
@update-status="updateStatus"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-b-toggle="'collapse' + String(contributionId)" class="text-center pointer clearboth">
|
||||
<BButton variant="outline-primary" block class="mb-3">
|
||||
<div class="text-center pointer clearboth">
|
||||
<BButton variant="outline-primary" block @click="$emit('close-messages-list')">
|
||||
<IBiArrowUpShort />
|
||||
{{ $t('form.close') }}
|
||||
</BButton>
|
||||
@ -46,11 +45,6 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateStatus(id) {
|
||||
this.$emit('update-status', id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@ -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>
|
||||
|
||||
71
frontend/src/components/Contributions/ContributionCreate.vue
Normal file
71
frontend/src/components/Contributions/ContributionCreate.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<transition name="fade-out" @after-leave="resetForm">
|
||||
<div v-if="showForm">
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</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())
|
||||
const showForm = ref(true)
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
contributionDate: undefined,
|
||||
memo: '',
|
||||
hours: '',
|
||||
amount: GDD_PER_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateContribution(contribution) {
|
||||
try {
|
||||
await createContributionMutation({ ...contribution })
|
||||
toastSuccess(t('contribution.submitted'))
|
||||
showForm.value = false
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
refetch()
|
||||
showForm.value = true
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.fade-out-enter-active,
|
||||
.fade-out-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.fade-out-enter-from,
|
||||
.fade-out-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
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"
|
||||
@abort="emit('contribution-updated')"
|
||||
/>
|
||||
</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'])
|
||||
|
||||
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,7 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionForm from './ContributionForm.vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
// Mock external components and dependencies
|
||||
vi.mock('@/components/Inputs/InputAmount', () => ({
|
||||
@ -37,12 +36,16 @@ vi.mock('vee-validate', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
const global = {
|
||||
stubs: ['BForm', 'BFormInput', 'BRow', 'BCol', 'BButton'],
|
||||
}
|
||||
|
||||
describe('ContributionForm', () => {
|
||||
let wrapper
|
||||
|
||||
const defaultProps = {
|
||||
modelValue: {
|
||||
date: '2024-09-12',
|
||||
contributionDate: '2024-09-12',
|
||||
memo: 'Test memo',
|
||||
hours: 2,
|
||||
amount: 40,
|
||||
@ -51,12 +54,26 @@ describe('ContributionForm', () => {
|
||||
maxGddThisMonth: 200,
|
||||
}
|
||||
|
||||
const createWrapperWithDate = (date) => {
|
||||
return mount(ContributionForm, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
modelValue: {
|
||||
...defaultProps.modelValue,
|
||||
contributionDate: date.toISOString(),
|
||||
},
|
||||
},
|
||||
global,
|
||||
})
|
||||
}
|
||||
const thisMonth = new Date()
|
||||
const lastMonth = new Date()
|
||||
lastMonth.setMonth(lastMonth.getMonth() - 1, 1)
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(ContributionForm, {
|
||||
props: defaultProps,
|
||||
global: {
|
||||
stubs: ['BForm', 'BFormInput', 'BRow', 'BCol', 'BButton'],
|
||||
},
|
||||
global,
|
||||
})
|
||||
})
|
||||
|
||||
@ -65,19 +82,17 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
|
||||
describe('compute isThisMonth', () => {
|
||||
it('return true', async () => {
|
||||
await wrapper.setProps({
|
||||
modelValue: { date: new Date().toISOString() },
|
||||
})
|
||||
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)
|
||||
it.each([
|
||||
[thisMonth, true, 'should return true for current month'],
|
||||
[lastMonth, false, 'should return false for last month'],
|
||||
[
|
||||
new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
false,
|
||||
'should return false for next year',
|
||||
],
|
||||
])('%s => %s (%s)', (date, expected, desc) => {
|
||||
const wrapper = createWrapperWithDate(date)
|
||||
expect(wrapper.vm.isThisMonth).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -85,21 +100,39 @@ describe('ContributionForm', () => {
|
||||
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() },
|
||||
describe('if form.date is in last month', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapperWithDate(lastMonth)
|
||||
})
|
||||
it('if max gdd for this month is 0', async () => {
|
||||
await wrapper.setProps({
|
||||
maxGddThisMonth: 0,
|
||||
})
|
||||
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||
})
|
||||
it('if max gdd last month is 0', async () => {
|
||||
await wrapper.setProps({
|
||||
maxGddLastMonth: 0,
|
||||
})
|
||||
expect(wrapper.vm.noOpenCreation).toBe('contribution.noOpenCreation.lastMonth')
|
||||
})
|
||||
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() },
|
||||
describe('if form.date is in this month', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapperWithDate(thisMonth)
|
||||
})
|
||||
it('if max gdd for last month is 0', async () => {
|
||||
await wrapper.setProps({
|
||||
maxGddLastMonth: 0,
|
||||
})
|
||||
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||
})
|
||||
it('if max gdd this month is 0', async () => {
|
||||
await wrapper.setProps({
|
||||
maxGddThisMonth: 0,
|
||||
})
|
||||
expect(wrapper.vm.noOpenCreation).toBe('contribution.noOpenCreation.thisMonth')
|
||||
})
|
||||
expect(wrapper.vm.noOpenCreation).toBeUndefined()
|
||||
})
|
||||
it('if max gdd is 0 for both months', async () => {
|
||||
await wrapper.setProps({
|
||||
@ -108,30 +141,12 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
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 () => {
|
||||
expect(wrapper.vm.disabled).toBe(true)
|
||||
|
||||
await wrapper.setProps({
|
||||
modelValue: { date: new Date().toISOString().slice(0, 10) },
|
||||
})
|
||||
wrapper = createWrapperWithDate(thisMonth)
|
||||
|
||||
wrapper.vm.form.amount = 100
|
||||
|
||||
@ -154,7 +169,7 @@ describe('ContributionForm', () => {
|
||||
expect(wrapper.vm.form.amount).toBe('60.00')
|
||||
})
|
||||
|
||||
it('emits update-contribution event on submit for existing contribution', async () => {
|
||||
it('emits upsert-contribution event on submit for existing contribution', async () => {
|
||||
const existingContribution = {
|
||||
...defaultProps.modelValue,
|
||||
id: '123',
|
||||
@ -174,17 +189,17 @@ describe('ContributionForm', () => {
|
||||
|
||||
wrapper.vm.submit()
|
||||
|
||||
expect(wrapper.emitted('update-contribution')).toBeTruthy()
|
||||
expect(wrapper.emitted('update-contribution')[0][0]).toEqual(
|
||||
expect(wrapper.emitted('upsert-contribution')).toBeTruthy()
|
||||
expect(wrapper.emitted('upsert-contribution')[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits set-contribution event on submit for new contribution', async () => {
|
||||
it('emits upsert-contribution event on submit for new contribution', async () => {
|
||||
wrapper.vm.submit()
|
||||
|
||||
expect(wrapper.emitted('set-contribution')).toBeTruthy()
|
||||
expect(wrapper.emitted('upsert-contribution')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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('abort')"
|
||||
>
|
||||
{{ $t('form.cancel') }}
|
||||
</BButton>
|
||||
@ -78,45 +84,56 @@
|
||||
</BForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import { reactive, computed, ref, onMounted, onUnmounted, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ValidatedInput from '@/components/Inputs/ValidatedInput'
|
||||
import LabeledInput from '@/components/Inputs/LabeledInput'
|
||||
import { memo as memoSchema } from '@/validationSchemas'
|
||||
import OpenCreationsAmount from './OpenCreationsAmount.vue'
|
||||
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 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', 'abort'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const form = reactive({ ...props.modelValue })
|
||||
const entityDataToForm = computed(() => ({
|
||||
...props.modelValue,
|
||||
hours:
|
||||
props.modelValue.hours !== undefined
|
||||
? props.modelValue.hours
|
||||
: amountToHours(props.modelValue.amount),
|
||||
contributionDate: props.modelValue.contributionDate
|
||||
? new Date(props.modelValue.contributionDate).toISOString().slice(0, 10)
|
||||
: undefined,
|
||||
}))
|
||||
|
||||
// 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),
|
||||
)
|
||||
const form = reactive({ ...entityDataToForm.value })
|
||||
|
||||
// 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 +146,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 +166,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 +182,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,11 +1,40 @@
|
||||
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'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
contribution: {
|
||||
noContributions: {
|
||||
myContributions: 'No contributions',
|
||||
emptyPage: 'No contributions',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: { template: '<div>Home</div>' },
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'test',
|
||||
component: ContributionList,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
vi.mock('@/components/Contributions/ContributionListItem.vue', () => ({
|
||||
@ -15,11 +44,15 @@ vi.mock('@/components/Contributions/ContributionListItem.vue', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useQuery: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ContributionList', () => {
|
||||
let wrapper
|
||||
|
||||
const global = {
|
||||
plugins: [i18n],
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$filters: {
|
||||
GDD: vi.fn((val) => val),
|
||||
@ -30,47 +63,69 @@ describe('ContributionList', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionCount: 3,
|
||||
showPagination: true,
|
||||
pageSize: 25,
|
||||
items: [
|
||||
{
|
||||
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 contributions = ref({
|
||||
listContributions: {
|
||||
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 mountWrapper = () => {
|
||||
return mount(ContributionList, {
|
||||
props: propsData,
|
||||
global,
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
describe('mount', () => {
|
||||
const mockListContributionsQuery = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useQuery).mockImplementation((query) => {
|
||||
return {
|
||||
result: contributions,
|
||||
loading,
|
||||
onResult: mockListContributionsQuery,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
wrapper = mountWrapper()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('mount as user contributions list', () => {
|
||||
it('fetches initial data', () => {
|
||||
expect(mockListContributionsQuery).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list', () => {
|
||||
expect(wrapper.find('div.contribution-list').exists()).toBe(true)
|
||||
})
|
||||
@ -84,7 +139,7 @@ describe('ContributionList', () => {
|
||||
|
||||
describe('list count greater than page size', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ contributionCount: 33 })
|
||||
contributions.value.listContributions.contributionCount = 33
|
||||
})
|
||||
|
||||
it('has pagination buttons', () => {
|
||||
@ -97,17 +152,17 @@ describe('ContributionList', () => {
|
||||
window.scrollTo = scrollToMock
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ contributionCount: 33 })
|
||||
await wrapper.findComponent({ name: 'BPagination' }).vm.$emit('update:modelValue', 2)
|
||||
contributions.value.listContributions.contributionCount = 33
|
||||
await wrapper
|
||||
.findComponent({ name: 'PaginatorRouteParamsPage' })
|
||||
.vm.$emit('update:modelValue', 2)
|
||||
})
|
||||
|
||||
it('emits update contribution list', () => {
|
||||
expect(wrapper.emitted('update-list-contributions')).toEqual([
|
||||
[{ currentPage: 2, pageSize: 25 }],
|
||||
])
|
||||
it('updates current page', () => {
|
||||
expect(wrapper.vm.currentPage).toBe(2)
|
||||
})
|
||||
|
||||
it('scrolls to top', () => {
|
||||
it.skip('scrolls to top', () => {
|
||||
expect(scrollToMock).toHaveBeenCalledWith(0, 0)
|
||||
})
|
||||
})
|
||||
@ -121,31 +176,7 @@ describe('ContributionList', () => {
|
||||
})
|
||||
|
||||
it('emits update contribution form', () => {
|
||||
expect(wrapper.emitted('update-contribution-form')).toEqual([['item']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionListItem' })
|
||||
.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('emits delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('update status', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionListItem' })
|
||||
.vm.$emit('update-status', { id: 2 })
|
||||
})
|
||||
|
||||
it('emits update status', () => {
|
||||
expect(wrapper.emitted('update-status')).toEqual([[{ id: 2 }]])
|
||||
expect(wrapper.emitted('update-contribution-form')).toEqual([[{ item: 'item', page: 1 }]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,107 +1,107 @@
|
||||
<template>
|
||||
<div class="contribution-list">
|
||||
<div v-for="item in items" :key="item.id + 'a'" class="mb-3">
|
||||
<contribution-list-item
|
||||
v-if="item.status === 'IN_PROGRESS'"
|
||||
v-bind="item"
|
||||
:contribution-id="item.id"
|
||||
:all-contribution="allContribution"
|
||||
@close-all-open-collapse="$emit('close-all-open-collapse')"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
@update-status="updateStatus"
|
||||
/>
|
||||
<div v-if="items.length === 0 && !loading">
|
||||
<div v-if="currentPage === 1">
|
||||
{{ t('contribution.noContributions.myContributions') }}
|
||||
</div>
|
||||
<div v-for="item2 in items" :key="item2.id" class="mb-3">
|
||||
<contribution-list-item
|
||||
v-if="item2.status !== 'IN_PROGRESS'"
|
||||
v-bind="item2"
|
||||
:contribution-id="item2.id"
|
||||
:all-contribution="allContribution"
|
||||
@close-all-open-collapse="$emit('close-all-open-collapse')"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
@update-status="updateStatus"
|
||||
/>
|
||||
<div v-else>
|
||||
{{ t('contribution.noContributions.emptyPage') }}
|
||||
</div>
|
||||
<BPagination
|
||||
v-if="isPaginationVisible"
|
||||
:model-value="currentPage"
|
||||
class="mt-3"
|
||||
pills
|
||||
size="lg"
|
||||
:per-page="pageSize"
|
||||
:total-rows="contributionCount"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
@update:model-value="currentPage = $event"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="contribution-list">
|
||||
<div v-for="item in items" :key="item.id + 'a'" class="mb-3">
|
||||
<div :id="`contributionListItem-${item.id}`">
|
||||
<contribution-list-item
|
||||
v-bind="item"
|
||||
:contribution-id="item.id"
|
||||
:messages-visible="openMessagesListId === item.id"
|
||||
@toggle-messages-visible="toggleMessagesVisible(item.id)"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@contribution-changed="refetch()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<paginator-route-params-page
|
||||
v-model="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total-count="contributionCount"
|
||||
:loading="loading"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import ContributionListItem from '@/components/Contributions/ContributionListItem.vue'
|
||||
import { listContributions } from '@/graphql/contributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { PAGE_SIZE } from '@/constants'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import CONFIG from '@/config'
|
||||
import { useRoute } from 'vue-router'
|
||||
import PaginatorRouteParamsPage from '@/components/PaginatorRouteParamsPage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
contributionCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showPagination: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
allContribution: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
const route = useRoute()
|
||||
|
||||
// composables
|
||||
const { t } = useI18n()
|
||||
|
||||
// constants
|
||||
const pageSize = PAGE_SIZE
|
||||
const pollInterval = CONFIG.AUTO_POLL_INTERVAL || undefined
|
||||
|
||||
// events
|
||||
const emit = defineEmits(['update-contribution-form'])
|
||||
|
||||
// refs
|
||||
const currentPage = ref(Number(route.params.page) || 1)
|
||||
const openMessagesListId = ref(null)
|
||||
|
||||
// queries
|
||||
const { result, loading, refetch, onResult } = useQuery(
|
||||
listContributions,
|
||||
() => ({
|
||||
pagination: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize,
|
||||
order: 'DESC',
|
||||
},
|
||||
}),
|
||||
{
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval,
|
||||
},
|
||||
)
|
||||
|
||||
// computed
|
||||
const contributionCount = computed(() => {
|
||||
return result.value?.listContributions.contributionCount || 0
|
||||
})
|
||||
const items = computed(() => {
|
||||
return [...(result.value?.listContributions.contributionList || [])]
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'close-all-open-collapse',
|
||||
'update-list-contributions',
|
||||
'update-contribution-form',
|
||||
'delete-contribution',
|
||||
'update-status',
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const messages = ref([])
|
||||
|
||||
const isPaginationVisible = computed(() => {
|
||||
return props.showPagination && props.pageSize < props.contributionCount
|
||||
})
|
||||
|
||||
watch(currentPage, () => {
|
||||
updateListContributions()
|
||||
})
|
||||
|
||||
const updateListContributions = () => {
|
||||
emit('update-list-contributions', {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: props.pageSize,
|
||||
// callbacks
|
||||
// scroll to anchor, if hash ist present in url and after data where loaded
|
||||
onResult(({ _data }) => {
|
||||
nextTick(() => {
|
||||
if (!route.hash) {
|
||||
return
|
||||
}
|
||||
const el = document.querySelector(route.hash)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
|
||||
// methods
|
||||
const toggleMessagesVisible = (contributionId) => {
|
||||
if (openMessagesListId.value === contributionId) {
|
||||
openMessagesListId.value = 0
|
||||
} else {
|
||||
openMessagesListId.value = contributionId
|
||||
}
|
||||
}
|
||||
const updateContributionForm = (item) => {
|
||||
emit('update-contribution-form', item)
|
||||
}
|
||||
|
||||
const deleteContribution = (item) => {
|
||||
emit('delete-contribution', item)
|
||||
}
|
||||
|
||||
const updateStatus = (id) => {
|
||||
emit('update-status', id)
|
||||
emit('update-contribution-form', { item, page: currentPage.value })
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,158 @@
|
||||
import { listAllContributions } from '@/graphql/contributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ContributionListAll from './ContributionListAll.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: { template: '<div>Home</div>' },
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'test',
|
||||
component: ContributionListAll,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
vi.mock('@/components/Contributions/ContributionListAllItem.vue', () => ({
|
||||
default: {
|
||||
name: 'ContributionListAllItem',
|
||||
template: '<div></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useQuery: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ContributionListAll', () => {
|
||||
let wrapper
|
||||
|
||||
const global = {
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$filters: {
|
||||
GDD: vi.fn((val) => val),
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
BPagination: true,
|
||||
},
|
||||
}
|
||||
|
||||
const allContributions = ref({
|
||||
listAllContributions: {
|
||||
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 mountWrapper = () => {
|
||||
return mount(ContributionListAll, {
|
||||
global,
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useQuery).mockImplementation((query) => {
|
||||
return {
|
||||
result: allContributions,
|
||||
loading,
|
||||
}
|
||||
})
|
||||
|
||||
wrapper = mountWrapper()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('mount as all contributions list', () => {
|
||||
it('fetches initial data', () => {
|
||||
expect(useQuery).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list-all', () => {
|
||||
expect(wrapper.find('div.contribution-list-all').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('pagination', () => {
|
||||
describe('list count smaller than page size', () => {
|
||||
it('has no pagination buttons', () => {
|
||||
expect(wrapper.find('b-pagination-stub').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('list count greater than page size', () => {
|
||||
beforeEach(async () => {
|
||||
allContributions.value.listAllContributions.contributionCount = 33
|
||||
})
|
||||
|
||||
it('has pagination buttons', () => {
|
||||
expect(wrapper.find('b-pagination-stub').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('switch page', () => {
|
||||
const scrollToMock = vi.fn()
|
||||
window.scrollTo = scrollToMock
|
||||
|
||||
beforeEach(async () => {
|
||||
allContributions.value.listAllContributions.contributionCount = 33
|
||||
await wrapper
|
||||
.findComponent({ name: 'PaginatorRouteParamsPage' })
|
||||
.vm.$emit('update:modelValue', 2)
|
||||
})
|
||||
|
||||
it('updates current page', () => {
|
||||
expect(wrapper.vm.currentPage).toBe(2)
|
||||
})
|
||||
|
||||
it.skip('scrolls to top', () => {
|
||||
expect(scrollToMock).toHaveBeenCalledWith(0, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="items.length === 0 && !loading">
|
||||
<div v-if="currentPage === 1">
|
||||
{{ $t('contribution.noContributions.allContributions') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('contribution.noContributions.emptyPage') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="contribution-list-all">
|
||||
<div v-for="item in items" :key="item.id + 'a'" class="mb-3">
|
||||
<div :id="`contributionListItem-${item.id}`">
|
||||
<contribution-list-all-item v-bind="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<paginator-route-params-page
|
||||
v-model="currentPage"
|
||||
:total-count="contributionCount"
|
||||
:loading="loading"
|
||||
:page-size="pageSize"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import ContributionListAllItem from '@/components/Contributions/ContributionListAllItem.vue'
|
||||
import { listAllContributions } from '@/graphql/contributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import CONFIG from '@/config'
|
||||
import PaginatorRouteParamsPage from '@/components/PaginatorRouteParamsPage.vue'
|
||||
import { PAGE_SIZE } from '@/constants'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// constants
|
||||
const pollInterval = CONFIG.AUTO_POLL_INTERVAL || undefined
|
||||
const pageSize = PAGE_SIZE
|
||||
|
||||
// computed
|
||||
const currentPage = ref(Number(route.params.page) || 1)
|
||||
|
||||
const { result, loading } = useQuery(
|
||||
listAllContributions,
|
||||
() => ({
|
||||
pagination: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize,
|
||||
order: 'DESC',
|
||||
},
|
||||
}),
|
||||
{
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval,
|
||||
},
|
||||
)
|
||||
|
||||
const contributionCount = computed(() => {
|
||||
return result.value?.listAllContributions.contributionCount || 0
|
||||
})
|
||||
const items = computed(() => {
|
||||
return [...(result.value?.listAllContributions.contributionList || [])]
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,217 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ContributionListItem from './ContributionListItem'
|
||||
import { BRow, BCol, BCollapse, BButton, BForm, BTextArea, BFormTextarea } from 'bootstrap-vue-next'
|
||||
import VariantIcon from '@/components/VariantIcon.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
short: 'Short format date',
|
||||
contribution: {
|
||||
deleted: 'Deleted contribution',
|
||||
delete: 'Delete contribution',
|
||||
confirmed: 'Confirmed contribution',
|
||||
},
|
||||
form: {
|
||||
reply: 'Reply',
|
||||
memo: 'Memo',
|
||||
},
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
moderatorChat: 'Chat',
|
||||
},
|
||||
},
|
||||
datetimeFormats: {
|
||||
en: {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useQuery: vi.fn(),
|
||||
useLazyQuery: vi.fn(() => ({
|
||||
onResult: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
load: vi.fn(),
|
||||
refetch: vi.fn(),
|
||||
})),
|
||||
useMutation: vi.fn(() => ({
|
||||
mutate: vi.fn(),
|
||||
onDone: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useAppToast: vi.fn(() => ({
|
||||
addToast: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('ContributionListItem', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$filters: {
|
||||
GDD: vi.fn((val) => val),
|
||||
},
|
||||
$t: vi.fn((key) => key),
|
||||
$d: (date, format) => date.toISOString(),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionId: 42,
|
||||
contributionStatus: 'PENDING',
|
||||
messagesCount: 2,
|
||||
id: 1,
|
||||
createdAt: '26/07/2022',
|
||||
contributionDate: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
}
|
||||
|
||||
const mountWrapper = () => {
|
||||
return mount(ContributionListItem, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
mocks,
|
||||
stubs: [
|
||||
'IBiPencil',
|
||||
'IBiTrash',
|
||||
'IBiChatDots',
|
||||
'BAvatar',
|
||||
'VariantIcon',
|
||||
'BButton',
|
||||
'IBiArrowDownCircle',
|
||||
'IBiArrowUpCircle',
|
||||
'IBiArrowUpShort',
|
||||
'ContributionMessagesListItem',
|
||||
],
|
||||
components: {
|
||||
BRow,
|
||||
BCol,
|
||||
BCollapse,
|
||||
BButton,
|
||||
BForm,
|
||||
BTextArea,
|
||||
BFormTextarea,
|
||||
},
|
||||
},
|
||||
props: propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
wrapper = mountWrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list-item', () => {
|
||||
expect(wrapper.find('div.contribution-list-item').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('contribution icon', () => {
|
||||
it('is bell-fill by default', () => {
|
||||
expect(wrapper.vm.icon).toBe('bell-fill')
|
||||
})
|
||||
|
||||
it('is x-circle when contributionStatus is DELETED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'DELETED' })
|
||||
expect(wrapper.vm.icon).toBe('trash')
|
||||
})
|
||||
|
||||
it('is check when contributionStatus is CONFIRMED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'CONFIRMED' })
|
||||
expect(wrapper.vm.icon).toBe('check')
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribution variant', () => {
|
||||
it('is primary by default', () => {
|
||||
expect(wrapper.vm.variant).toBe('primary')
|
||||
})
|
||||
|
||||
it('is danger when contributionStatus is DELETED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'DELETED' })
|
||||
expect(wrapper.vm.variant).toBe('danger')
|
||||
})
|
||||
|
||||
it('is success at when contributionStatus is CONFIRMED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'CONFIRMED' })
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
|
||||
it('is warning at when contributionStatus is IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'IN_PROGRESS' })
|
||||
expect(wrapper.vm.variant).toBe('205')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution', () => {
|
||||
describe('edit contribution', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('div.test-edit-contribution').trigger('click')
|
||||
})
|
||||
|
||||
it('emits update contribution form', () => {
|
||||
expect(wrapper.emitted('update-contribution-form')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
contributionDate: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm deletion', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, 'confirm').mockImplementation(() => true)
|
||||
wrapper.find('div.test-delete-contribution').trigger('click')
|
||||
})
|
||||
|
||||
it('emits delete contribution', () => {
|
||||
expect(wrapper.emitted('contribution-changed')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancel deletion', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(window, 'confirm').mockImplementation(() => false)
|
||||
await wrapper.find('div.test-delete-contribution').trigger('click')
|
||||
})
|
||||
|
||||
it('does not emit delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('updates status of a contribution', async () => {
|
||||
wrapper.vm.contributionStatus = 'IN_PROGRESS'
|
||||
|
||||
wrapper.vm.addContributionMessage({})
|
||||
|
||||
expect(wrapper.vm.localStatus).toBe('PENDING')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="contribution-list-item bg-white app-box-shadow gradido-border-radius pt-3 px-3">
|
||||
<BRow>
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<app-avatar
|
||||
v-if="username.username"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
color="#fff"
|
||||
class="vue3-avatar fw-bold"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<div v-if="username.username" class="me-3 fw-bold">
|
||||
{{ username.username }}
|
||||
<variant-icon :icon="icon" :variant="variant" />
|
||||
</div>
|
||||
<div class="small">
|
||||
{{ $d(new Date(contributionDate), 'short') }}
|
||||
</div>
|
||||
<div class="mt-3 fw-bold">{{ $t('contributionText') }}</div>
|
||||
<div class="mb-3 text-break word-break">{{ memo }}</div>
|
||||
<div v-if="updatedBy > 0" class="mt-2 mb-2 small">
|
||||
{{ $t('moderatorChangedMemo') }}
|
||||
</div>
|
||||
</BCol>
|
||||
<BCol cols="9" lg="3" offset="3" offset-md="0" offset-lg="0">
|
||||
<div class="small">
|
||||
{{ $t('creation') }} {{ $t('(') }}{{ hours }} {{ $t('h') }}{{ $t(')') }}
|
||||
</div>
|
||||
<div v-if="contributionStatus === 'DENIED'" class="fw-bold">
|
||||
<variant-icon icon="x-circle" variant="danger" />
|
||||
{{ $t('contribution.alert.denied') }}
|
||||
</div>
|
||||
<div v-if="contributionStatus === 'DELETED'" class="small">
|
||||
{{ $t('contribution.deleted') }}
|
||||
</div>
|
||||
<div v-else class="fw-bold">{{ $filters.GDD(amount) }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="1" lg="1" class="text-end align-items-center" />
|
||||
</BRow>
|
||||
<div class="pb-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
import { GDD_PER_HOUR } from '../../constants'
|
||||
import { useContributionStatus } from '@/composables/useContributionStatus'
|
||||
|
||||
const props = defineProps({
|
||||
amount: {
|
||||
type: String,
|
||||
},
|
||||
memo: {
|
||||
type: String,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
contributionDate: {
|
||||
type: String,
|
||||
},
|
||||
updatedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
contributionStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const { getVariant, getIcon } = useContributionStatus()
|
||||
const variant = computed(() => getVariant(props.contributionStatus))
|
||||
const icon = computed(() => getIcon(props.contributionStatus))
|
||||
|
||||
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)))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.b-avatar-custom > svg) {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,41 @@
|
||||
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'
|
||||
import { BRow, BCol, BCollapse, BButton, BForm, BTextArea, BFormTextarea } from 'bootstrap-vue-next'
|
||||
import VariantIcon from '@/components/VariantIcon.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
short: 'Short format date',
|
||||
contribution: {
|
||||
deleted: 'Deleted contribution',
|
||||
delete: 'Delete contribution',
|
||||
confirmed: 'Confirmed contribution',
|
||||
},
|
||||
form: {
|
||||
reply: 'Reply',
|
||||
memo: 'Memo',
|
||||
},
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
moderatorChat: 'Chat',
|
||||
},
|
||||
},
|
||||
datetimeFormats: {
|
||||
en: {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
@ -26,6 +56,8 @@ vi.mock('@vue/apollo-composable', () => ({
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useAppToast: vi.fn(() => ({
|
||||
addToast: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
@ -36,11 +68,13 @@ describe('ContributionListItem', () => {
|
||||
$filters: {
|
||||
GDD: vi.fn((val) => val),
|
||||
},
|
||||
$t: vi.fn((key) => key),
|
||||
$d: (date, format) => date.toISOString(),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionId: 42,
|
||||
status: 'PENDING',
|
||||
contributionStatus: 'PENDING',
|
||||
messagesCount: 2,
|
||||
id: 1,
|
||||
createdAt: '26/07/2022',
|
||||
@ -54,6 +88,27 @@ describe('ContributionListItem', () => {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
mocks,
|
||||
stubs: [
|
||||
'IBiPencil',
|
||||
'IBiTrash',
|
||||
'IBiChatDots',
|
||||
'BAvatar',
|
||||
'VariantIcon',
|
||||
'BButton',
|
||||
'IBiArrowDownCircle',
|
||||
'IBiArrowUpCircle',
|
||||
'IBiArrowUpShort',
|
||||
'ContributionMessagesListItem',
|
||||
],
|
||||
components: {
|
||||
BRow,
|
||||
BCol,
|
||||
BCollapse,
|
||||
BButton,
|
||||
BForm,
|
||||
BTextArea,
|
||||
BFormTextarea,
|
||||
},
|
||||
},
|
||||
props: propsData,
|
||||
})
|
||||
@ -74,13 +129,13 @@ describe('ContributionListItem', () => {
|
||||
expect(wrapper.vm.icon).toBe('bell-fill')
|
||||
})
|
||||
|
||||
it('is x-circle when deletedAt is present', async () => {
|
||||
await wrapper.setProps({ deletedAt: new Date().toISOString() })
|
||||
it('is x-circle when contributionStatus is DELETED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'DELETED' })
|
||||
expect(wrapper.vm.icon).toBe('trash')
|
||||
})
|
||||
|
||||
it('is check when confirmedAt is present', async () => {
|
||||
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
|
||||
it('is check when contributionStatus is CONFIRMED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'CONFIRMED' })
|
||||
expect(wrapper.vm.icon).toBe('check')
|
||||
})
|
||||
})
|
||||
@ -90,18 +145,18 @@ describe('ContributionListItem', () => {
|
||||
expect(wrapper.vm.variant).toBe('primary')
|
||||
})
|
||||
|
||||
it('is danger when deletedAt is present', async () => {
|
||||
await wrapper.setProps({ deletedAt: new Date().toISOString() })
|
||||
it('is danger when contributionStatus is DELETED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'DELETED' })
|
||||
expect(wrapper.vm.variant).toBe('danger')
|
||||
})
|
||||
|
||||
it('is success at when confirmedAt is present', async () => {
|
||||
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
|
||||
it('is success at when contributionStatus is CONFIRMED', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'CONFIRMED' })
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
|
||||
it('is warning at when status is IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({ status: 'IN_PROGRESS' })
|
||||
it('is warning at when contributionStatus is IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({ contributionStatus: 'IN_PROGRESS' })
|
||||
expect(wrapper.vm.variant).toBe('205')
|
||||
})
|
||||
})
|
||||
@ -133,7 +188,7 @@ describe('ContributionListItem', () => {
|
||||
})
|
||||
|
||||
it('emits delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 1 }]])
|
||||
expect(wrapper.emitted('contribution-changed')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@ -147,26 +202,15 @@ describe('ContributionListItem', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateStatus', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.vm.updateStatus()
|
||||
})
|
||||
|
||||
it('emit update-status', () => {
|
||||
expect(wrapper.emitted('update-status')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getListContributionMessages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'ContributionMessagesList' })
|
||||
.vm.$emit('get-list-contribution-messages')
|
||||
})
|
||||
it('emits close-all-open-collapse', () => {
|
||||
expect(wrapper.emitted('close-all-open-collapse')).toBeTruthy()
|
||||
describe('updateStatus', () => {
|
||||
it('updates status of a contribution', async () => {
|
||||
wrapper.vm.contributionStatus = 'IN_PROGRESS'
|
||||
|
||||
wrapper.vm.addContributionMessage({})
|
||||
|
||||
expect(wrapper.vm.localStatus).toBe('PENDING')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,34 +2,15 @@
|
||||
<div>
|
||||
<div
|
||||
class="contribution-list-item bg-white app-box-shadow gradido-border-radius pt-3 px-3"
|
||||
:class="status === 'IN_PROGRESS' && !allContribution ? 'pulse border border-205' : ''"
|
||||
:class="localStatus === 'IN_PROGRESS' ? 'pulse border border-205' : ''"
|
||||
>
|
||||
<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"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
color="#fff"
|
||||
class="vue3-avatar fw-bold"
|
||||
/>
|
||||
<BAvatar v-else rounded="lg" :variant="variant" size="4.55em">
|
||||
<BAvatar rounded="lg" :variant="variant" size="4.55em">
|
||||
<variant-icon :icon="icon" variant="white" />
|
||||
</BAvatar>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<div v-if="firstName" class="me-3 fw-bold">
|
||||
{{ firstName }} {{ lastName }}
|
||||
<variant-icon :icon="icon" :variant="variant" />
|
||||
</div>
|
||||
<div class="small">
|
||||
{{ $d(new Date(contributionDate), 'short') }}
|
||||
</div>
|
||||
@ -39,9 +20,9 @@
|
||||
{{ $t('moderatorChangedMemo') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="status === 'IN_PROGRESS' && !allContribution"
|
||||
v-if="localStatus === 'IN_PROGRESS'"
|
||||
class="text-danger pointer hover-font-bold"
|
||||
@click="visible = !visible"
|
||||
@click="emit('toggle-messages-visible')"
|
||||
>
|
||||
{{ $t('contribution.alert.answerQuestion') }}
|
||||
</div>
|
||||
@ -50,30 +31,23 @@
|
||||
<div class="small">
|
||||
{{ $t('creation') }} {{ $t('(') }}{{ hours }} {{ $t('h') }}{{ $t(')') }}
|
||||
</div>
|
||||
<div v-if="status === 'DENIED' && allContribution" class="fw-bold">
|
||||
<variant-icon icon="x-circle" variant="danger" />
|
||||
{{ $t('contribution.alert.denied') }}
|
||||
</div>
|
||||
<div v-if="status === 'DELETED'" class="small">
|
||||
<div v-if="localStatus === 'DELETED'" class="small">
|
||||
{{ $t('contribution.deleted') }}
|
||||
</div>
|
||||
<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>
|
||||
<BRow
|
||||
v-if="(!['CONFIRMED', 'DELETED'].includes(status) && !allContribution) || messagesCount > 0"
|
||||
class="p-2"
|
||||
>
|
||||
<BRow v-if="!['CONFIRMED', 'DELETED'].includes(localStatus) || messagesCount > 0" class="p-2">
|
||||
<BCol cols="3" class="me-auto text-center">
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(status) && !allContribution && !moderatorId"
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(localStatus) && !moderatorId"
|
||||
class="test-delete-contribution pointer me-3"
|
||||
@click="deleteContribution({ id })"
|
||||
@click="processDeleteContribution({ id })"
|
||||
>
|
||||
<IBiTrash />
|
||||
|
||||
@ -82,14 +56,14 @@
|
||||
</BCol>
|
||||
<BCol cols="3" class="text-center">
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(status) && !allContribution && !moderatorId"
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(localStatus) && !moderatorId"
|
||||
class="test-edit-contribution pointer me-3"
|
||||
@click="
|
||||
$emit('update-contribution-form', {
|
||||
id: id,
|
||||
contributionDate: contributionDate,
|
||||
memo: memo,
|
||||
amount: amount,
|
||||
id,
|
||||
contributionDate,
|
||||
memo,
|
||||
amount,
|
||||
})
|
||||
"
|
||||
>
|
||||
@ -98,22 +72,27 @@
|
||||
</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>
|
||||
<div v-else class="pb-3"></div>
|
||||
<BCollapse :id="collapseId" :model-value="visible" class="mt-2">
|
||||
<BCollapse :model-value="messagesVisible">
|
||||
<contribution-messages-list
|
||||
:messages="messagesGet"
|
||||
:status="status"
|
||||
:contribution-id="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-status="updateStatus"
|
||||
v-if="messagesCount > 0"
|
||||
:messages="localMessages"
|
||||
:status="localStatus"
|
||||
:contribution-id="id"
|
||||
@close-messages-list="emit('toggle-messages-visible')"
|
||||
@add-contribution-message="addContributionMessage"
|
||||
/>
|
||||
</BCollapse>
|
||||
<div class="pb-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -122,12 +101,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 AppAvatar from '@/components/AppAvatar.vue'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { GDD_PER_HOUR } from '../../constants'
|
||||
import { deleteContribution } from '@/graphql/contributions.graphql'
|
||||
import { useContributionStatus } from '@/composables/useContributionStatus'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@ -139,45 +118,19 @@ const props = defineProps({
|
||||
memo: {
|
||||
type: String,
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
messages: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: () => [],
|
||||
},
|
||||
contributionDate: {
|
||||
type: String,
|
||||
},
|
||||
deletedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
confirmedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
confirmedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
deniedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
deniedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
updatedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
status: {
|
||||
contributionStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
@ -186,112 +139,78 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
contributionId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
allContribution: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
moderatorId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
messagesVisible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const { t } = useI18n()
|
||||
const { getVariant, getIcon } = useContributionStatus()
|
||||
|
||||
const messagesGet = ref([])
|
||||
const visible = ref(false)
|
||||
const { mutate: deleteContributionMutation } = useMutation(deleteContribution)
|
||||
|
||||
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'
|
||||
})
|
||||
const localMessages = ref([])
|
||||
const localStatus = ref(props.contributionStatus)
|
||||
const variant = computed(() => getVariant(props.contributionStatus))
|
||||
const icon = computed(() => getIcon(props.contributionStatus))
|
||||
|
||||
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'
|
||||
})
|
||||
// if parent reload messages, update local messages copy
|
||||
watch(
|
||||
() => props.messages,
|
||||
() => {
|
||||
if (props.messages?.length > 0) {
|
||||
localMessages.value = [...props.messages]
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const collapseId = computed(() => 'collapse' + String(props.id))
|
||||
|
||||
const username = computed(() => ({
|
||||
username: `${props.firstName} ${props.lastName}`,
|
||||
initials: `${props.firstName[0]}${props.lastName[0]}`,
|
||||
}))
|
||||
watch(
|
||||
() => props.contributionStatus,
|
||||
() => {
|
||||
localStatus.value = props.contributionStatus
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
onResult((resultValue) => {
|
||||
if (resultValue.data) {
|
||||
messagesGet.value.length = 0
|
||||
resultValue.data.listContributionMessages.messages.forEach((message) => {
|
||||
messagesGet.value.push(message)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onError((err) => {
|
||||
toastError(err.message)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
getListContributionMessages()
|
||||
try {
|
||||
await deleteContributionMutation(item)
|
||||
toastSuccess(t('contribution.deleted'))
|
||||
localStatus.value = 'DELETED'
|
||||
emit('contribution-changed')
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function updateStatus(id) {
|
||||
emit('update-status', id)
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['delete-contribution', 'close-all-open-collapse', 'update-status'])
|
||||
function addContributionMessage(message) {
|
||||
localMessages.value.push(message)
|
||||
localStatus.value = 'PENDING'
|
||||
emit('contribution-changed')
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
54
frontend/src/components/PaginatorRouteParamsPage.vue
Normal file
54
frontend/src/components/PaginatorRouteParamsPage.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<BPagination
|
||||
v-if="isPaginationVisible"
|
||||
:model-value="currentPage"
|
||||
class="mt-3"
|
||||
pills
|
||||
size="lg"
|
||||
:per-page="pageSize"
|
||||
:total-rows="totalCount"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
@update:model-value="updatePage"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import CONFIG from '@/config'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: CONFIG.PAGE_SIZE,
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:model-value'])
|
||||
|
||||
const isPaginationVisible = computed(() => {
|
||||
return (props.totalCount > props.pageSize || props.modelValue > 1) && !props.loading
|
||||
})
|
||||
const currentPage = computed(() => Number(route.params.page) || props.modelValue)
|
||||
const updatePage = (page) => {
|
||||
router.push({ params: { page } })
|
||||
emit('update:model-value', page)
|
||||
}
|
||||
</script>
|
||||
@ -4,8 +4,8 @@
|
||||
class="nav-community-btn-wrapper bg-209 rounded-26 d-flex bd-highlight mx-xl-6 mx-lg-5 shadow justify-content-between"
|
||||
>
|
||||
<BButton
|
||||
to="contribute"
|
||||
active-class="btn-active svg-icon-active"
|
||||
:to="{ path: '/community/contribute' }"
|
||||
:class="stateClasses('/community/contribute')"
|
||||
block
|
||||
variant="link"
|
||||
class="nav-community__btn"
|
||||
@ -14,8 +14,8 @@
|
||||
{{ $t('community.submitContribution') }}
|
||||
</BButton>
|
||||
<BButton
|
||||
to="contributions"
|
||||
active-class="btn-active svg-icon-active"
|
||||
:to="{ path: '/community/contributions' }"
|
||||
:class="stateClasses('/community/contributions')"
|
||||
block
|
||||
variant="link"
|
||||
class="nav-community__btn"
|
||||
@ -24,8 +24,8 @@
|
||||
{{ $t('community.myContributions') }}
|
||||
</BButton>
|
||||
<BButton
|
||||
to="community"
|
||||
active-class="btn-active svg-icon-active"
|
||||
:to="{ path: '/community/community' }"
|
||||
:class="stateClasses('/community/community')"
|
||||
block
|
||||
variant="link"
|
||||
class="nav-community__btn"
|
||||
@ -39,6 +39,15 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavCommunity',
|
||||
|
||||
methods: {
|
||||
stateClasses(path) {
|
||||
if (this.$route.path.includes(path)) {
|
||||
return 'router-link-active router-link-exact-active'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
22
frontend/src/composables/useContributionStatus.js
Normal file
22
frontend/src/composables/useContributionStatus.js
Normal file
@ -0,0 +1,22 @@
|
||||
export const useContributionStatus = () => {
|
||||
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 getVariant = (status) => {
|
||||
return (statusMapping[status] || statusMapping.default).variant
|
||||
}
|
||||
|
||||
const getIcon = (status) => {
|
||||
return (statusMapping[status] || statusMapping.default).icon
|
||||
}
|
||||
|
||||
return {
|
||||
getVariant,
|
||||
getIcon,
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,7 @@ if (process.env.FRONTEND_HOSTING === 'nodejs') {
|
||||
const features = {
|
||||
GMS_ACTIVE: process.env.GMS_ACTIVE === 'true',
|
||||
HUMHUB_ACTIVE: process.env.HUMHUB_ACTIVE === 'true',
|
||||
AUTO_POLL_INTERVAL: Number.parseInt(process.env.AUTO_POLL_INTERVAL ?? 0),
|
||||
}
|
||||
|
||||
const environment = {
|
||||
|
||||
@ -42,6 +42,13 @@ module.exports = Joi.object({
|
||||
.default('http://0.0.0.0/admin/authenticate?token=')
|
||||
.required(),
|
||||
|
||||
AUTO_POLL_INTERVAL: Joi.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(600000)
|
||||
.description('Auto Polling for new data in ms. 0 = disabled = default. Experimental!')
|
||||
.default(0),
|
||||
|
||||
COMMUNITY_REGISTER_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.description('URL for Register a new Account in frontend.')
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export const GDD_PER_HOUR = 20
|
||||
export const PAGE_SIZE = 25
|
||||
|
||||
116
frontend/src/graphql/contributions.graphql
Normal file
116
frontend/src/graphql/contributions.graphql
Normal file
@ -0,0 +1,116 @@
|
||||
#import './user.graphql'
|
||||
|
||||
fragment unconfirmedContributionFields on Contribution {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
contributionDate
|
||||
contributionStatus
|
||||
messagesCount
|
||||
}
|
||||
|
||||
fragment contributionFields on Contribution {
|
||||
...unconfirmedContributionFields
|
||||
createdAt
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
deniedAt
|
||||
deniedBy
|
||||
updatedBy
|
||||
updatedAt
|
||||
}
|
||||
|
||||
fragment contributionMessageFields on ContributionMessage {
|
||||
id
|
||||
message
|
||||
createdAt
|
||||
updatedAt
|
||||
type
|
||||
userFirstName
|
||||
userLastName
|
||||
userId
|
||||
}
|
||||
|
||||
query listContributions ($pagination: Paginated!) {
|
||||
listContributions(pagination: $pagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
contributionDate
|
||||
contributionStatus
|
||||
messagesCount
|
||||
messages {
|
||||
...contributionMessageFields
|
||||
}
|
||||
updatedBy
|
||||
moderatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query listAllContributions ($pagination: Paginated!) {
|
||||
listAllContributions(pagination: $pagination) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
amount
|
||||
memo
|
||||
user {
|
||||
...userFields
|
||||
}
|
||||
contributionDate
|
||||
updatedBy
|
||||
contributionStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query countContributionsInProgress {
|
||||
countContributionsInProgress
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -224,66 +224,6 @@ export const listContributionLinks = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const listContributions = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$statusFilter: [ContributionStatus!]
|
||||
) {
|
||||
listContributions(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
statusFilter: $statusFilter
|
||||
) {
|
||||
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 {
|
||||
@ -328,16 +268,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
|
||||
}
|
||||
@ -91,7 +91,8 @@
|
||||
"lastContribution": "Letzte Beiträge",
|
||||
"noContributions": {
|
||||
"allContributions": "Es wurden noch keine Beiträge eingereicht.",
|
||||
"myContributions": "Du hast noch keine Beiträge eingereicht."
|
||||
"myContributions": "Du hast noch keine Beiträge eingereicht.",
|
||||
"emptyPage": "Diese Seite ist leer."
|
||||
},
|
||||
"noOpenCreation": {
|
||||
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
|
||||
@ -323,7 +324,7 @@
|
||||
"settings": "Einstellungen",
|
||||
"circles": "Gradido Kreise",
|
||||
"transactions": "Deine Transaktionen",
|
||||
"usersearch": "Geografische Mitgliedssuche (experimentell)"
|
||||
"usersearch": "Geografische Mitgliedssuche (beta)"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "GDD versenden",
|
||||
|
||||
@ -91,7 +91,8 @@
|
||||
"lastContribution": "Last Contributions",
|
||||
"noContributions": {
|
||||
"allContributions": "No contributions have been submitted yet.",
|
||||
"myContributions": "You have not submitted any entries yet."
|
||||
"myContributions": "You have not submitted any entries yet.",
|
||||
"emptyPage": "This page is empty."
|
||||
},
|
||||
"noOpenCreation": {
|
||||
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
|
||||
@ -323,7 +324,7 @@
|
||||
"settings": "Settings",
|
||||
"transactions": "Your transactions",
|
||||
"circles": "Gradido Circles",
|
||||
"usersearch": "Geographic member search (experimental)"
|
||||
"usersearch": "Geographic member search (beta)"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "Send GDD",
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
"settings": "Soporte",
|
||||
"transactions": "Tu Transacciones",
|
||||
"circles": "Círculos Gradido",
|
||||
"usersearch": "Búsqueda geográfica de miembros (experimental)"
|
||||
"usersearch": "Búsqueda geográfica de miembros (beta)"
|
||||
},
|
||||
"qrCode": "Código QR",
|
||||
"send_gdd": "Enviar GDD",
|
||||
|
||||
@ -292,7 +292,7 @@
|
||||
"settings": "Configuration",
|
||||
"transactions": "Vos transactions",
|
||||
"circles": "Cercles Gradido",
|
||||
"usersearch": "Recherche géographique de membres (expérimentale)"
|
||||
"usersearch": "Recherche géographique de membres (bèta)"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "Envoyer GDD",
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
"settings": "Settings",
|
||||
"transactions": "Your transactions",
|
||||
"circles": "Gradido Kringen",
|
||||
"usersearch": "Geografisch leden zoeken (experimenteel)"
|
||||
"usersearch": "Geografisch leden zoeken (bèta)"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "GDD sturen",
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
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 { useMutation, useQuery } from '@vue/apollo-composable'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { BTab, BTabs } from 'bootstrap-vue-next'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Community from './Community'
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock('vue-router', () => ({
|
||||
@ -17,13 +14,10 @@ vi.mock('vue-router', () => ({
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useQuery: vi.fn(),
|
||||
useMutation: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useAppToast: vi.fn(() => ({
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
toastInfo: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
@ -34,36 +28,16 @@ vi.mock('vue-i18n', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('vee-validate', () => ({
|
||||
useField: vi.fn(() => ({
|
||||
value: ref(''),
|
||||
errorMessage: ref(''),
|
||||
handleChange: vi.fn(),
|
||||
meta: reactive({
|
||||
valid: true,
|
||||
touched: false,
|
||||
dirty: false,
|
||||
}),
|
||||
})),
|
||||
useForm: vi.fn(() => ({
|
||||
handleSubmit: vi.fn(),
|
||||
errors: reactive({}),
|
||||
resetForm: vi.fn(),
|
||||
})),
|
||||
defineRule: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/Contributions/OpenCreationsAmount', () => ({
|
||||
vi.mock('@/components/Contributions/ContributionEdit', () => ({
|
||||
default: {
|
||||
name: 'OpenCreationsAmount',
|
||||
name: 'ContributionEdit',
|
||||
template: '<div></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/Contributions/ContributionForm', () => ({
|
||||
vi.mock('@/components/Contributions/ContributionCreate', () => ({
|
||||
default: {
|
||||
name: 'ContributionForm',
|
||||
name: 'ContributionCreate',
|
||||
template: '<div></div>',
|
||||
},
|
||||
}))
|
||||
@ -75,57 +49,41 @@ vi.mock('@/components/Contributions/ContributionList', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/Contributions/ContributionListAll', () => ({
|
||||
default: {
|
||||
name: 'ContributionListAll',
|
||||
template: '<div></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Community', () => {
|
||||
let wrapper
|
||||
let mockRouter
|
||||
let mockToast
|
||||
|
||||
const mockOpenCreationsQuery = vi.fn()
|
||||
const mockListContributionsQuery = vi.fn()
|
||||
const mockListAllContributionsQuery = vi.fn()
|
||||
const mockCreateContributionMutation = vi.fn()
|
||||
const mockUpdateContributionMutation = vi.fn()
|
||||
const mockDeleteContributionMutation = vi.fn()
|
||||
const mockCountContributionsInProgress = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouter = { push: vi.fn() }
|
||||
vi.mocked(useRouter).mockReturnValue(mockRouter)
|
||||
|
||||
mockToast = {
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
toastInfo: vi.fn(),
|
||||
}
|
||||
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() }
|
||||
})
|
||||
|
||||
vi.mocked(useMutation).mockImplementation((mutation) => {
|
||||
if (mutation === createContribution) return { mutate: mockCreateContributionMutation }
|
||||
if (mutation === updateContribution) return { mutate: mockUpdateContributionMutation }
|
||||
if (mutation === deleteContribution) return { mutate: mockDeleteContributionMutation }
|
||||
})
|
||||
|
||||
const { defineRule } = require('vee-validate')
|
||||
defineRule('required', (value) => !!value)
|
||||
vi.mocked(useQuery).mockImplementation((query) => ({
|
||||
onResult: mockCountContributionsInProgress,
|
||||
}))
|
||||
|
||||
wrapper = mount(Community, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key, // Mock $t function
|
||||
$d: (date) => date.toISOString(), // Mock $d function if needed
|
||||
},
|
||||
components: {
|
||||
BTabs,
|
||||
},
|
||||
stub: {
|
||||
BTab: true,
|
||||
BTab,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -138,21 +96,13 @@ 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()
|
||||
})
|
||||
})
|
||||
|
||||
describe('tabs', () => {
|
||||
it('has three tabs', () => {
|
||||
expect(wrapper.findAll('.tabs')).toHaveLength(1)
|
||||
expect(wrapper.findAll('btab')).toHaveLength(3)
|
||||
expect(wrapper.findAllComponents(BTab)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.skip('updates tab index when route changes', async () => {
|
||||
@ -162,118 +112,23 @@ describe('Community', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSaveContribution', () => {
|
||||
it('calls createContributionMutation and shows success toast on success', async () => {
|
||||
const contributionData = { date: '2023-09-12', memo: 'Test contribution', amount: '100' }
|
||||
mockCreateContributionMutation.mockResolvedValue({ data: { createContribution: true } })
|
||||
|
||||
await wrapper.vm.handleSaveContribution(contributionData)
|
||||
|
||||
expect(mockCreateContributionMutation).toHaveBeenCalledWith({
|
||||
creationDate: contributionData.date,
|
||||
memo: contributionData.memo,
|
||||
amount: contributionData.amount,
|
||||
})
|
||||
expect(mockToast.toastSuccess).toHaveBeenCalledWith('contribution.submitted')
|
||||
})
|
||||
|
||||
it('shows error toast on failure', async () => {
|
||||
const contributionData = { date: '2023-09-12', memo: 'Test contribution', amount: '100' }
|
||||
mockCreateContributionMutation.mockRejectedValue(new Error('Create Contribution failed!'))
|
||||
|
||||
await wrapper.vm.handleSaveContribution(contributionData)
|
||||
|
||||
expect(mockToast.toastError).toHaveBeenCalledWith('Create Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleUpdateContribution', () => {
|
||||
it('calls updateContributionMutation and shows success toast on success', async () => {
|
||||
const contributionData = {
|
||||
id: 1,
|
||||
date: '2023-09-12',
|
||||
memo: 'Updated contribution',
|
||||
amount: '200',
|
||||
}
|
||||
mockUpdateContributionMutation.mockResolvedValue({ data: { updateContribution: true } })
|
||||
|
||||
await wrapper.vm.handleUpdateContribution(contributionData)
|
||||
|
||||
expect(mockUpdateContributionMutation).toHaveBeenCalledWith({
|
||||
contributionId: contributionData.id,
|
||||
creationDate: contributionData.date,
|
||||
memo: contributionData.memo,
|
||||
amount: contributionData.amount,
|
||||
})
|
||||
expect(mockToast.toastSuccess).toHaveBeenCalledWith('contribution.updated')
|
||||
})
|
||||
|
||||
it('shows error toast on failure', async () => {
|
||||
const contributionData = {
|
||||
id: 1,
|
||||
date: '2023-09-12',
|
||||
memo: 'Updated contribution',
|
||||
amount: '200',
|
||||
}
|
||||
mockUpdateContributionMutation.mockRejectedValue(new Error('Update Contribution failed!'))
|
||||
|
||||
await wrapper.vm.handleUpdateContribution(contributionData)
|
||||
|
||||
expect(mockToast.toastError).toHaveBeenCalledWith('Update Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleDeleteContribution', () => {
|
||||
it('calls deleteContributionMutation and shows success toast on success', async () => {
|
||||
const contributionData = { id: 1 }
|
||||
mockDeleteContributionMutation.mockResolvedValue({ data: { deleteContribution: true } })
|
||||
|
||||
await wrapper.vm.handleDeleteContribution(contributionData)
|
||||
|
||||
expect(mockDeleteContributionMutation).toHaveBeenCalledWith({ id: contributionData.id })
|
||||
expect(mockToast.toastSuccess).toHaveBeenCalledWith('contribution.deleted')
|
||||
})
|
||||
|
||||
it('shows error toast on failure', async () => {
|
||||
const contributionData = { id: 1 }
|
||||
mockDeleteContributionMutation.mockRejectedValue(new Error('Delete Contribution failed!'))
|
||||
|
||||
await wrapper.vm.handleDeleteContribution(contributionData)
|
||||
|
||||
expect(mockToast.toastError).toHaveBeenCalledWith('Delete Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleUpdateContributionForm', () => {
|
||||
it('updates form data and changes tab', () => {
|
||||
const contributionData = {
|
||||
id: 2,
|
||||
contributionDate: '2023-09-12',
|
||||
memo: 'Test contribution',
|
||||
amount: '300',
|
||||
item: { id: 2, contributionDate: '2023-09-12', memo: 'Test contribution', amount: '300' },
|
||||
page: 2,
|
||||
}
|
||||
|
||||
wrapper.vm.handleUpdateContributionForm(contributionData)
|
||||
|
||||
expect(wrapper.vm.form).toEqual({
|
||||
expect(wrapper.vm.itemData).toEqual({
|
||||
id: 2,
|
||||
date: '2023-09-12',
|
||||
contributionDate: '2023-09-12',
|
||||
memo: 'Test contribution',
|
||||
amount: '300',
|
||||
hours: 15, // 300 / 20
|
||||
})
|
||||
expect(wrapper.vm.tabIndex).toBe(0)
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,55 +2,19 @@
|
||||
<div class="community-page">
|
||||
<div>
|
||||
<BTabs :model-value="tabIndex" no-nav-style borderless align="center">
|
||||
<BTab no-body>
|
||||
<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"
|
||||
<BTab no-body lazy>
|
||||
<contribution-edit
|
||||
v-if="itemData"
|
||||
v-model="itemData"
|
||||
@contribution-updated="handleContributionUpdated"
|
||||
/>
|
||||
<contribution-create v-else />
|
||||
</BTab>
|
||||
<BTab no-body>
|
||||
<div v-if="items.length === 0">
|
||||
{{ $t('contribution.noContributions.myContributions') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<contribution-list
|
||||
:items="items"
|
||||
:contribution-count="contributionCount"
|
||||
:show-pagination="true"
|
||||
:page-size="pageSize"
|
||||
@close-all-open-collapse="closeAllOpenCollapse"
|
||||
@update-list-contributions="handleUpdateListContributions"
|
||||
@update-contribution-form="handleUpdateContributionForm"
|
||||
@delete-contribution="handleDeleteContribution"
|
||||
@update-status="updateStatus"
|
||||
/>
|
||||
</div>
|
||||
<BTab no-body lazy>
|
||||
<contribution-list @update-contribution-form="handleUpdateContributionForm" />
|
||||
</BTab>
|
||||
<BTab no-body>
|
||||
<div v-if="itemsAll.length === 0">
|
||||
{{ $t('contribution.noContributions.allContributions') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<contribution-list
|
||||
:items="itemsAll"
|
||||
:contribution-count="contributionCountAll"
|
||||
:show-pagination="true"
|
||||
:page-size="pageSizeAll"
|
||||
:all-contribution="true"
|
||||
@update-list-contributions="handleUpdateListAllContributions"
|
||||
@update-contribution-form="handleUpdateContributionForm"
|
||||
/>
|
||||
</div>
|
||||
<BTab no-body lazy>
|
||||
<contribution-list-all />
|
||||
</BTab>
|
||||
</BTabs>
|
||||
</div>
|
||||
@ -58,123 +22,41 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import OpenCreationsAmount from '@/components/Contributions/OpenCreationsAmount'
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
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 { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||
import ContributionListAll from '@/components/Contributions/ContributionListAll'
|
||||
import { countContributionsInProgress } from '@/graphql/contributions.graphql'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { GDD_PER_HOUR } from '../constants'
|
||||
|
||||
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
|
||||
|
||||
const emit = defineEmits(['update-transactions'])
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const { toastError, toastSuccess, toastInfo } = useAppToast()
|
||||
const { 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 itemData = ref(null)
|
||||
const editContributionPage = ref(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,
|
||||
() => ({}),
|
||||
const { onResult: handleInProgressContributionFound } = useQuery(
|
||||
countContributionsInProgress,
|
||||
{},
|
||||
{
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
)
|
||||
const { onResult: onListAllContributionsResult, refetch: refetchAllContributions } = useQuery(
|
||||
listAllContributions,
|
||||
() => ({
|
||||
currentPage: currentPageAll.value,
|
||||
pageSize: pageSizeAll.value,
|
||||
}),
|
||||
{ fetchPolicy: 'no-cache' },
|
||||
)
|
||||
const { onResult: onListContributionsResult, refetch: refetchContributions } = useQuery(
|
||||
listContributions,
|
||||
() => ({
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
}),
|
||||
{ fetchPolicy: 'network-only' },
|
||||
)
|
||||
|
||||
const { mutate: createContributionMutation } = useMutation(createContribution)
|
||||
const { mutate: updateContributionMutation } = useMutation(updateContribution)
|
||||
const { mutate: deleteContributionMutation } = useMutation(deleteContribution)
|
||||
|
||||
onOpenCreationsResult(({ data }) => {
|
||||
// jump to my contributions if in progress contribution found
|
||||
handleInProgressContributionFound(({ data }) => {
|
||||
if (data) {
|
||||
openCreationsData.value = data.openCreations
|
||||
}
|
||||
})
|
||||
|
||||
onListAllContributionsResult(({ data }) => {
|
||||
if (data) {
|
||||
contributionCountAll.value = data.listAllContributions.contributionCount
|
||||
itemsAll.value.length = 0
|
||||
data.listAllContributions.contributionList.forEach((entry) => {
|
||||
itemsAll.value.push(entry)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onListContributionsResult(({ data }) => {
|
||||
if (data) {
|
||||
contributionCount.value = data.listContributions.contributionCount
|
||||
items.value.length = 0
|
||||
data.listContributions.contributionList.forEach((entry) => {
|
||||
items.value.push({ ...entry })
|
||||
})
|
||||
if (items.value.find((item) => item.status === 'IN_PROGRESS')) {
|
||||
if (data.countContributionsInProgress > 0) {
|
||||
tabIndex.value = 1
|
||||
if (route.params.tab !== 'contributions') {
|
||||
router.push({ params: { tab: 'contributions' } })
|
||||
@ -187,96 +69,23 @@ onListContributionsResult(({ data }) => {
|
||||
const updateTabIndex = () => {
|
||||
const index = COMMUNITY_TABS.indexOf(route.params.tab)
|
||||
tabIndex.value = index > -1 ? index : 0
|
||||
closeAllOpenCollapse()
|
||||
}
|
||||
|
||||
const closeAllOpenCollapse = () => {
|
||||
document.querySelectorAll('.collapse.show').forEach((el) => {
|
||||
el.classList.remove('show')
|
||||
// after a edit contribution was saved, jump to contributions tab
|
||||
function handleContributionUpdated() {
|
||||
const contributionItemId = itemData.value.id
|
||||
itemData.value = null
|
||||
tabIndex.value = 1
|
||||
router.push({
|
||||
params: { tab: 'contributions', page: editContributionPage.value },
|
||||
hash: `#contributionListItem-${contributionItemId}`,
|
||||
})
|
||||
}
|
||||
|
||||
const refetchData = () => {
|
||||
refetchAllContributions()
|
||||
refetchContributions()
|
||||
refetchOpenCreations()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateListAllContributions = (pagination) => {
|
||||
currentPageAll.value = pagination.currentPage
|
||||
pageSizeAll.value = pagination.pageSize
|
||||
refetchAllContributions({
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateListContributions = (pagination) => {
|
||||
currentPage.value = pagination.currentPage
|
||||
pageSize.value = pagination.pageSize
|
||||
refetchContributions()
|
||||
}
|
||||
|
||||
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
|
||||
// if user clicks on edit contribution in contributions tab, jump to contribute tab and populate form with contribution data
|
||||
const handleUpdateContributionForm = (data) => {
|
||||
itemData.value = data.item
|
||||
editContributionPage.value = data.page
|
||||
tabIndex.value = 0
|
||||
router.push({ params: { tab: 'contribute' } })
|
||||
}
|
||||
|
||||
const updateStatus = (id) => {
|
||||
const item = items.value.find((item) => item.id === id)
|
||||
if (item) {
|
||||
item.status = 'PENDING'
|
||||
}
|
||||
router.push({ params: { tab: 'contribute', page: undefined } })
|
||||
}
|
||||
|
||||
watch(() => route.params.tab, updateTabIndex)
|
||||
|
||||
@ -68,7 +68,7 @@ const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/community/:tab',
|
||||
path: '/community/:tab/:page?',
|
||||
component: () => import('@/pages/Community.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
|
||||
@ -6,6 +6,11 @@
|
||||
"lint": {
|
||||
"dependsOn": ["stylelint", "locales"]
|
||||
},
|
||||
"stylelint:fix": {},
|
||||
"locales:fix": {},
|
||||
"lint:fix": {
|
||||
"dependsOn": ["stylelint:fix", "locales:fix"]
|
||||
},
|
||||
"compile-scss": {},
|
||||
"watch-scss": {
|
||||
"cache": false,
|
||||
|
||||
@ -111,6 +111,7 @@ export default defineConfig(async ({ command }) => {
|
||||
autoInstall: true,
|
||||
}),
|
||||
EnvironmentPlugin({
|
||||
AUTO_POLL_INTERVAL: CONFIG.AUTO_POLL_INTERVAL,
|
||||
BUILD_COMMIT: CONFIG.BUILD_COMMIT,
|
||||
GMS_ACTIVE: CONFIG.GMS_ACTIVE,
|
||||
HUMHUB_ACTIVE: CONFIG.HUMHUB_ACTIVE,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user