Skip to content

Commit

Permalink
Financial reports and expenses list for a campaign (#1383)
Browse files Browse the repository at this point in the history
* Expenses for campaigns:
1. The users can report how spent the money from a campaign.
2. They can upload files to justify those expenses.

* Add the layout of the portal to the expenses form.

* Allow for expenses files to be downloaded and delete.
Make sure the expenses edit button and UI is only visible for the coordinator and the admin user.

* Add the expenses list to the campaigns page if there are any.
The coordinator can edit them, the rest can download them.

* Fix the build of the frontend. Remove unused elements.

* Stop using the can-edit endpoint. We can use the useCurrentUser to get the current user id and compare it to the organizer.

* Refactor the endpoints for the expenses lists. In order to make the more REST like - we are going via the campaign.

* Fix a bug - isAdmin does not seems to be working in this context.

* We should not allow any expenses to be saved without an attachment.

* Add an on error handler when uploading an expense file.
  • Loading branch information
slavcho authored Mar 29, 2023
1 parent 9186abc commit bcf03ef
Show file tree
Hide file tree
Showing 34 changed files with 1,101 additions and 33 deletions.
2 changes: 2 additions & 0 deletions public/locales/bg/campaigns.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
"status": "Статус:",
"messages": "Послания:",
"gallery": "Галерия",
"report-irregularity": "Сигнализирай за злоупотреба",
"financial-report": "Финансови отчети",
"report-campaign": "Докладвайте кампанията",
"feedback": "Обратна връзка",
"images": {
Expand Down
38 changes: 35 additions & 3 deletions public/locales/bg/expenses.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"fields-error": {
"amount-unavailable": "Недостатъчна наличност в трезора!"
},
"errors": {
"no-default-vault": "Не е избран трезор по подразбиране!"
},
"fields": {
"type": "Тип",
"status": "Статус",
Expand All @@ -14,8 +17,30 @@
"documentId": "Документ Id",
"approvedById": "Одобрен от (Id)",
"approvedBy": "Одобрен от",
"approved": "Одобрен",
"action": "Действие",
"empty": "Празно"
"empty": "Празно",
"date": "Дата",
"attached-files": "Прикачени файлове",
"files": "Файлове"
},
"field-types": {
"none": "Няма",
"internal": "Вътрешен",
"operating": "Оперативен",
"administrative": "Административен",
"medical" : "Медицински",
"services" : "Услуги",
"groceries" : "Хранителни",
"transport" : "Транспорт",
"accommodation" : "Жилищни",
"shipping" : "Доставки",
"utility" : "Комунални",
"rental" : "Наеми",
"legal" : "Юридически",
"bank" : "Банкови",
"advertising" : "Рекламни",
"other" : "Други"
},
"alerts": {
"new-row": {
Expand All @@ -35,7 +60,9 @@
"question": "Желаете ли да изтриете избраните разходи",
"error": "Възникна грешка при опит за изтриване на разходи.",
"success": "Разходите са изтрити успешно!"
}
},
"delete": "Изтрит разход",
"no-files-uploaded": "Няма прикачени файлове!"
},
"btns": {
"add": "Добави",
Expand All @@ -52,10 +79,15 @@
"info": "Информация за разход"
},
"description": "Всички разходи",
"reported": "Общо отчетени",
"uploaded-documents": "Прикачени документи",
"uploaded-files": "Прикачени файлове",
"deleteTitle": "Сигурни ли сте, че искате да изтриете файла?",
"tooltips": {
"add": "Добави",
"view": "Преглед",
"edit": "Редактирай",
"delete": "Изтрий"
"delete": "Изтрий",
"download": "Свали"
}
}
2 changes: 2 additions & 0 deletions public/locales/en/campaigns.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"profile": "Profile:",
"status": "Status:",
"gallery": "Gallery",
"report-irregularity": "Report irregularity",
"financial-report": "Financial report",
"report-campaign": "Report the campaign",
"feedback": "Feedback",
"images": {
Expand Down
19 changes: 16 additions & 3 deletions public/locales/en/expenses.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"fields-error": {
"amount-unavailable": "Insufficient stock in the vault!"
},
"errors": {
"no-default-vault": "No default vault found!"
},
"fields": {
"type": "Type",
"status": "Status",
Expand All @@ -13,7 +16,10 @@
"approvedById": "Approved by (Id)",
"approvedBy": "Approved by",
"action": "Action",
"empty": "Empty"
"empty": "Empty",
"date": "Date",
"attached-files": "Attached files",
"files": "Files"
},
"alerts": {
"new-row": {
Expand All @@ -33,7 +39,9 @@
"question": "Are you sure you want to delete multiple expenses",
"error": "Error occurred trying to delete multiple expenses.",
"success": "Expenses were deleted successfully!!"
}
},
"delete": "Deleted expense",
"no-files-uploaded": "No files uploaded"
},
"btns": {
"add": "Add",
Expand All @@ -50,10 +58,15 @@
"info": "Expense information"
},
"description": "All expenses",
"reported": "Total reported",
"uploaded-documents": "Uploaded documents",
"uploaded-files": "Uploaded files",
"deleteTitle": "Delete expense file?",
"tooltips": {
"add": "Add",
"view": "View",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"download": "Download"
}
}
22 changes: 22 additions & 0 deletions src/common/hooks/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'gql/campaigns'
import { DonationStatus } from 'gql/donations.enums'
import { apiClient } from 'service/apiClient'
import { useCurrentPerson } from 'common/util/useCurrentPerson'

