frontend/src/routes/borne/commande/+page.svelte

Summary

Maintainability
Test Coverage
<script lang="ts">
    import Categories from '$lib/components/borne/categories.svelte';
    import Items from '$lib/components/borne/items.svelte';
    import { onMount, onDestroy } from 'svelte';
    import { formatPrice } from '$lib/utils';
    import { store } from '$lib/store/store';
    import { fly } from 'svelte/transition';
    import type {
        Account,
        Item,
        MenuCategory,
        NewTransaction,
        NewTransactionItem,
        TransactionItem
    } from '$lib/api';
    import Transactions from '$lib/components/borne/transactions.svelte';
    import { api } from '$lib/config/config';
    import Confirm from '$lib/components/borne/confirm.svelte';
    import { accountsApi, authApi, transactionsApi } from '$lib/requests/requests';
    import Pin from '$lib/components/borne/pin.svelte';
    import Error from '$lib/components/error.svelte';
    import Success from '$lib/components/success.svelte';
    import { goto } from '$app/navigation';
    import Stars from '$lib/components/random/stars.svelte';
    import Price from '$lib/components/random/price.svelte';

    let account: Account | undefined = undefined;
    let unsub: () => void;

    type NewTransactionItemWithItem = NewTransactionItem & {
        category: string;
        item: Item;
        pickedItems: NewTransactionItem[] | undefined;
    };

    let order: NewTransactionItemWithItem[] = [];
    let orderPrice: number = 0;

    onMount(() => {
        unsub = store.subscribe((state) => {
            account = state.account;
        });
    });

    onDestroy(() => {
        unsub();
    });

    let currentCatgory: string = '';

    let changeCategory: (category: string) => void = (category: string) => {
        currentCatgory = '';
        setTimeout(() => {
            currentCatgory = category;
        }, 10);
    };

    type MenuPopup = {
        categories: MenuCategory[] | undefined;
        pickedItems: NewTransactionItemWithItem[];
        tItem: NewTransactionItemWithItem;
        step: number;
    };
    let menuPicks: MenuPopup | undefined = undefined;

    let clickItemMenu: (item: Item) => void = (item: Item) => {
        let newPicks = menuPicks?.pickedItems ?? [];

        if (item.amount_left == 0) {
            return;
        }

        if (newPicks.find((i) => i.item_id == item.id)) {
            let found = newPicks.find((i) => i.item_id == item.id)!;
            if (found.amount >= (found.item.buy_limit ?? 9999)) {
                return;
            }
            if (found.amount >= (found.item.amount_left ?? 9999)) {
                return;
            }
            found.amount++;
        } else {
            let newTItem: NewTransactionItemWithItem = {
                category: currentCatgory,
                item_id: item.id,
                amount: 1,
                item: item,
                pickedItems: undefined
            };

            newPicks.push(newTItem);
        }

        if (menuPicks) {
            let amt = 0;
            for (let i = 0; i < menuPicks.pickedItems.length; i++) {
                if (menuPicks.pickedItems[i].category == currentCatgory) {
                    amt += menuPicks.pickedItems[i].amount;
                }
            }

            menuPicks.pickedItems = newPicks;

            if (amt >= (menuPicks.categories ?? [])[menuPicks.step].amount) {
                menuPicks.step++;
                if (menuPicks.step >= (menuPicks.categories ?? []).length) {
                    menuPicks.step = 0;

                    menuPicks.tItem.pickedItems = menuPicks.pickedItems;

                    let newOrder = order;
                    newOrder.push(menuPicks.tItem);
                    order = newOrder;
                    orderPrice += menuPicks.tItem.item.display_price ?? 999;
                    menuPicks = undefined;
                    return;
                }
                changeCategory((menuPicks.tItem.item.menu_categories ?? [])[menuPicks.step].id);
            }
        }
    };

    let clickItem: (item: Item) => void = (item: Item) => {
        let newOrder = order;

        if (newOrder.find((i) => i.item_id == item.id)) {
            let found = newOrder.find((i) => i.item_id == item.id)!;
            found.item = item;
            if (found.amount >= (found.item.buy_limit ?? 9999)) {
                return;
            }
            if (found.amount >= (found.item.amount_left ?? 9999)) {
                return;
            }
            found.amount++;
            order = newOrder;
            orderPrice += item.display_price ?? 999;
            return;
        }

        let newTItem: NewTransactionItemWithItem = {
            item_id: item.id,
            amount: 1,
            item: item,
            pickedItems: undefined,
            category: ''
        };

        if (item.is_menu && item.menu_categories) {
            menuPicks = {
                categories: item.menu_categories,
                pickedItems: [],
                tItem: newTItem,
                step: 0
            };
            changeCategory(item.menu_categories[0].id);
        } else {
            newOrder.push(newTItem);
            order = newOrder;
            orderPrice += item.display_price ?? 999;
        }
    };

    function removeItem(item: NewTransactionItemWithItem, amount: number = 1) {
        return () => {
            let newOrder = order;
            let found = newOrder.find((i) => i.item_id == item.item.id)!;

            if (found) {
                found!.amount -= amount;

                if (found!.amount < 0) {
                    amount += found!.amount;
                }

                if (found!.amount == 0) {
                    newOrder.splice(newOrder.indexOf(item), 1);
                }

                order = newOrder;
                orderPrice -= amount * (item.item.display_price ?? 999);
                return;
            }
        };
    }

    function confirmOrder(response: Boolean) {
        confirm = false;
        if (!response) {
            return;
        }
        pin = true;
    }

    function finalizeTransaction(card_pin: string) {
        if (card_pin == '') {
            pin = false;
            error = "J'ai besoin de votre code pin pour valider la transaction";
            setTimeout(() => {
                error = '';
            }, 3000);
            return;
        }
        let transaction: NewTransaction = {
            items: order.map((item) => {
                return {
                    item_id: item.item_id,
                    amount: item.amount,
                    picked_categories_items: item.pickedItems?.map((i) => {
                        return {
                            item_id: i.item_id,
                            amount: i.amount
                        };
                    })
                };
            }),
            card_pin: card_pin
        };

        transactionsApi()
            .postTransactions(transaction, { withCredentials: true })
            .then((res) => {
                success = 'Transaction effectuée avec succès';
                setTimeout(() => {
                    success = '';
                    authApi().logout({ withCredentials: true });
                    goto('/borne');
                }, 3000);
                order = [];
                orderPrice = 0;
            })
            .catch((err) => {
                error = 'Erreur lors de la transaction';
                setTimeout(() => {
                    error = '';
                }, 3000);
                pin = false;
            });
        confirm = false;
    }

    let error = '';
    let success = '';
    let pin = false;
    let confirm = false;
    let sidebar = true;
