# [WP] BistroQ - Milestone 2 Report > [!Note] > This report can be viewed in [HackMD - Milestone 2](https://hackmd.io/gwAceDgMRQSu2DYeAvD3Sg) (We strongly recommend to read on the web for better reading experience). > [!Note] Links > Milestone 1 report: [HackMD - Milestone 1](https://hackmd.io/4tSDCY54TkaXIRHzsXetDg) > Link to project: [Google Drive](https://drive.google.com/drive/folders/1L-M17WuqwJLnX6Ev0qFW0QjEw0gDJ0Uv?usp=sharing) > Link to demo video [Youtube](https://www.youtube.com/watch?v=Jig65qtiblU) > [!Important] > The demo video does not have sound. Please turn on subtitle in Vietnamese. ## Table Of Contents - [Overview](#Overview) - [Project Setup](#Project-Setup) - [Backend Setup](#Backend-Setup) - [Frontend Setup](#Frontend-Setup) - [Testing Acount](#Testing-Acount) - [Features](#Features) - [Completed Works](#Completed-Works) - [UI/UX](#UIUX) - [Design Pattern](#Design-Pattern) - [Publish-Subscribe Pattern](#Publish-Subscribe-Pattern) - [Strategy Pattern](#Strategy-Pattern) - [Factory Pattern](#Factory-Pattern) - [Architecture](#Architecture) - [WinUI](#WinUI) - [Backend](#Backend) - [Advanced Topics](#Advanced-Topics) - [Semaphore (Reused)](#Semaphore-Reused) - [Cloud Service](#Cloud-Service) - [DispatcherTimer](#DispatcherTimer) - [Storyboard](#Storyboard) - [AutoMapper](#AutoMapper) - [IMessenger](#IMessenger) - [IDisposable](#IDisposable) - [Resources](#Resources) - [Storage File and File Picker](#Storage-File-and-File-Picker) - [Unit test](#Unit-test) - [Git Flow Evidence](#Git-Flow-Evidence) - [Code Review Evidence](#Code-Review-Evidence) - [PR Cloud Product Image API](#PR-Cloud-Product-Image-API) - [PR Table Fix API](#PR-Table-Fix-API) - [PR Cashier Table Enhancement Page](#PR-Cashier-Table-Enhancement-Page) - [PR Refactor WinUI Version 1](#PR-Refactor-WinUI-Version-1) - [PR Client Order Page](#PR-Client-Order-Page) - [PR Kitchen Order Page](#PR-Kitchen-Order-Page) - [PR API Integration Client Order Page](#PR-API-Integration-Client-Order-Page) - [PR API Integration Kitchen Order Page](#PR-API-Integration-Kitchen-Order-Page) - [PR Update Client Order API](#PR-Update-Client-Order-API) - [PR Update Database And Kitchen Order API](#PR-Update-Database-And-Kitchen-Order-API) - [PR Product and Category Admin Interface](#PR-Product-and-Category-Admin-Interface) - [PR Client Order Crud Test](#PR-Client-Order-Crud-Test) ## Overview - In this milestone, we've upgraded our WinUI project to .NET 8.0. ## Project Setup ### Backend Setup 1. Create `.env` based on `.env.sample` file and fill the API Key with the key provided in submission in Moodle. 2. Ensure MySQL and database is working 3. Run database migration: ```bash! dotnet ef database update --project BistroQ.Infrastructure --startup-project BistroQ.API --context BistroQ.Infrastructure.Data.BistroQContext --verbose # Run migrations ``` 4. Run server: ```bash! dotnet watch run --project .\BistroQ.API ``` ### Frontend Setup 1. Have backend server running successfully. 2. Run `BistroQ.Presentation` Project through Visual Studio. ### Testing Acount | Role | Username | Password | | ------- | -------- | -------- | | Admin | admin | admin | | Kitchen | kitchen | kitchen | | Cashier | cashier | cashier | | Client | client1 | client | | Client | client2 | client | | Client | client3 | client | | Client | client4 | client | | Client | client5 | client | | Client | client6 | client | | Client | client7 | client | | Client | client8 | client | | Client | client9 | client | ## Features ### Completed Works In this milestone, we have done these tasks below: | Task | Working hours (est.) | Responsible by | | ---- | -------------------- | -------------- | | API for Order (Client) | 1.25 hour | Tiến | | API for Order (Cashier) | 0.75 hour | Toàn | | API for Order (Kitchen) | 1 hour | Toàn | | Client screen (See product list, filter products by category, cart, order) | 2.75 hours | Tiến | | Tracking order by status (client) | 0.75 hour | Tiến | | Cashier screen (See available tables, see orders on table) | 2 hours | Toàn | | Kitchen screen (See incoming orders, change state of orders, view old orders) | 2.75 hours | Toàn | | API and Configurate Cloud service (for storing images) | 1.75 hours | Toàn | | Admin Product Interface | 0.75 hour *(excluded CRUD)* | Toàn | | Admin Category Interface | 0.5 hour *(excluded CRUD)* | Toàn | ## UI/UX Our application supports both light and dark themes. You can watch [Demo Video](https://www.youtube.com/watch?v=Jig65qtiblU) or try it yourself for a better experience. ## Design Pattern :::info We continued to reuse most of the design patterns from the previous milestone. The following are the newly introduced ones: ::: ### Publish-Subscribe Pattern Handling and passing `Events` across `Pages` and `Controls` can sometimes be complex. To address this, we decided to adopt a communication system that strongly follows the `Publish-Subscribe Pattern`. This approach is ideal for our needs, especially since we plan to implement `WebSocket` functionality in next milestone. Additionally, it aligns with Microsoft's recommendation in [Enterprise Application Patterns in MAUI](https://learn.microsoft.com/en-us/dotnet/architecture/maui/). We used `IMessenger` to implement this pattern, you can view detail at [IMessenger](#IMessenger). ### Strategy Pattern We used this pattern in `KitchenOrderPage` because the chef has multiple actions to perform on an `Order Item` based on its status. By implementing this pattern, we moved the logic for these actions to separate classes for better organization and maintainability. ```csharp! public record KitchenActionMessage(KitchenAction Action); public enum KitchenAction { Complete, Move, Cancel } // Strategy Interface public interface IOrderItemActionStrategy { Task ExecuteAsync(IEnumerable<OrderItemViewModel> orderItems); } public class CompleteItemStrategy : IOrderItemActionStrategy { public async Task ExecuteAsync(IEnumerable<OrderItemViewModel> orderItems) { // Implementation } } public class MoveItemStrategy : IOrderItemActionStrategy { public async Task ExecuteAsync(IEnumerable<OrderItemViewModel> orderItems) { // Implementation } } public class CancelItemStrategy : IOrderItemActionStrategy { public async Task ExecuteAsync(IEnumerable<OrderItemViewModel> orderItems) { // Implementation } } // Call strategies public async void Receive(KitchenActionMessage message) { var strategy = _strategyFactory.GetStrategy(message.Action, State); strategy.ExecuteAsync(State.SelectedItems); } ``` ### Factory Pattern We used this pattern to create an `OrderItemStrategy` instance (usage example above), eliminating the need for hard-coding `if-else` or `switch-case` statements inside the `ViewModel`. ```csharp! public class KitchenStrategyFactory { private readonly Dictionary<KitchenAction, Func<IOrderItemDataService, KitchenOrderState, IOrderItemActionStrategy>> _strategies; private readonly IOrderItemDataService _orderItemDataService; public KitchenStrategyFactory(IOrderItemDataService service) { _orderItemDataService = service; _strategies = new Dictionary<KitchenAction, Func<IOrderItemDataService, KitchenOrderState, IOrderItemActionStrategy>> { [KitchenAction.Complete] = (s, state) => new CompleteItemStrategy(s, state), [KitchenAction.Move] = (s, state) => new MoveItemStrategy(s, state), [KitchenAction.Cancel] = (s, state) => new CancelItemStrategy(s, state) }; } public IOrderItemActionStrategy GetStrategy(KitchenAction action, KitchenOrderState state) => _strategies[action](_orderItemDataService, state); } ``` ## Architecture ### WinUI - We have decided to reconstruct our WinUI project. The folder structure shown below. ``` . ├── BistroQ.Domain │   ├── Contracts │   │   ├── Common │   │   ├── Http │   │   └── Services │   ├── Dtos │   │   ├── Auth │   │   ├── Category │   │   ├── Common │   │   ├── Orders │   │   ├── Products │   │   ├── Tables │   │   └── Zones │   ├── Enums │   ├── Helpers │   └── Models │   ├── Entities │   └── Exceptions ├── BistroQ.Presentation │   ├── Activation │   ├── Assets │   │   └── Icons │   ├── Behaviors │   ├── Contracts │   │   ├── Services │   │   └── ViewModels │   ├── Controls │   │   └── Skeleton │   ├── Converters │   ├── Enums │   ├── Helpers │   ├── Mappings │   ├── Messages │   ├── Models │   ├── Properties │   ├── Services │   ├── Strings │   │   └── en-us │   ├── Styles │   ├── Validation │   ├── ViewModels │   │   ├── AdminTable │   │   ├── AdminZone │   │   ├── CashierTable │   │   ├── Client │   │   ├── Commons │   │   ├── KitchenHistory │   │   ├── KitchenOrder │   │   ├── Models │   │   └── States │   └── Views │   ├── AdminTable │   ├── AdminZone │   ├── CashierTable │   ├── Client │   ├── KitchenHistory │   ├── KitchenOrder │   └── UserControls ├── BistroQ.Service │   ├── Auth │   ├── Common │   ├── Data │   └── Http ├── BistroQ.Tests.MsTest ``` - Key differences: - Separate UI Model and Domain Model. Connect through `AutoMapper`. - Create `Services` project containing business logic. - Rename Projects: `BistroQ` to `BistroQ.Presentation`, `BistroQ.Core` to `BistroQ.Domain`. :::success Now we can see clearly each layer doing its job. ::: ### Backend :::info We decided to keep backend architecture since it was well-constructed. ::: ## Advanced Topics ### Semaphore (Reused) Used in `DialogService` to ensure that only one dialog is displayed at a time. The system will throw an exception if multiple dialogs are shown simultaneously. ```csharp private static readonly SemaphoreSlim _semaphore = new(1); await _semaphore.WaitAsync(); try { await dialog.ShowAsync(); } finally { _semaphore.Release(); } ``` ### Cloud Service We used `AWS S3` to store Products Images and retrieve them via `CloudFront`, avoiding local storage. ```csharp! public class AwsStorageService : ICloudStorageService { private readonly IAmazonS3 _s3Client; private readonly AwsSettings _awsSettings; public async Task UploadFileAsync(Stream fileStream, string key, string contentType) { try { var putRequest = new PutObjectRequest { BucketName = _awsSettings.BucketName, Key = key, InputStream = fileStream, ContentType = contentType, }; await _s3Client.PutObjectAsync(putRequest); } catch (AmazonS3Exception ex) { throw new ApplicationException($"Error uploading to S3: {ex.Message}", ex); } } } ``` ### DispatcherTimer A `DispatcherTimer` is a timer class which used to execute code at specified intervals on the UI thread. We used it to implement a time counter for tracking the duration a table has been in use for dining. ```csharp public partial class TimeCounterViewModel : ObservableObject { private DispatcherTimer _timer; private TimeSpan _elapsedTime; private DateTime _startTime; public TimeCounterViewModel() { _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromSeconds(1); _timer.Tick += Timer_Tick; } // This will be called every second private void Timer_Tick(object sender, object e) { ElapsedTime = DateTime.Now - _startTime; } } ``` ### Storyboard This is an animation container that lets you create and control animations in WinUI. We used it to create a repeated fade animation for skeleton (shows when the content is loading). ```xml! <Rectangle x:Name="SkeletonRectangle" Width="{x:Bind Width, Mode=OneWay}" Height="{x:Bind Height, Mode=OneWay}" Fill="{ThemeResource SystemControlBackgroundBaseLowBrush}" RadiusX="4" RadiusY="4"> <Rectangle.Resources> <Storyboard x:Name="PulseAnimation"> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="SkeletonRectangle" Storyboard.TargetProperty="Opacity" RepeatBehavior="Forever"> <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0.8"/> <LinearDoubleKeyFrame KeyTime="0:0:0.5" Value="0.2"/> <LinearDoubleKeyFrame KeyTime="0:0:1" Value="0.8"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </Rectangle.Resources> </Rectangle> ``` ### AutoMapper `AutoMapper` is an object-to-object mapping library in `.NET`. We used it to map between DTOs and objects across layers, eliminating the need for manual coding. ```csharp! public class MappingProfile : Profile { public MappingProfile() { CreateMap<Table, TableViewModel>(); CreateMap<Table, TableViewModel>().ReverseMap(); // Other Mapping Definitions } } // Usage var table = new Table(); var tableVm = _mapper.Map<TableViewModel>(table); ``` ### IMessenger `IMessenger` is a messaging interface used in WinUI to integrate MVVM pattern. We used it in mostly all across controls/pages communication. ```csharp! // At source [RelayCommand] public void CheckoutRequestedCommand() { _messenger.Send(new CheckoutRequestedMessage(Order.TableId)); } // At destination public partial class CashierTableViewModel : ObservableObject, IRecipient<CheckoutRequestedMessage>, IDisposable { public CashierTableViewModel(IMessenger messenger) { _messenger = messenger; _messenger.RegisterAll(this); } public void Receive(CheckoutRequestedMessage message) { Debug.WriteLine("Checkout requested"); } public void Dispose() { _messenger.UnregisterAll(this); } } ``` ### IDisposable An interface in C# that provides a mechanism for managing resources. We used it to unregister messages and dispose of ViewModels when navigating away from a view. ```csharp! public AdminTablePage() { ViewModel = App.GetService<AdminTableViewModel>(); InitializeComponent(); this.Unloaded += (s, e) => ViewModel.Dispose(); } ``` ### Resources This allows the creation of reusable elements. For instance, the following defines a custom color that adapts to both light and dark themes. ```xaml! <ResourceDictionary.ThemeDictionaries> <ResourceDictionary x:Key="Light"> <Color x:Key="SuccessBaseColor">#2ECC71</Color> <Color x:Key="SuccessHoverColor">#27AE60</Color> </ResourceDictionary> <ResourceDictionary x:Key="Dark"> <Color x:Key="SuccessBaseColor">#2ECC71</Color> <Color x:Key="SuccessHoverColor">#27AE60</Color> </ResourceDictionary> </ResourceDictionary.ThemeDictionaries> ``` ### Storage File and File Picker The `FilePicker` is a UI element that allows users to select files, while `StorageFile` is a class representing a file in the system, used for reading, writing, and managing files in a Windows app. We utilized these two components to implement file uploading and send files to the API ```csharp! // ProductEditPage.xaml.cs private async void ProductEditPage_SelectImageButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { FileOpenPicker fileOpenPicker = new() { ViewMode = PickerViewMode.Thumbnail, FileTypeFilter = { ".jpg", ".jpeg", ".png", ".gif" }, }; var windowHandle = WindowNative.GetWindowHandle(App.MainWindow); InitializeWithWindow.Initialize(fileOpenPicker, windowHandle); var file = await fileOpenPicker.PickSingleFileAsync(); // Returns StorageFile if (file != null) { ViewModel.Form.ImageFile = await FileWrapper.FromStorageFileAsync(file); } } // FileWrapper.cs public static async Task<FileWrapper> FromStorageFileAsync(StorageFile file) { var stream = await file.OpenStreamForReadAsync(); var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream); memoryStream.Position = 0; var contentType = file.ContentType; var fileName = file.Name; return new FileWrapper(memoryStream, fileName, contentType); // This will be passed to call api via service } ``` ## Unit test We have implement some tests in backend. We decided to mock the backend services and test routes in controllers. To run the test, please use the Test runner/Test explorer in IDE like Visual Studio or Jetbrains Rider. ## Documentation In our API project, we have prepared the Doxyfile for generating documentations. Please install `doxygen` and its dependencies, then run the command below to generate documents. ```bash doxygen ``` ## Git Flow Evidence #### WinUI project ![image](https://hackmd.io/_uploads/r1VsLMDNkl.png) ![image](https://hackmd.io/_uploads/SkNYLzvV1x.png) ![image](https://hackmd.io/_uploads/B1muLzP41g.png) #### ASP .NET project ![image](https://hackmd.io/_uploads/rk10UfvEyx.png) ![image](https://hackmd.io/_uploads/SJZ6UfvEJg.png) ## Code Review Evidence :::success These PRs are sorted in ascending chronological order. ::: :::info ### PR Cloud Product Image API ::: ![PR_Product_Image_API-images-0](https://hackmd.io/_uploads/SJEyk9AXJg.jpg) ![PR_Product_Image_API-images-1](https://hackmd.io/_uploads/Bk4yk5RQyx.jpg) :::info ### PR Table Fix API ::: ![PR_Table_Fix_API-images-0](https://hackmd.io/_uploads/SJc3AYCQJe.jpg) ![PR_Table_Fix_API-images-1](https://hackmd.io/_uploads/BkqhAFRXyg.jpg) :::info ### PR Cashier Table Enhancement Page ::: ![PR_Cashier_Table_Enhancement-images-0](https://hackmd.io/_uploads/HJ_HJqR71x.jpg) ![PR_Cashier_Table_Enhancement-images-1](https://hackmd.io/_uploads/HytB1cRXJg.jpg) ![PR_Cashier_Table_Enhancement-images-2](https://hackmd.io/_uploads/BkFSJq0X1g.jpg) ![PR_Cashier_Table_Enhancement-images-3](https://hackmd.io/_uploads/HkYrJ9RXye.jpg) :::info ### PR Refactor WinUI Version 1 ::: ![PR_Refactor_Admin_Page-images-0](https://hackmd.io/_uploads/rkDEJcCXke.jpg) ![PR_Refactor_Admin_Page-images-1](https://hackmd.io/_uploads/S1DVk5C7Jg.jpg) ![PR_Refactor_Admin_Page-images-2](https://hackmd.io/_uploads/rJD4J9RQ1l.jpg) :::info ### PR Client Order Page ::: ![PR_Client_Order_Page-images-0](https://hackmd.io/_uploads/SyJQ1qCXkx.jpg) ![PR_Client_Order_Page-images-1](https://hackmd.io/_uploads/H1kQ1qAX1e.jpg) ![PR_Client_Order_Page-images-2](https://hackmd.io/_uploads/rykX1qAXkl.jpg) ![PR_Client_Order_Page-images-3](https://hackmd.io/_uploads/r1JQkc0Xkl.jpg) ![PR_Client_Order_Page-images-4](https://hackmd.io/_uploads/SyJXJcCmkl.jpg) ![PR_Client_Order_Page-images-5](https://hackmd.io/_uploads/S1k7J9C7ke.jpg) ![PR_Client_Order_Page-images-6](https://hackmd.io/_uploads/r11XkcAXJl.jpg) :::info ### PR Kitchen Order Page ::: ![PR_Kitchen_Order_Page-images-0](https://hackmd.io/_uploads/HJpCikPEyl.jpg) ![PR_Kitchen_Order_Page-images-1](https://hackmd.io/_uploads/rJp0jyDNyl.jpg) ![PR_Kitchen_Order_Page-images-2](https://hackmd.io/_uploads/HJ6AiJDE1e.jpg) ![PR_Kitchen_Order_Page-images-3](https://hackmd.io/_uploads/SyaCs1wVkg.jpg) :::info ### PR API Integration Client Order Page ::: ![PR_API_Integration_Client_Order-images-0](https://hackmd.io/_uploads/SkHXaJD4Jg.jpg) ![PR_API_Integration_Client_Order-images-1](https://hackmd.io/_uploads/BkSXTJP4kg.jpg) ![PR_API_Integration_Client_Order-images-2](https://hackmd.io/_uploads/r1rXayDN1l.jpg) :::info ### PR API Integration Kitchen Order Page ::: ![PR_API_Integration_Kitchen_Order-images-0](https://hackmd.io/_uploads/Byl-XTJPN1x.jpg) ![PR_API_Integration_Kitchen_Order-images-1](https://hackmd.io/_uploads/HyeZ7pkvE1e.jpg) ![PR_API_Integration_Kitchen_Order-images-2](https://hackmd.io/_uploads/HkZQpyv4ye.jpg) ![PR_API_Integration_Kitchen_Order-images-3](https://hackmd.io/_uploads/r1-7p1DNke.jpg) :::info ### PR Update Client Order API ::: ![PR_Update_Client_Order_API-images-0](https://hackmd.io/_uploads/BJZu6kwV1x.jpg) ![PR_Update_Client_Order_API-images-1](https://hackmd.io/_uploads/Skb_a1vEkx.jpg) ![PR_Update_Client_Order_API-images-2](https://hackmd.io/_uploads/rybdpyDEkl.jpg) ![PR_Update_Client_Order_API-images-3](https://hackmd.io/_uploads/BJ-OpJPN1g.jpg) :::info ### PR Update Database And Kitchen Order API ::: ![PR_Update_Database_And_Kitchen_Order_API-images-0](https://hackmd.io/_uploads/By4_Tyv41l.jpg) ![PR_Update_Database_And_Kitchen_Order_API-images-1](https://hackmd.io/_uploads/Bk4uTJwVyg.jpg) :::info ### PR Product and Category Admin Interface ::: ![PR_Product_Category_Admin_Interface-images-0](https://hackmd.io/_uploads/rklBvlP4yx.jpg) ![PR_Product_Category_Admin_Interface-images-1](https://hackmd.io/_uploads/BylSwlPV1l.jpg) ![PR_Product_Category_Admin_Interface-images-2](https://hackmd.io/_uploads/H1lHwgPN1e.jpg) ![PR_Product_Category_Admin_Interface-images-3](https://hackmd.io/_uploads/BykBPgwNyl.jpg) :::info ### PR Client Order Crud Test ::: ![PR_Client_Order_CRUD_Test-images-0](https://hackmd.io/_uploads/Bk4wOew4ke.jpg) ![PR_Client_Order_CRUD_Test-images-1](https://hackmd.io/_uploads/SJVPuevE1g.jpg) ![PR_Client_Order_CRUD_Test-images-2](https://hackmd.io/_uploads/HyEPOgv4yx.jpg)