// NOTE: shuffling the campaigns so that each gets its fair chance to be on top row
export const campaignsOrderQueryFunction: QueryFunction<CampaignResponse[]> = async ({
Expand Down Expand Up @@ -110,3 +111,24 @@ export function useCampaignDonationHistory(
endpoints.donation.getDonations(campaignId, DonationStatus.succeeded, pageindex, pagesize).url,
])
}

export function useCanEditCampaign(slug: string) {
const { data: session } = useSession()

const { data: userData } = useCurrentPerson()
const { data: campaignData } = useViewCampaign(slug)

if (!session || !session.user) {
return false
}

if (!userData || !campaignData || !userData.user) {
return false
}

const canEdit =
userData.user.id === campaignData.campaign.organizer?.person.id ||
session?.user?.realm_access?.roles?.includes('podkrepi-admin')

return canEdit
}
2 changes: 1 addition & 1 deletion src/common/hooks/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function useDonationSession() {
mutationFn: createCheckoutSession,
onError: () => AlertStore.show(t('common:alerts.error'), 'error'),
onSuccess: () => AlertStore.show(t('common:alerts.message-sent'), 'success'),
retry(failureCount, error) {
retry(failureCount) {
if (failureCount < 4) {
return true
}
Expand Down
38 changes: 37 additions & 1 deletion src/common/hooks/expenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { useSession } from 'next-auth/react'

import { endpoints } from 'service/apiEndpoints'
import { ExpenseResponse } from 'gql/expenses'
import { ExpenseFile, ExpenseResponse } from 'gql/expenses'
import { authQueryFnFactory } from 'service/restRequests'

export function useExpensesList() {
Expand All @@ -19,3 +19,39 @@ export function useViewExpense(id: string) {
queryFn: authQueryFnFactory(session?.accessToken),
})
}

export function useCampaignExpensesList(slug: string) {
const { data: session } = useSession()
return useQuery<ExpenseResponse[]>([endpoints.campaign.listCampaignExpenses(slug).url], {
queryFn: authQueryFnFactory(session?.accessToken),
})
}

export function useCampaignApprovedExpensesList(slug: string) {
const { data: session } = useSession()
return useQuery<ExpenseResponse[]>([endpoints.campaign.listCampaignApprovedExpenses(slug).url], {
queryFn: authQueryFnFactory(session?.accessToken),
})
}

export function useCampaignExpenseFiles(id: string) {
const { data: session } = useSession()

return useQuery<ExpenseFile[]>([endpoints.expenses.listExpenseFiles(id).url], {
queryFn: authQueryFnFactory(session?.accessToken),
})
}

export function useDeleteCampaignExpense(id: string) {
const { data: session } = useSession()
return useQuery<ExpenseFile[]>([endpoints.expenses.deleteExpense(id).url], {
queryFn: authQueryFnFactory(session?.accessToken),
})
}

export function useDeleteExpenseFile(id: string) {
const { data: session } = useSession()
return useQuery<null>([endpoints.expenses.deleteExpenseFile(id).url], {
queryFn: authQueryFnFactory(session?.accessToken),
})
}
9 changes: 9 additions & 0 deletions src/common/hooks/person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ export function usePerson(id: string) {
authQueryFnFactory<PersonResponse>(session?.accessToken),
)
}

export function useViewPersonByKeylockId(id: string) {
const { data: session } = useSession()

return useQuery(
[endpoints.person.viewPersonByKeylockId(id).url],
authQueryFnFactory<PersonResponse>(session?.accessToken),
)
}
6 changes: 6 additions & 0 deletions src/common/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ export const routes = {
index: '/campaigns',
create: '/campaigns/create',
viewCampaignBySlug: (slug: string) => `/campaigns/${slug}`,
viewExpenses: (slug: string) => `/campaigns/${slug}/expenses`,
oneTimeDonation: (slug: string) => `/campaigns/donation/${slug}`,
expenses: {
create: (slug: string) => `/campaigns/${slug}/expenses/create`,
edit: (slug: string, id: string) => `/campaigns/${slug}/expenses/${id}`,
downloadFile: (id: string) => `/expenses/download-files/${id}`,
},
},
donation: {
viewCertificate: (donationId: string) => `/api/pdf/certificate/${donationId}`,
Expand Down
2 changes: 1 addition & 1 deletion src/common/util/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const canViewSupporters = (sessionRoles: SessionRoles): boolean => {
}

export const isAdmin = (session: Session | JWT | null): boolean => {
if (session) {
if (session && session.user && session.user.resource_access && session.user.realm_access) {
const sessionRoles: SessionRoles = {
realmRoles: session.user?.realm_access.roles ?? [],
resourceRoles: session.user?.resource_access?.account.roles ?? [],
Expand Down
17 changes: 11 additions & 6 deletions src/components/admin/GridActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ type Props = {
id: string
name: string
editLink?: string
disableView?: boolean
}

export default function GridActions({ modalStore, id, name, editLink }: Props) {
export default function GridActions({ modalStore, id, name, editLink, disableView }: Props) {
const { t } = useTranslation('admin')
const { showDetails, showDelete, setSelectedRecord } = modalStore

Expand All @@ -31,11 +32,15 @@ export default function GridActions({ modalStore, id, name, editLink }: Props) {

return (
<>
<Tooltip title={t('cta.view') || ''}>
<IconButton size="small" color="primary" onClick={detailsClickHandler}>
<PageviewOutlinedIcon />
</IconButton>
</Tooltip>
{!disableView ? (
<Tooltip title={t('cta.view') || ''}>
<IconButton size="small" color="primary" onClick={detailsClickHandler}>
<PageviewOutlinedIcon />
</IconButton>
</Tooltip>
) : (
''
)}
{editLink ? (
<Link href={editLink} passHref>
<Tooltip title={t('cta.edit') || ''}>
Expand Down
5 changes: 1 addition & 4 deletions src/components/admin/expenses/ExpenseTypeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@ export default function ExpenseTypeSelect({ name = 'type' }) {
defaultValue=""
label={t('fields.' + name)}
{...field}>
<MenuItem value="" disabled>
{t('fields.' + name)}
</MenuItem>
{values?.map((value, index) => (
<MenuItem key={index} value={value}>
{value}
{t('expenses:field-types.' + value)}
</MenuItem>
))}
</FormTextField>
Expand Down
2 changes: 2 additions & 0 deletions src/components/admin/expenses/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ export default function Form() {
status: data?.status || ExpenseStatus.pending,
currency: data?.currency || Currency.BGN,
amount: data?.amount || 0,
money: 0,
vaultId: data?.vaultId || '',
deleted: data?.deleted || false,
description: data?.description || '',
documentId: data?.documentId || '',
approvedById: data?.approvedById || '',
spentAt: '',
}

const mutationFn = id ? useEditExpense(id) : useCreateExpense()
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/expenses/grid/GridAppbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const addIconStyles = {
boxShadow: 3,
}

export default function GridAppbar() {
export default function ExpensesGridAppbar() {
const router = useRouter()

return (
Expand Down
10 changes: 10 additions & 0 deletions src/components/client/campaign-expenses/CampaignExpenseCreate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Form from 'components/client/campaign-expenses/Form'
import Layout from 'components/client/layout/Layout'

export default function CreateCampaignExpensePage() {
return (
<Layout maxWidth={false}>
<Form />
</Layout>
)
}
11 changes: 11 additions & 0 deletions src/components/client/campaign-expenses/CampaignExpenseEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Layout from 'components/client/layout/Layout'

import Form from './Form'

export default function ExpensesEditPage() {
return (
<Layout maxWidth={false}>
<Form />
</Layout>
)
}
Loading

0 comments on commit bcf03ef

Please sign in to comment.