Skip to content

Commit

Permalink
Merge pull request #503 from AOEpeople/bugfix/#264947-max-two-meals
Browse files Browse the repository at this point in the history
Bugfix/#264947 max two meals
  • Loading branch information
MalibusParty authored Aug 12, 2024
2 parents 7a72a4f + fc5ea85 commit 60daea6
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 28 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# generate frontend assets
FROM node:20 as frontend
FROM node:20 AS frontend
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests \
build-essential \
nodejs
WORKDIR var/www/html/src/Resources
WORKDIR /var/www/html/src/Resources
COPY src/Resources/package.json src/Resources/package-lock.json ./
RUN npm install
COPY src/Resources/ .
Expand Down
15 changes: 15 additions & 0 deletions src/Mealz/MealBundle/Repository/ParticipantRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -535,4 +535,19 @@ public function getParticipationsOfSlot(Slot $slot): array

return $participations;
}

public function getParticipationCountByProfile(Profile $profile, DateTime $date): int
{
$queryBuilder = $this->createQueryBuilder('p')
->select('p.id')
->join('p.meal', 'm', 'ON')
->where('p.profile = :profile')
->andWhere('m.dateTime = :day')
->setParameter('profile', $profile->getUsername())
->setParameter('day', $date->format('Y-m-d H:i:s.u'));

$participations = $queryBuilder->getQuery()->execute();

return count($participations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ public function removeFutureMealsByProfile(Profile $profile): void;
* Returns an array of all the participations for a given slot starting from the current week.
*/
public function getParticipationsOfSlot(Slot $slot): array;

/**
* Returns the number of participations a profile has on a date.
*/
public function getParticipationCountByProfile(Profile $profile, DateTime $date): int;
}
4 changes: 4 additions & 0 deletions src/Mealz/MealBundle/Service/ParticipationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public function __construct(
*/
public function join(Profile $profile, Meal $meal, ?Slot $slot = null, array $dishSlugs = []): ?array
{
if (2 <= $this->participantRepo->getParticipationCountByProfile($profile, $meal->getDateTime())) {
return null;
}

// user is attempting to take over an already booked meal by some participant
if (true === $this->mealIsOffered($meal) && true === $this->allowedToAccept($meal)) {
return $this->reassignOfferedMeal($meal, $profile, $dishSlugs);
Expand Down
6 changes: 3 additions & 3 deletions src/Resources/src/components/dashboard/CombiButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ interface DishInfo {
}
const props = defineProps<{
weekID: number | string;
dayID: number | string;
weekID?: number | string;
dayID?: number | string;
mealID: number | string;
meal: Meal;
}>();
const meal = props.meal ?? dashboardStore.getMeal(props.weekID, props.dayID, props.mealID);
const meal = props.meal ?? dashboardStore.getMeal(props.weekID ?? -1, props.dayID ?? -1, props.mealID);
const emit = defineEmits(['addEntry', 'removeEntry']);
const selected = ref();
let dishes: DishInfo[] = [];
Expand Down
3 changes: 1 addition & 2 deletions src/Resources/src/components/dashboard/CombiModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
class="mt-2 grid"
>
<CombiButtonGroup
v-if="weekID && dayID"
:key="key"
:weekID="weekID"
:dayID="dayID"
Expand Down Expand Up @@ -105,7 +104,7 @@ const props = defineProps<{
const { t } = useI18n();
const emit = defineEmits(['closeCombiModal']);
const meals = props.meals ?? dashboardStore.getMeals(props.weekID ?? 0, props.dayID ?? 0);
let keys = Object.keys(meals).filter((mealID) => meals[mealID].dishSlug !== 'combined-dish');
const keys = computed(() => Object.keys(meals).filter((mealID) => meals[mealID].dishSlug !== 'combined-dish'));
const slugs = ref<string[]>([]);
const bookingDisabled = computed(() => slugs.value.length < 2);
Expand Down
2 changes: 1 addition & 1 deletion src/Resources/src/components/dashboard/Day.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
:class="[day?.isLocked || !day?.isEnabled || (emptyDay && !isEventDay) ? 'bg-[#80909F]' : 'bg-primary-2']"
>
<InformationButton
v-if="!day?.isLocked && !emptyDay"
v-if="!emptyDay"
:dayID="dayID"
:index="index"
class="hover: row-start-1 size-[24px] cursor-pointer p-1 text-center"
Expand Down
75 changes: 60 additions & 15 deletions src/Resources/src/components/guest/GuestCheckbox.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<template>
<span
data-cy="guest-checkbox"
:class="[
enabled ? 'bg-primary-3' : '',
'size-[30px] cursor-pointer rounded-md border-[0.5px] border-[#ABABAB] xl:size-[20px]'
]"
class="size-[30px] cursor-pointer rounded-md border-[0.5px] border-[#ABABAB] xl:size-[20px]"
:class="isChecked ? 'bg-primary-3' : ''"
@click="handle"
>
<CheckIcon
v-if="enabled"
v-if="isChecked"
class="relative left-[10%] top-[10%] size-4/5 text-white"
/>
</span>
Expand All @@ -22,7 +20,7 @@

<script setup lang="ts">
import { CheckIcon } from '@heroicons/vue/solid';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import useEventsBus from '@/tools/eventBus';
import CombiModal from '@/components/dashboard/CombiModal.vue';
import { type Meal } from '@/api/getDashboardData';
Expand All @@ -31,44 +29,91 @@ import { type Dictionary } from '@/types/types';
const props = defineProps<{
meals: Dictionary<Meal>;
mealId: number | string;
chosenMeals: string[];
}>();
const enabled = ref(false);
const open = ref(false);
const { emit } = useEventsBus();
const isCombiBox = (props.meals[props.mealId] as Meal).dishSlug === 'combined-dish';
let hasVariations = false;
let hasVariations = false;
Object.values(props.meals).forEach((meal) => ((meal as Meal).variations ? (hasVariations = true) : ''));
const isChecked = computed(() => props.chosenMeals.includes(props.mealId.toString() ?? ''));
function handle() {
// Is a combi meal
if (isCombiBox) {
if (isCombiBox && (isMealBookable() || isChecked.value)) {
// has variations
if (hasVariations) {
if (hasVariations && isChecked.value === false) {
open.value = true;
} else {
let combiDishes = Object.values(props.meals)
const combiDishes = Object.values(props.meals)
.filter((meal) => (meal as Meal).dishSlug !== 'combined-dish')
.map((meal) => (meal as Meal).dishSlug);
emit('guestChosenCombi', combiDishes);
emit('guestChosenMeals', props.mealId);
enabled.value = !enabled.value;
}
} else {
} else if (isMealBookable() || isChecked.value) {
emit('guestChosenMeals', props.mealId);
enabled.value = !enabled.value;
}
}
function handleCombiModal(dishes: string[]) {
if (dishes !== undefined) {
emit('guestChosenCombi', dishes);
emit('guestChosenMeals', props.mealId);
enabled.value = !enabled.value;
}
open.value = false;
}
function isMealBookable() {
const meal = props.meals[props.mealId];
if (meal.dishSlug === 'combined-dish') {
return isCombiBookable(meal);
} else {
return isDishBookable(meal);
}
}
function isCombiBookable(meal: Meal): boolean {
return !meal.reachedLimit && getBookableCombiMealIds().length >= 2;
}
function getBookableCombiMealIds() {
const combiMeals: number[] = [];
Object.values(props.meals)
.filter((meal) => (meal as Meal).dishSlug !== 'combined-dish')
.flatMap((combi) => {
if (combi.variations !== null && combi.variations !== undefined) {
return Object.values(combi.variations).map((variation) => getBookableObject(variation));
}
return getBookableObject(combi);
})
.filter((combi) => combi.bookable)
.forEach((combi) => {
if (!combiMeals.includes(combi.parent)) {
return combiMeals.push(combi.parent);
}
});
return combiMeals;
}
function getBookableObject(combi: Meal) {
return {
parent: combi.parentId ?? Math.random(),
bookable: isDishBookable(combi, 0.5)
};
}
function isDishBookable(meal: Meal, mealValue: number = 1): boolean {
return (
!meal.reachedLimit &&
(meal.limit === 0 || meal.limit === null || meal.limit >= (meal.participations ?? 0) + mealValue)
);
}
</script>
4 changes: 4 additions & 0 deletions src/Resources/src/components/guest/GuestDay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@
<GuestVariation
v-if="meal.variations && Object.values(meal.variations).length > 0"
:meal="meal"
:meal-id="mealID"
:chosen-meals="chosenMeals"
/>
<GuestMeal
v-else
:meals="guestData.meals"
:mealId="mealID"
:chosen-meals="chosenMeals"
/>
</div>
</div>
Expand Down Expand Up @@ -70,6 +73,7 @@ const { t, locale } = useI18n();
const props = defineProps<{
guestData: GuestDay;
chosenMeals: string[];
}>();
const weekday = computed(() => translateWeekday(props.guestData.date, locale));
Expand Down
4 changes: 3 additions & 1 deletion src/Resources/src/components/guest/GuestMeal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<GuestCheckbox
:meals="meals"
:mealId="mealId"
:chosen-meals="chosenMeals"
/>
</div>
</div>
Expand All @@ -57,6 +58,7 @@ import { MealState } from '@/enums/MealState';
const props = defineProps<{
meals: Dictionary<Meal>;
mealId: number | string;
chosenMeals: string[];
}>();
const { t, locale } = useI18n();
Expand All @@ -81,7 +83,7 @@ const description = computed(() => {
});
const mealCSS = computed(() => {
let css = 'grid grid-cols-2 content-center rounded-md h-[30px] xl:h-[20px] mr-[15px] ';
let css = 'flex content-center rounded-md h-[30px] xl:h-[20px] mr-[15px] ';
switch (meal.value.mealState) {
case MealState.OFFERABLE:
case MealState.DISABLED:
Expand Down
5 changes: 4 additions & 1 deletion src/Resources/src/components/guest/GuestVariation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<GuestCheckbox
:mealId="variationID"
:meals="{ [variationID]: variation }"
:chosen-meals="chosenMeals"
/>
</div>
</div>
Expand All @@ -64,14 +65,16 @@ const { generateMealState } = useMealState();
const props = defineProps<{
meal: Meal;
mealId: string | number;
chosenMeals: string[];
}>();
const parentTitle = computed(() => (locale.value.substring(0, 2) === 'en' ? props.meal.title.en : props.meal.title.de));
const mealCSS = computed(() => {
const css: Map<string, string> = new Map();
for (const [variationId, variation] of Object.entries(props.meal.variations ?? {})) {
let cssStr = 'grid grid-cols-2 content-center rounded-md h-[30px] xl:h-[20px] mr-[15px] ';
let cssStr = 'flex content-center rounded-md h-[30px] xl:h-[20px] mr-[15px] ';
switch (generateMealState(variation as Meal)) {
case MealState.OFFERABLE:
case MealState.DISABLED:
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@
},
"guest": {
"joined": "Wir freuen uns Sie dabei zu haben."
},
"meal": {
"maxReached": "Es dürfen maximal zwei Essen am Tag bestellt werden. Bitte wähle ein Essen ab um ein anderes Essen zu buchen."
}
},
"print": {
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@
},
"guest": {
"joined": "We are looking forward to meeting you."
},
"meal": {
"maxReached": "You may only book two meals per day. Please uncheck one meal to book another one."
}
},
"print": {
Expand Down
17 changes: 14 additions & 3 deletions src/Resources/src/views/Guest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
v-if="invitation"
:class="{ 'ring-2 ring-red': mealsMissing }"
:guestData="invitation"
:chosen-meals="form.chosenMeals"
/>
<GuestForm
v-model:firstName="form.firstName"
Expand All @@ -42,6 +43,8 @@ import { useI18n } from 'vue-i18n';
import GuestCompletion from '@/components/guest/GuestCompletion.vue';
import GuestForm from '@/components/guest/GuestForm.vue';
import GuestDay from '@/components/guest/GuestDay.vue';
import useFlashMessage from '@/services/useFlashMessage';
import { FlashMessageType } from '@/enums/FlashMessage';
interface IForm {
firstName: string;
Expand All @@ -58,6 +61,7 @@ const { invitation, error } = await useInvitationData(route.params.hash as strin
const result = ref(error.value === true ? 'data_error' : '');
const { receive } = useEventsBus();
const { t, locale } = useI18n();
const { sendFlashMessage } = useFlashMessage();
const form = reactive<IForm>({
firstName: '',
Expand All @@ -76,12 +80,19 @@ const lastNameMissing = ref(false);
const companyMissing = ref(false);
const mealsMissing = ref(false);
receive('guestChosenMeals', (slug: string) => {
const index = form.chosenMeals.indexOf(slug);
const reachedLimit = computed(() => form.chosenMeals.length >= 2);
receive('guestChosenMeals', (mealId: string) => {
const index = form.chosenMeals.indexOf(mealId);
if (index !== -1) {
form.chosenMeals.splice(index, 1);
} else if (reachedLimit.value === true) {
sendFlashMessage({
type: FlashMessageType.INFO,
message: 'meal.maxReached'
});
} else {
form.chosenMeals.push(slug);
form.chosenMeals.push(mealId);
}
});
Expand Down

0 comments on commit 60daea6

Please sign in to comment.