Skip to content

Commit

Permalink
added an endpoint for getting the costssheet and a basic table view f…
Browse files Browse the repository at this point in the history
…or it
  • Loading branch information
Felix Ruf committed Aug 9, 2023
1 parent d0e5db5 commit 2d9ec1b
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 29 deletions.
30 changes: 14 additions & 16 deletions src/Mealz/AccountingBundle/Controller/CostSheetController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

/**
* @Security("is_granted('ROLE_KITCHEN_STAFF')")
*/
class CostSheetController extends BaseController
{
private MailerInterface $mailer;
Expand All @@ -28,43 +32,38 @@ public function __construct(MailerInterface $mailer, EventDispatcherInterface $e
$this->eventDispatcher = $eventDispatcher;
}

/**
* @TODO: use own data model for user costs
*/
public function list(
ParticipantRepositoryInterface $participantRepo,
TransactionRepositoryInterface $transactionRepo
): Response {
$this->denyAccessUnlessGranted('ROLE_KITCHEN_STAFF');

): JsonResponse {
$transactionsPerUser = $transactionRepo->findUserDataAndTransactionAmountForGivenPeriod();
$users = $participantRepo->findCostsGroupedByUserGroupedByMonth();

// create column names
$numberOfMonths = 3;
$columnNames = ['earlier' => 'Prior to that'];
$columnNames = ['earlier' => 'earlier'];
$dateTime = new DateTime("first day of -$numberOfMonths month 00:00");
$earlierTimestamp = $dateTime->getTimestamp();
for ($i = 0; $i < $numberOfMonths + 1; ++$i) {
$columnNames[$dateTime->getTimestamp()] = $dateTime->format('F');
$columnNames[$dateTime->getTimestamp()] = clone $dateTime;
$dateTime->modify('+1 month');
}
$columnNames['total'] = 'Total';
$columnNames['total'] = 'total';

// create table rows
foreach ($users as $username => &$user) {
$userCosts = array_fill_keys(array_keys($columnNames), '0');
foreach ($user['costs'] as $cost) {
$monthCosts = $this->getRemainingCosts($cost['costs'], $transactionsPerUser[$username]['amount']);
if ($cost['timestamp'] < $earlierTimestamp) {
$userCosts['earlier'] = bcadd($userCosts['earlier'], $monthCosts, 4);
$userCosts['earlier'] = (float) bcadd($userCosts['earlier'], $monthCosts, 4);
} else {
$userCosts[$cost['timestamp']] = $monthCosts;
}
$userCosts['total'] = bcadd($userCosts['total'], $monthCosts, 4);
$userCosts['total'] = (float) bcadd($userCosts['total'], $monthCosts, 4);
}
if ($transactionsPerUser[$username]['amount'] > 0) {
$userCosts['total'] = '+' . $transactionsPerUser[$username]['amount'];
$userCosts['total'] = $transactionsPerUser[$username]['amount'];
}
$user['costs'] = $userCosts;

Expand All @@ -75,8 +74,10 @@ public function list(
}

ksort($users, SORT_STRING);
unset($columnNames['total']);
unset($columnNames['earlier']);

return $this->render('MealzAccountingBundle::costSheet.html.twig', [
return new JsonResponse([
'columnNames' => $columnNames,
'users' => $users,
]);
Expand Down Expand Up @@ -128,9 +129,6 @@ private function getRemainingCosts($costs, &$transactions)
return ($result < 0) ? 0 : $result * -1;
}

/**
* @Security("is_granted('ROLE_KITCHEN_STAFF')")
*/
public function sendSettlementRequest(
Profile $userProfile,
Wallet $wallet,
Expand Down
5 changes: 3 additions & 2 deletions src/Mealz/AccountingBundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ mealz_accounting_accounting_book_finance_export:
path: /accounting/book/finance/export/{dateRange}
defaults: { _controller: App\Mealz\AccountingBundle\Controller\AccountingBookController::exportPDF, dateRange: null }

mealz_accounting.cost_sheet:
path: /print/costsheet
mealz_accounting_api_costs:
path: /api/costs
defaults: { _controller: App\Mealz\AccountingBundle\Controller\CostSheetController::list }
methods: [ GET ]

mealz_accounting_cost_sheet_hide_user_request:
path: /print/costsheet/hideuser/request/{profile}
Expand Down
16 changes: 16 additions & 0 deletions src/Resources/src/api/getCosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ICosts } from "@/stores/costsStore";
import useApi from "./api";

/**
* Performs a GET request for a list of costs
*/
export default async function getCosts() {
const { error, response: costs, request } = useApi<ICosts>(
'GET',
'api/costs'
);

await request();

return { error, costs };
}
16 changes: 16 additions & 0 deletions src/Resources/src/components/costs/CashRegisterLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<router-link
:to="'/costs'"
class="flex flex-row items-center gap-2"
>
<CurrencyEuroIcon class="h-6 w-6" />
<span>{{ t('costs.cashRegister') }}</span>
</router-link>
</template>

<script setup lang="ts">
import { CurrencyEuroIcon } from '@heroicons/vue/outline';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
38 changes: 38 additions & 0 deletions src/Resources/src/components/costs/CostsHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<div class="mb-8 grid w-full grid-cols-3 gap-3 sm:grid-rows-[minmax(0,1fr)_30px] min-[900px]:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] min-[900px]:gap-1">
<h2 class="col-span-3 col-start-1 row-span-1 row-start-1 m-0 w-full self-center justify-self-start max-[380px]:text-[24px] min-[900px]:col-span-1">
{{ t('costs.header') }}
</h2>
<CashRegisterLink
class="col-span-3 row-start-2 justify-self-center sm:col-span-1 sm:col-start-1 sm:justify-self-start min-[900px]:row-start-2"
/>
<InputLabel
v-model="filter"
:label-text="t('costs.search')"
/>
</div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import CashRegisterLink from './CashRegisterLink.vue';
import InputLabel from '../misc/InputLabel.vue';
import { computed } from 'vue';
const { t } = useI18n();
const props = defineProps<{
modelValue: string
}>();
const emit = defineEmits(['update:modelValue']);
const filter = computed({
get() {
return props.modelValue;
},
set(filter) {
emit('update:modelValue', filter);
}
})
</script>
78 changes: 78 additions & 0 deletions src/Resources/src/components/costs/CostsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<Table
:labels="columnNames"
:header-text-left="false"
>
<tr
v-for="[username, costs] in filteredUsers"
:key="username"
class="max-h-[62px] border-b-2 border-gray-200 text-right text-[12px] xl:text-[18px]"
>
<td class="py-2 text-left">
{{ `${costs.name}, ${costs.firstName}` }}
</td>
<td>
{{ new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(costs.costs['earlier']) }}
</td>
<td
v-for="column in Object.keys(CostsState.columnNames)"
:key="`${String(column)}_${username}`"
>
{{ new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(costs.costs[column]) }}
</td>
<td>
{{ new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(costs.costs['total']) }}
</td>
<td>
Actions to be implemented
</td>
</tr>
</Table>
</template>

<script setup lang="ts">
import Table from '@/components/misc/Table.vue';
import { useCosts } from '@/stores/costsStore';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const { CostsState, getColumnNames } = useCosts();
const props = defineProps<{
filter: string
}>();
const columnNames = computed(() => ['Name', t('costs.table.earlier'), ...getColumnNames(locale.value), t('costs.table.total'), t('costs.table.actions')]);
const filteredUsers = computed(() => {
if (props.filter === '') {
return Object.entries(CostsState.users)
}
const filterStrings = props.filter.split(/[\s,.]+/).map(filterStr => filterStr.toLowerCase());
const regex = createRegexForFilter(filterStrings);
return Object.entries(CostsState.users).filter(user => {
const [key, value] = user;
const searchStrings = [value.firstName, value.name].join(' ');
return regex.test(searchStrings);
});
});
function createRegexForFilter(filterStrings: string[]): RegExp {
let regexStr = '';
for (let i = 0; i < filterStrings.length - 1; i++) {
regexStr += `(${filterStrings[i]}.*${filterStrings[i+1]})?`;
regexStr += `(${filterStrings[i+1]}.*${filterStrings[i]})?`;
}
regexStr += `(${filterStrings.join('|')})`;
if (filterStrings.length > 1) {
regexStr = '^' + regexStr + '?$';
}
return new RegExp(regexStr, 'i');
}
</script>
10 changes: 7 additions & 3 deletions src/Resources/src/components/misc/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
:key="label"
scope="col"
class="text-left text-[11px] font-bold uppercase leading-4 tracking-[1.5px] last:text-right"
:class="headerTextLeft ? 'text-left' : 'first:text-left last:text-right text-center'"
>
{{ label }}
</th>
Expand All @@ -23,7 +24,10 @@
</template>

<script setup lang="ts">
defineProps<{
labels: string[]
}>();
withDefaults(defineProps<{
labels: string[],
headerTextLeft?: boolean
}>(), {
headerTextLeft: true
});
</script>
10 changes: 10 additions & 0 deletions src/Resources/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
"english": "Englischer Titel"
}
},
"costs": {
"header": "Liste der Kosten",
"cashRegister": "Kasse",
"search": "Benutzer filtern",
"table": {
"earlier": "Vorher",
"total": "Gesamt",
"actions": "Aktionen"
}
},
"changeLanguage": "English version",
"combiModal": {
"title": "Wähle eine Kombination für dein Kombi-Meal",
Expand Down
10 changes: 10 additions & 0 deletions src/Resources/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
"english": "English Title"
}
},
"costs": {
"header": "Liste der Kosten",
"cashRegister": "Kasse",
"search": "Filter users",
"table": {
"earlier": "Prior to that",
"total": "Total",
"actions": "Actions"
}
},
"changeLanguage": "Deutsche Version",
"combiModal": {
"title": "Choose a combination for your combined dish",
Expand Down
83 changes: 83 additions & 0 deletions src/Resources/src/stores/costsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import getCosts from "@/api/getCosts";
import { DateTime } from "@/api/getDashboardData";
import { isResponseObjectOkay } from "@/api/isResponseOkay";
import { Dictionary } from "types/types";
import { reactive, readonly } from "vue";
import { translateMonth } from "@/tools/localeHelper";

export interface ICosts {
columnNames: Dictionary<DateTime>,
users: Dictionary<UserCost>
}

interface UserCost {
name: string,
firstName: string,
hidden: boolean,
costs: Dictionary<number>
}

interface ICostsState extends ICosts {
error: string,
isLoading: boolean
}

const CostsState = reactive<ICostsState>({
columnNames: {},
users: {},
error: "",
isLoading: false
});

function isCosts(costs: ICosts): costs is ICosts {

if (costs.columnNames !== null && costs.columnNames !== undefined && costs.users !== null && costs.users !== undefined) {
const cost = Object.values(costs.users)[0];
const column = Object.values(costs.columnNames)[0];

return (
cost !== null &&
cost !== undefined &&
typeof (cost as UserCost).name === 'string' &&
typeof (cost as UserCost).firstName === 'string' &&
typeof (cost as UserCost).hidden === 'boolean' &&
(cost as UserCost).costs !== undefined &&
(cost as UserCost).costs !== null &&
Object.keys(cost).length === 4 &&
typeof (column as DateTime).date === 'string' &&
typeof (column as DateTime).timezone === 'string' &&
typeof (column as DateTime).timezone_type === 'number'
);
}

return false;
}

export function useCosts() {

async function fetchCosts() {
CostsState.isLoading = true;
const { error, costs } = await getCosts();

if (isResponseObjectOkay(error, costs, isCosts) === true) {
CostsState.columnNames = costs.value.columnNames;
CostsState.users = costs.value.users;
CostsState.error = '';
} else {
CostsState.error = 'Error on fetching Costs';
}
CostsState.isLoading = false;
}

function getColumnNames(locale: string) {
return Object.values(CostsState.columnNames).map(dateTime => {
return translateMonth(dateTime, locale);
});
}

return {
CostsState: readonly(CostsState),
fetchCosts,
getColumnNames
}
}
Loading

0 comments on commit 2d9ec1b

Please sign in to comment.