</script>

{#if confirm}
    <Confirm custom_text="Envoyer la commande ?" callback={confirmOrder} />
{/if}

{#if pin}
    <Pin callback={finalizeTransaction} />
{/if}

{#if error}
    <Error {error} />
{/if}

{#if success}
    <Success message={success} />
{/if}

<div
    id="main"
    class="absolute w-screen h-screen top-0 left-0 overflow-y-hidden"
    style="background-color:#393E46"
>
    {#if !menuPicks}
        <div class="{sidebar ? 'w-4/5' : 'w-full'} h-full relative transition-all ease-in-out">
            <div class="p-4 flex justify-between" style="background-color:#222831">
                <button
                    class="flex items-center h-1/2 space-x-2 px-4 py-2 mr-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors duration-300"
                    on:click={() => {
                        goto('/borne/index');
                    }}
                >
                    <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:chevron-left" />
                </button>
                <Categories {changeCategory} />
                <button
                    class="flex items-center space-x-2 px-4 py-2 ml-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors duration-300 animate-pulse"
                    on:click={() => {
                        sidebar = !sidebar;
                    }}
                >
                    {#if sidebar}
                        <iconify-icon
                            class="text-white align-middle text-2xl"
                            icon="akar-icons:chevron-right"
                        />
                    {:else}
                        <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:chevron-left" />
                    {/if}
                </button>
            </div>
            {#if currentCatgory != ''}
                <Items category={currentCatgory} click={clickItem} />
            {/if}
        </div>
    {:else}
        <div class="{sidebar ? 'w-4/5' : 'w-full'} h-full relative transition-all ease-in-out">
            <div class="p-4 flex justify-between" style="background-color:#222831">
                <button
                    class="flex items-center h-1/2 space-x-2 px-4 py-2 mr-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors duration-300"
                    on:click={() => {
                        goto('/borne/index');
                    }}
                >
                    <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:chevron-left" />
                </button>
                <!-- Title -->
                <h1 class="text-white text-md md:text-md lg:text-2xl">
                    Choix de {(menuPicks.categories ?? [])[menuPicks.step].amount}
                    {(menuPicks.categories ?? [])[menuPicks.step].name}
                </h1>
                <button
                    class="flex items-center space-x-2 px-4 py-2 ml-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors duration-300 animate-pulse"
                    on:click={() => {
                        sidebar = !sidebar;
                    }}
                >
                    {#if sidebar}
                        <iconify-icon
                            class="text-white align-middle text-2xl"
                            icon="akar-icons:chevron-right"
                        />
                    {:else}
                        <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:chevron-left" />
                    {/if}
                </button>
            </div>
            {#if currentCatgory != ''}
                <Items category={currentCatgory} click={clickItemMenu} />
            {/if}
        </div>
    {/if}
    {#if sidebar}
        <div
            class="absolute top-0 right-0 w-1/5 h-screen"
            style="background-color:#222831"
            in:fly={{ x: 300, duration: 200 }}
            out:fly={{ x: 300, duration: 200 }}
        >
            <div class="px-4 py-1 flex justify-between h-[12%]">
                <div
                    class="flex flex-col gap-5 justify-center items-center w-full h-full overflow-x-auto overflow-y-hidden"
                >
                    <!-- Commande en cours title -->
                    <h1 class="text-white text-2xl font-semibold">Commande actuelle</h1>
                    <!-- Subtitle with current balance -->
                    <h2 class="text-white text-md flex flex-row gap-2">
                        Disponible:
                        <div class="flex flex-col">
                            <Price amount={account?.balance ?? 0} />
                            {#if (account?.points ?? 0) > 0}
                                <Stars stars={account?.points ?? 0} />
                            {/if}
                        </div>
                    </h2>

                    <!-- Spacer -->
                </div>
            </div>
            <hr class="w-full border-white" />

            <!-- Items in current commande with buttons for + and - with how much there is and the cost -->
            <div class="relative flex flex-col gap-5 justify-center items-center h-[70%] p-4">
                {#if order.length == 0}
                    <h1 class="text-white text-md md:text-md lg:text-2xl">Aucun article</h1>
                {:else}
                    <button
                        class="w-10 h-10 rounded-full absolute top-2 bg-red-500"
                        on:click={() => {
                            order = [];
                            orderPrice = 0;
                        }}
                    >
                        <iconify-icon class="text-white align-middle text-2xl" icon="icomoon-free:bin" />
                    </button>
                {/if}
                <div class="grid grid-cols-2 gap-10 overflow-x-auto overflow-y-scroll">
                    {#each order as item}
                        <div class="flex flex-col justify-center gap-5 items-center w-full">
                            <button
                                class="-mr-20 -my-6 w-6 h-6 rounded-full z-10"
                                on:click={removeItem(item, item.amount)}
                            >
                                <iconify-icon class="text-white align-middle text-2xl" icon="ic:outline-cancel" />
                            </button>
                            <img
                                draggable="false"
                                class="w-16 h-16 object-contain"
                                src={api() + item.item.picture_uri}
                                alt={item.item.name}
                            />
                            <div class="flex flex-row justify-center items-center">
                                <button
                                    class="w-10 h-10 border-2 border-gray-300 rounded-full"
                                    on:click={removeItem(item)}
                                >
                                    <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:minus" />
                                </button>
                                <span class="text-lg text-white mx-4">{item.amount}</span>
                                <button
                                    class="w-10 h-10 border-2 border-gray-300 rounded-full"
                                    on:click={() => clickItem(item.item)}
                                >
                                    <iconify-icon class="text-white align-middle text-2xl" icon="akar-icons:plus" />
                                </button>
                            </div>
                            <span class="text-lg text-white"
                                >{formatPrice((item.item.display_price ?? 999) * item.amount)}</span
                            >
                        </div>
                    {/each}
                </div>
            </div>
            <hr class="w-full border-white" />
            <div class="p-1 flex justify-between bottom-0 h-[20%]">
                <div
                    class="flex flex-col gap-2 justify-center items-center w-full h-full overflow-x-auto overflow-y-hidden"
                >
                    <h1 class="text-md md:text-md lg:text-2xl text-white">Total</h1>
                    <h2 class="w-full text-md text-white flex flex-row gap-10 justify-center">
                        <div class="flex flex-col text-center">
                            <h3 class="font-semibold">Coût:</h3>
                            <Price amount={orderPrice} />
                        </div>
                        <div class="flex flex-col text-center">
                            <h3 class="font-semibold">Reste:</h3>
                            <div class="flex flex-col">
                                {#if orderPrice < (account?.points ?? 0)}
                                    <Price amount={account?.balance ?? 0} />
                                    {#if (account?.points ?? 0) > 0}
                                        <Stars stars={(account?.points ?? 0) - orderPrice} />
                                    {/if}
                                {:else}
                                    <Price amount={(account?.balance ?? 0) - orderPrice + (account?.points ?? 0)} />
                                {/if}
                            </div>
                        </div>
                    </h2>

                    <button
                        class="w-full h-16 bg-green-500 rounded-lg text-white text-lg font-bold"
                        on:click={() => (confirm = true)}
                    >
                        Valider la commande
                    </button>
                </div>
            </div>
        </div>
    {/if}
</div>