diff --git a/README.md b/README.md index 10b6436..5fad245 100644 --- a/README.md +++ b/README.md @@ -1,196 +1,240 @@ # Eighth Assignment: Restaurant πŸ• -## Overview πŸ‘‹ - -Welcome to your final assignment before the grand project! πŸ½οΈπŸš€ We’re wrapping things up with a full-stack desktop application that simulates a restaurant ordering system. You’ll build it using **JavaFX** for the UI and **PostgreSQL** for persistent data storage. - -You’ll implement essential user flows like **sign-up**, **login**, and **placing orders**, and use **JDBC** for direct interaction with the database. Your journey will bring together everything you've learned in this course β€” from file-based applications to database-backed systems with a graphical interface. - -## Prerequisites βœ… - - Make sure you have installed these programs and services before starting your project: - -- Git -- Java 23 -- Gradle -- PostgreSQL -- Scene Builder -> πŸ’‘ You might need VPN or DNS setting for downloading JavaFX packages using Gradle. -> In case of any major problems with gradle, you can create a new project with maven and copy the readme file in the new project. - -## Objectives ✏️ - -By completing this assignment, you will: - -- Build a **full-stack desktop application** that integrates a graphical user interface with a relational database. - -- Understand how to **connect a Java application to a relational database** using JDBC. - -- Gain hands-on experience in **designing and implementing UI using JavaFX**. - -## Logic - -### Entities - -Your project should **at minimum** include the following entities to properly model a simple restaurant ordering system. The following entities are essential for managing users, menu items, and orders: - ------- - -#### 1. **User** - -Represents a person using the system (e.g., a customer). - -- **Attributes:** - - `id`: Unique identifier - - `username` - - `password` - - `email` *(optional)* -- **Relationships:** - - One **User** can place many **Orders**. - ------- - -#### 2. **MenuItem** - -Represents a single item on the restaurant's menu. - -- **Attributes:** - - `id`: Unique identifier - - `name` - - `description` *(optional)* - - `price` - - `category` *(optional, useful for the bonus part)* -- **Relationships:** - - A **MenuItem** can appear in many **OrderDetails** (i.e., be ordered in many different orders). - - -> **⚠️ There should be at least 3 items in the menu.** - -> **πŸ’‘ You don't have to add a menu item through your app. Inserting items by sql commands in pg admin is enough.** - ------- - -#### 3. **Order** - -Represents a single order placed by a user. - -- **Attributes:** - - `id`: Unique identifier - - `userId`: Reference to the `User` who placed the order - - `createdAt`: Timestamp of when the order was placed - - `totalPrice`: Total cost of the order (can be calculated or stored) -- **Relationships:** - - An **Order** belongs to one **User**. - - An **Order** has many **OrderDetails**, each representing a specific item in the order. - ------- - -#### 4. **OrderDetail** - -Represents a line item in an order (i.e., a specific menu item and its quantity). - -- **Attributes:** - - `id`: Unique identifier - - `orderId`: Reference to the associated `Order` - - `menuItemId`: Reference to the selected `MenuItem` - - `quantity`: Number of units of the menu item ordered - - `price`: Price per unit at the time of ordering -- **Relationships:** - - An **OrderDetail** belongs to one **Order** and one **MenuItem**. - - Multiple **OrderDetails** make up an **Order**. - ------- - -### Entity Relationships Summary - -- A **User** ➝ can have many **Orders**. - -- An **Order** ➝ belongs to one **User**, and has many **OrderDetails**. - -- A **MenuItem** ➝ can appear in many **OrderDetails**. - -- An **OrderDetail** ➝ connects one **Order** to one **MenuItem** with quantity and price. - - -### Scenario -Your project flow should go through the following steps -#### Step 1: Login / Sign-Up: -The customer creates a new account or logs into an existing one to access the restaurant's features. -* Accounts should contain a **Username** and a **Password** - * Having a more detailed account setup is bonus -* Passwords should be hashed - -#### Step 2: Display Menu: -A menu is displayed to the customer consisting of all the foods available in the restaurant. -* Menu Items should include " Food Name + Price " - * Including the used ingredients is bonus - -#### Step 3: Order Food: -The customer selects desired items and adds them to the cart for checkout. -* The customer should be able to add or delete selected foods from their order. - -#### Step 4: Checkout -After the order is placed, a detailed receipt is shown with itemized costs and confirmation of the transaction. -* The receipt has to have the following information: - 1. Names of the ordered food items - 2. Quantity of each item - 3. Price per unit of each item - 4. Total price for each item (unit price Γ— quantity) - 5. Total price of the order -* After the confirmation, the order should be saved to the database - -### UI -The User Interface of your application should include the features listed below (note that this is just a suggested UI implementation and your app can have any design of your choice) -#### 1. Login / Sign-Up page: -* A text field and a password field for logging in -* A text field and two password fields for signing up (two pass fields for verifying the password) -* Note that errors for common problems such as the ones listed below should be displayed to the user: - * "Username unavailable" in login - * "Incorrect Password" in login - * "Username is already taken" in sign up - * etc. -#### 2. Ordering page: -* On the left side of the screen a list of the customer's currently selected food - * The customer must have the ability to increase or decrease the number of selected items - * Also include a "Checkout" button under the order so the customer can end their selection -* On the right side, the menu of the restaurant - * The menu items should be buttons so the customer can select them - * It can be implemented as a simple table showing the food and its price or by using small components like bubbles that get added to the page -* Implementing a "Back" button is bonus so that the user can return to the login page -#### 3. Checkout page: -* As explained in the last section, display each of the necessary information either in a table or any other way you see fit -* Implement a "Set Order" button to finalize the order and add it to the database -* Display something like "Thanks for your purchase :)" message to the customer -* Give the options to order again or leave to the login/sign-up pages and implement the functionality for each button correspondingly -## Bonus - -* Smooth and consistent navigation, allowing users to move forward and backward between pages without breaking the app’s functionality. -* Include food categories (e.g., Fast Food, Traditional, Italian, etc.) to organize the menu and help users find items more easily. -* The ability to view the order history of a customer. -* Using images for displaying menu items. -* Being able to add more menu items as a restaurant admin. - -## Evaluation πŸ“ƒ - -Your submission will be evaluated based on the following criteria: - -- **Database Design and Integration (PostgreSQL)** - You must design your database schema properly with clear relationships between entities (as explained in the Logic section). The application should interact with the PostgreSQL database to: - - Store and retrieve user data - - Save and update orders and their details - - Load menu items dynamically - - Include your initial sql codes as a separate file in the project -- **User Interface (JavaFX)** - The UI should be clean, user-friendly, and visually organized. Use JavaFX effectively to implement different screens (e.g., login/signup, ordering, order summary). -- **Code Quality and Structure** - Your code should be clean, modular, and follow object-oriented principles. Use appropriate naming, structure your classes and packages well, and avoid redundant or duplicate code. - ------- - -> ⚠️ **Important:** Use of ChatGPT or any AI-based generative tools for completing any part of the assignment is **strictly prohibited**. Detection will result in an automatic **score of 0** with no warnings. - -## Submission - -The deadline for submitting your code is **Wednesday, June 4th (Khordad 14th)**. -Good luck! +## πŸ‘‹ Overview + +Welcome to the Restaurant Ordering System\! This project is a full-stack desktop application developed as the final assignment for the course. It simulates a restaurant's ordering process, allowing users to sign up, log in, browse a menu, place orders, and view their order history. Administrators have the ability to manage menu items. + +The application features a Graphical User Interface (GUI) built with **JavaFX** and utilizes a **PostgreSQL** database for persistent data storage. Direct database interaction is handled via **JDBC**. This project brings together concepts from file-based applications to database-backed systems with a graphical interface. + +----- + +## πŸ’» Technologies Used + +* **Programming Language:** Java (Targeting JDK 24, compatible with recent versions like 17/21+) +* **User Interface (UI):** JavaFX +* **Database:** PostgreSQL +* **Database Connectivity:** JDBC (Java Database Connectivity) +* **Build Tool:** Gradle +* **UI Design Tool (Recommended):** Scene Builder + +----- + +## βœ… Prerequisites + +Before running or developing this project, ensure you have the following installed: + +* **Git:** For version control (cloning the project). +* **Java JDK:** Version 24 (as specified in project description) or a compatible version (e.g., JDK 17, 21). +* **Gradle:** For building the project and managing dependencies. The project includes a Gradle Wrapper (`gradlew`) for consistent builds. +* **PostgreSQL:** The relational database server. +* **pgAdmin (or similar SQL client):** For managing the PostgreSQL database and running the initial schema script. +* **Scene Builder (Optional but Recommended):** For visually designing or inspecting the JavaFX FXML files. + +----- + +## ✏️ Objectives + +This project aims to: + +* Build a **full-stack desktop application** integrating a GUI with a relational database. +* Demonstrate understanding of connecting a Java application to a relational database using **JDBC**. +* Provide hands-on experience in designing and implementing a UI using **JavaFX**. +* Implement core application logic for user management, menu display, order processing, and data persistence. + +----- + +## πŸš€ Project Setup & Running Instructions + +### 1\. Database Setup + +* **Ensure PostgreSQL Server is Running:** Start your PostgreSQL server instance. +* **Create a Database:** + * Using pgAdmin or another SQL client, connect to your PostgreSQL server. + * Create a new database (e.g., name it `restaurant_db`). +* **Run the Schema Script:** + * Locate the `schema.sql` file in the `sql/` directory of this project. + * Open the Query Tool for your newly created `restaurant_db`. + * Copy the entire content of `schema.sql` and execute it. This will create all necessary tables (`User`, `MenuItem`, `Order`, `OrderDetail`) and insert sample menu items. +* **Configure Database Connection in Java:** + * Open the file: `src/main/java/ap/restaurant/restaurant/util/DatabaseUtil.java`. + * Update the following constants with your actual PostgreSQL connection details: + ```java + private static final String DB_URL = "jdbc:postgresql://localhost:5432/your_database_name"; // Replace your_database_name + private static final String DB_USER = "your_db_user"; // Replace your_db_user + private static final String DB_PASSWORD = "your_db_password"; // Replace your_db_password + ``` + +### 2\. Building & Running the Application + +* **Clone the Project (if applicable):** + ```bash + git clone + cd Eighth-Assignment-Restaurant + ``` +* **Using Gradle (Recommended):** + * Open a terminal or command prompt in the root directory of the project. + * Make the Gradle wrapper executable (on Linux/macOS, one-time setup): + ```bash + chmod +x ./gradlew + ``` + * Run the application: + ```bash + ./gradlew run # On Linux/macOS + gradlew run # On Windows + ``` + * The first run might take longer as Gradle downloads dependencies. +* **Running from IntelliJ IDEA (or similar IDE):** + 1. Open the project in IntelliJ (File -\> Open -\> navigate to the project's root folder). + 2. Trust the project and allow IntelliJ to import/sync with Gradle. This will download dependencies. + 3. Ensure the Project SDK is correctly set (e.g., JDK 24). + 4. Ensure IntelliJ is configured to use Gradle from the `'gradle-wrapper.properties' file` (Settings -\> Build, Execution, Deployment -\> Build Tools -\> Gradle). + 5. Locate the main class `ap.restaurant.restaurant.HelloApplication`. + 6. You can either run the `main` method directly from IntelliJ or use the run configuration automatically created by the Gradle `application` plugin (usually found in the run configurations dropdown). + +----- + +## πŸ›οΈ System Architecture & Design Choices + +This project follows a layered architecture, loosely based on the Model-View-Controller (MVC) pattern, to organize its components and responsibilities. + +* **Model:** + * Represented by the Java classes in the `ap.restaurant.restaurant.model` package (`User.java`, `MenuItem.java`, `Order.java`, `OrderDetail.java`). + * These are Plain Old Java Objects (POJOs) that encapsulate the application's data structure, mirroring the database entities. +* **View:** + * Implemented using **JavaFX FXML**. Files are located in `src/main/resources/ap/restaurant/restaurant/`. + * FXML provides a declarative way to define the user interface structure, separating it from the application logic. Scene Builder can be used to design these views. +* **Controller (JavaFX Controllers):** + * Located in the `ap.restaurant.restaurant.controller` package (e.g., `LoginSignUpController.java`, `OrderingController.java`). + * These classes act as the bridge between the View (FXML) and the Model/Business Logic. They handle user interactions from the FXML views, process input, and update the views accordingly. +* **Data Access Objects (DAO):** + * Found in the `ap.restaurant.restaurant.dao` package (e.g., `UserDAO.java`, `MenuItemDAO.java`, `OrderDAO.java`). + * These classes encapsulate all database interaction logic using **JDBC**. They provide CRUD (Create, Read, Update, Delete) operations for the model entities, abstracting the database specifics from the controllers. +* **Utility Classes:** + * The `ap.restaurant.restaurant.util` package contains helper classes: + * `DatabaseUtil.java`: Manages JDBC connections to the PostgreSQL database. + * `PasswordUtil.java`: Handles password hashing (using SHA-256 for this project) and verification. For a production environment, stronger algorithms like BCrypt or SCrypt with unique salts per user would be recommended. +* **Main Application Class:** + * `HelloApplication.java` is the entry point for the JavaFX application. It manages scene switching and initializes the primary stage. + +### Database Design Rationale: + +* **Normalization:** The database schema is designed with normalization principles in mind to reduce data redundancy and improve data integrity. + * `User`, `MenuItem`, and `Order` are distinct entities. + * The `OrderDetail` table serves as a join table to model the many-to-many relationship between `Order` and `MenuItem`, also capturing the quantity and price per unit at the time of the order (crucial for historical accuracy if menu prices change). +* **Primary and Foreign Keys:** Enforce relationships and ensure referential integrity. +* **Data Types:** Chosen to appropriately represent the data (e.g., `DECIMAL` for prices, `TIMESTAMP WITH TIME ZONE` for order creation). +* **Indexes:** Added on foreign key columns to improve query performance for fetching orders and order details. +* **Cascade Rules:** `ON DELETE CASCADE` is used judiciously (e.g., deleting a user also deletes their orders) while `ON DELETE RESTRICT` protects `MenuItem`s from being deleted if they are part of historical orders. + +----- + +## ✨ Core Features Implemented + +* **User Authentication:** + * **Sign-Up:** New users can create an account with a username and password. Passwords are **hashed** before being stored in the database. Email is optional. + * **Login:** Existing users can log in with their credentials. The system differentiates between regular users and administrators. +* **Menu Display:** + * A dynamic menu is displayed to the customer, loading items from the `MenuItem` table in the database. + * Menu items show at least "Food Name + Price". +* **Food Ordering & Cart Management:** + * Customers can select desired menu items and add them to their virtual cart. + * Quantities of items in the cart can be increased or decreased. + * Items can be removed from the cart. + * The total price of the cart is updated dynamically. +* **Checkout Process:** + * After finalizing their selections, customers proceed to checkout. + * A detailed receipt/order summary is shown, including: + * Names of ordered food items. + * Quantity of each item. + * Price per unit of each item. + * Total price for each item (unit price Γ— quantity). + * Total price of the entire order. + * Upon confirmation, the order (including `Order` and `OrderDetail` records) is saved to the PostgreSQL database. +* **Error Handling:** + * User-facing error messages are provided for common issues like incorrect login credentials, username already taken, empty fields, etc. + +----- + +## ⭐ Bonus Features Implemented + +1. **Smooth and Consistent Navigation:** + + * The application allows users to move between different pages (Login/Sign-Up, Ordering, Checkout, Order History, Admin) through dedicated buttons and logical flows managed by `HelloApplication.java`. + +2. **Food Categories:** + + * Menu items can be assigned categories (e.g., "Pizza", "Burgers", "Drinks"). + * The ordering page includes a `ComboBox` allowing customers to filter the displayed menu items by category, enhancing usability. + +3. **View Order History:** + + * Logged-in customers can access a dedicated page to view their past orders. + * Each order in the history displays its ID, creation date, total price, and a detailed breakdown of the items included (name, quantity, price per unit, item total). Orders are displayed in an `Accordion` for a clean presentation. + +4. **Using Images for Displaying Menu Items:** + + * The `MenuItem` entity includes an `image_url` field. + * The ordering page attempts to load and display an image for each menu item. + * A placeholder image (`src/main/resources/images/placeholder.png`) is used if an item's specific image is not found or the URL is invalid. + * **To fully utilize this:** Actual image files need to be placed in the `src/main/resources/images/` directory, and the `image_url` in the database for `MenuItem` records must correctly point to these files (e.g., `images/margherita.png`). + +5. **Admin: Add/Manage Menu Items:** + + * Users can be designated as administrators (via the `is_admin` flag in the `User` table, settable during sign-up). + * Admin users are directed to a "Menu Management" page after login. + * This page allows admins to: + * View all current menu items in a table. + * Add new menu items by providing name, price, category, description, and image URL. + * Select an existing item from the table to populate an edit form. + * Update the details of selected menu items. + * Delete menu items (with a confirmation dialog). + * The form includes a `ComboBox` for categories that is populated with existing categories and also allows typing in new ones. + +----- + +## πŸ“ Code Structure + +The project follows a standard Maven/Gradle directory structure: + +* **`src/main/java/ap/restaurant/restaurant/`**: Contains all Java source code. + * **`controller/`**: JavaFX controller classes that manage UI logic. + * **`dao/`**: Data Access Object classes for database interactions. + * **`model/`**: Entity classes representing the data model. + * **`util/`**: Utility classes (e.g., `DatabaseUtil`, `PasswordUtil`). + * **`HelloApplication.java`**: The main entry point for the JavaFX application. + * **`module-info.java`**: Java Platform Module System module descriptor. +* **`src/main/resources/ap/restaurant/restaurant/`**: Contains FXML files for UI layout and other resources. + * **`images/`**: (To be created by the user) For storing menu item images. +* **`sql/`**: Contains the database schema setup script (`schema.sql`). +* **`build.gradle`**: Gradle build script for managing dependencies and build tasks. + +----- + +## πŸ’‘ Potential Future Enhancements + +* **Stronger Password Hashing:** Implement BCrypt or SCrypt with unique salts per user for production-level security. +* **Enhanced Input Validation:** More comprehensive client-side and server-side validation. +* **Detailed Order Status:** (e.g., "Preparing", "Out for Delivery", "Delivered") - would require schema changes. +* **User Profile Management:** Allow users to update their email or password. +* **More Sophisticated UI/UX:** Animations, custom styling beyond default JavaFX. +* **Reporting/Analytics:** For admin users (e.g., popular items, sales reports). +* **Unit and Integration Testing:** Implement JUnit tests for DAOs, services, and controllers. + +----- + +## πŸ“ƒ Notes on Evaluation Criteria (Self-Assessment) + +* **Database Design and Integration (PostgreSQL):** + * The database schema is designed with clear relationships as per the project logic section. + * The application interacts with PostgreSQL via JDBC to store/retrieve user data, save/update orders and details, and load menu items dynamically. + * The initial SQL code is provided as a separate file. +* **User Interface (JavaFX):** + * The UI is designed to be clean, user-friendly, and organized. + * JavaFX is used effectively to implement different screens and functionalities as described. +* **Code Quality and Structure:** + * The code is organized into logical packages (model, dao, controller, util). + * Object-oriented principles are applied. + * Efforts were made to keep code modular and avoid redundancy. + +----- diff --git a/build.gradle b/build.gradle index e8bdf58..21fc658 100644 --- a/build.gradle +++ b/build.gradle @@ -1,56 +1,58 @@ plugins { id 'java' - id 'application' - id 'org.javamodularity.moduleplugin' version '1.8.12' - id 'org.openjfx.javafxplugin' version '0.0.13' - id 'org.beryx.jlink' version '2.25.0' + id 'application' // For easily running the application + id 'org.openjfx.javafxplugin' version '0.1.0' // Or 0.0.13 if 0.1.0 causes issues } -group 'AP.Restaurant' +group 'ap.restaurant' version '1.0-SNAPSHOT' repositories { mavenCentral() } -ext { - junitVersion = '5.10.2' +java { + // Set to Java 24 as requested + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 } -sourceCompatibility = '23' -targetCompatibility = '23' +javafx { + version = "24" + modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.graphics' ] +} + +dependencies { + implementation 'org.postgresql:postgresql:42.7.3' + + testImplementation platform('org.junit:junit-bom:5.10.2') + testImplementation 'org.junit.jupiter:junit-jupiter' -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' } application { mainModule = 'ap.restaurant.restaurant' mainClass = 'ap.restaurant.restaurant.HelloApplication' -} -javafx { - version = '17.0.6' - modules = ['javafx.controls', 'javafx.fxml'] + applicationDefaultJvmArgs = ['--enable-native-access=javafx.graphics'] } -dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") +run { + + jvmArgs += [ + '--module-path', classpath.asPath, + '--add-modules', application.mainModule.get() + "," + javafx.modules.join(',') + ] + } -test { -useJUnitPlatform()} -jlink { - imageZip = project.file("${buildDir}/distributions/app-${javafx.platform.classifier}.zip") - options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages'] - launcher { - name = 'app' - } +tasks.withType(JavaCompile) { + options.compilerArgs += "-Xlint:unchecked" + options.encoding = 'UTF-8' } -jlinkZip { - group = 'distribution' -} \ No newline at end of file +test { + useJUnitPlatform() +} diff --git a/db_setup/schema.sql b/db_setup/schema.sql new file mode 100644 index 0000000..30672af --- /dev/null +++ b/db_setup/schema.sql @@ -0,0 +1,84 @@ +-- Drop tables if they exist to start fresh (optional, use with caution) +DROP TABLE IF EXISTS OrderDetail CASCADE; +DROP TABLE IF EXISTS "Order" CASCADE; -- "Order" is a keyword, so use quotes +DROP TABLE IF EXISTS MenuItem CASCADE; +DROP TABLE IF EXISTS "User" CASCADE; -- "User" is a keyword, so use quotes + +-- User Table +CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, -- Store hashed passwords + email VARCHAR(255), -- Optional + is_admin BOOLEAN DEFAULT FALSE -- For bonus admin feature +); + +-- MenuItem Table +CREATE TABLE MenuItem ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, -- Optional + price DECIMAL(10, 2) NOT NULL, + category VARCHAR(100), -- Optional, for bonus + image_url VARCHAR(512) -- Optional, for bonus image feature +); + +-- Order Table +CREATE TABLE "Order" ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + total_price DECIMAL(10, 2) NOT NULL, + CONSTRAINT fk_user + FOREIGN KEY(user_id) + REFERENCES "User"(id) + ON DELETE CASCADE -- If a user is deleted, their orders are also deleted +); + +-- OrderDetail Table +CREATE TABLE OrderDetail ( + id SERIAL PRIMARY KEY, + order_id INT NOT NULL, + menu_item_id INT NOT NULL, + quantity INT NOT NULL, + price_per_unit DECIMAL(10, 2) NOT NULL, -- Price at the time of order + CONSTRAINT fk_order + FOREIGN KEY(order_id) + REFERENCES "Order"(id) + ON DELETE CASCADE, -- If an order is deleted, its details are also deleted + CONSTRAINT fk_menu_item + FOREIGN KEY(menu_item_id) + REFERENCES MenuItem(id) + ON DELETE RESTRICT -- Prevent deleting a menu item if it's part of an order detail +); + +-- Indexes for better performance on foreign keys +CREATE INDEX idx_order_user_id ON "Order"(user_id); +CREATE INDEX idx_orderdetail_order_id ON OrderDetail(order_id); +CREATE INDEX idx_orderdetail_menu_item_id ON OrderDetail(menu_item_id); + +-- Sample Menu Items (added category and a placeholder for image_url) +INSERT INTO MenuItem (name, description, price, category, image_url) VALUES +('Margherita Pizza', 'Classic cheese and tomato pizza', 12.99, 'Pizza', 'images/margherita.png'), +('Pepperoni Pizza', 'Pizza with pepperoni topping', 14.99, 'Pizza', 'images/pepperoni.png'), +('Cheeseburger', 'Beef patty with cheese, lettuce, and tomato', 9.50, 'Burgers', 'images/cheeseburger.png'), +('Veggie Burger', 'Plant-based patty with lettuce and tomato', 8.75, 'Burgers', 'images/veggie_burger.png'), +('Caesar Salad', 'Romaine lettuce, croutons, Parmesan cheese, and Caesar dressing', 7.25, 'Salads', 'images/caesar_salad.png'), +('Spaghetti Carbonara', 'Pasta with eggs, cheese, pancetta, and pepper', 15.50, 'Pasta', 'images/carbonara.png'), +('Coca-Cola', 'Classic cola drink', 2.00, 'Drinks', 'images/coke.png'), +('Orange Juice', 'Freshly squeezed orange juice', 3.50, 'Drinks', 'images/orange_juice.png'), +('Chicken Alfredo', 'Creamy Alfredo pasta with grilled chicken', 16.00, 'Pasta', 'images/chicken_alfredo.png'), +('French Fries', 'Crispy golden french fries', 4.50, 'Sides', 'images/fries.png'); + +-- Sample User (for testing - password is 'password123') +-- The actual hashing will be done by PasswordUtil in Java. +-- INSERT INTO "User" (username, password_hash, email, is_admin) VALUES +-- ('testuser', 'hashed_password_for_password123', 'test@example.com', FALSE), +-- ('admin', 'hashed_password_for_adminpass', 'admin@example.com', TRUE); + +COMMENT ON TABLE "User" IS 'Represents a person using the system (e.g., a customer or admin).'; +COMMENT ON COLUMN "User".is_admin IS 'Flag to identify administrator users.'; +COMMENT ON TABLE MenuItem IS 'Represents a single item on the restaurant''s menu.'; +COMMENT ON COLUMN MenuItem.image_url IS 'URL or path to an image for the menu item.'; +COMMENT ON TABLE "Order" IS 'Represents a single order placed by a user.'; +COMMENT ON TABLE OrderDetail IS 'Represents a line item in an order.'; diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0d18421..ca025c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 1b6c787..23d15a9 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -133,22 +133,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/ap/restaurant/restaurant/HelloApplication.java b/src/main/java/ap/restaurant/restaurant/HelloApplication.java index 6fc62a1..463079b 100644 --- a/src/main/java/ap/restaurant/restaurant/HelloApplication.java +++ b/src/main/java/ap/restaurant/restaurant/HelloApplication.java @@ -1,23 +1,176 @@ package ap.restaurant.restaurant; +import ap.restaurant.restaurant.controller.LoginSignUpController; +import ap.restaurant.restaurant.controller.OrderingController; +import ap.restaurant.restaurant.controller.CheckoutController; +import ap.restaurant.restaurant.controller.OrderHistoryController; +import ap.restaurant.restaurant.controller.AdminMenuManagementController; + +import ap.restaurant.restaurant.model.Order; +import ap.restaurant.restaurant.model.User; import javafx.application.Application; import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.stage.Modality; import javafx.stage.Stage; import java.io.IOException; +import java.net.URL; public class HelloApplication extends Application { + + private Stage primaryStage; + private User currentUser; + @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 320, 240); - stage.setTitle("Hello!"); - stage.setScene(scene); - stage.show(); + this.primaryStage = stage; + this.primaryStage.setTitle("Restaurant Ordering System"); + showLoginSignUpPage(); + } + + public void showLoginSignUpPage() throws IOException { + FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource("login-signup-view.fxml")); + Parent root = loader.load(); + LoginSignUpController controller = loader.getController(); + controller.setMainApp(this); + Scene scene = new Scene(root, 420, 480); + primaryStage.setScene(scene); + primaryStage.show(); + } + + public void showOrderingPage() throws IOException { + FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource("ordering-view.fxml")); + Parent root = loader.load(); + OrderingController controller = loader.getController(); + controller.setMainApp(this); + controller.setCurrentUser(this.currentUser); + controller.loadInitialData(); + Scene scene = new Scene(root, 950, 700); + primaryStage.setScene(scene); + primaryStage.setTitle("Order Food - " + (currentUser != null ? currentUser.getUsername() : "Guest")); + primaryStage.show(); + } + + public void showCheckoutPage(Order order) throws IOException { + FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource("checkout-view.fxml")); + Parent root = loader.load(); + CheckoutController controller = loader.getController(); + controller.setMainApp(this); + controller.setCurrentUser(this.currentUser); + controller.setCurrentOrder(order); + controller.displayOrderDetails(); + Scene scene = new Scene(root, 550, 700); + primaryStage.setScene(scene); + primaryStage.setTitle("Checkout - " + (currentUser != null ? currentUser.getUsername() : "Guest")); + primaryStage.show(); } + public void showOrderHistoryPage() throws IOException { + System.out.println("[HelloApplication] Attempting to show Order History page..."); + if (currentUser == null) { + System.err.println("[HelloApplication] Cannot show order history: No user logged in. CurrentUser is null."); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(primaryStage); + alert.setTitle("Error"); + alert.setHeaderText("Login Required"); + alert.setContentText("You must be logged in to view order history."); + alert.showAndWait(); + return; + } + System.out.println("[HelloApplication] Current User for Order History: " + currentUser.getUsername() + " (ID: " + currentUser.getId() + ")"); + + Parent root = null; + OrderHistoryController controller = null; + try { + URL fxmlLocation = HelloApplication.class.getResource("order-history-view.fxml"); + if (fxmlLocation == null) { + System.err.println("[HelloApplication] FXML file 'order-history-view.fxml' not found!"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(primaryStage); + alert.setTitle("Application Error"); + alert.setHeaderText("Cannot Load Page"); + alert.setContentText("The Order History page (order-history-view.fxml) could not be found. Please contact support."); + alert.showAndWait(); + return; + } + System.out.println("[HelloApplication] FXML location found: " + fxmlLocation); + + FXMLLoader loader = new FXMLLoader(fxmlLocation); + root = loader.load(); + System.out.println("[HelloApplication] FXML 'order-history-view.fxml' loaded successfully."); + + controller = loader.getController(); + if (controller == null) { + System.err.println("[HelloApplication] Controller for Order History is null. Check fx:controller in FXML."); + return; + } + System.out.println("[HelloApplication] OrderHistoryController instance obtained."); + + } catch (IOException e) { + System.err.println("[HelloApplication] IOException while loading order-history-view.fxml or its controller!"); + e.printStackTrace(); // Print the full stack trace of the FXML loading error + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(primaryStage); + alert.setTitle("Application Error"); + alert.setHeaderText("Error Loading Page"); + alert.setContentText("An error occurred while trying to load the Order History page:\n" + e.getMessage()); + alert.showAndWait(); + return; // Stop further execution if FXML loading fails + } + + try { + controller.setMainApp(this); + System.out.println("[HelloApplication] MainApp set on OrderHistoryController."); + + controller.setCurrentUser(this.currentUser); + System.out.println("[HelloApplication] CurrentUser set on OrderHistoryController."); + + controller.loadOrderHistory(); + System.out.println("[HelloApplication] loadOrderHistory() called on OrderHistoryController."); + } catch (Exception e) { + System.err.println("[HelloApplication] Exception during OrderHistoryController setup or loadOrderHistory()!"); + e.printStackTrace(); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(primaryStage); + alert.setTitle("Application Error"); + alert.setHeaderText("Error Processing Order History"); + alert.setContentText("An error occurred while preparing the order history data:\n" + e.getMessage()); + alert.showAndWait(); + return; // Stop if there's an error preparing data + } + + Scene scene = new Scene(root, 750, 550); + primaryStage.setScene(scene); + primaryStage.setTitle("Order History - " + currentUser.getUsername()); + primaryStage.show(); + System.out.println("[HelloApplication] Order History page scene set and shown."); + } + + public void showAdminPage() throws IOException { + if (currentUser == null || !currentUser.isAdmin()) { + System.err.println("Access Denied: User is not an admin or not logged in."); + return; + } + FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource("admin-menu-management-view.fxml")); + Parent root = loader.load(); + AdminMenuManagementController controller = loader.getController(); + controller.setMainApp(this); + controller.loadMenuItems(); + + Scene scene = new Scene(root, 800, 600); + primaryStage.setScene(scene); + primaryStage.setTitle("Admin - Menu Management"); + primaryStage.show(); + } + + public User getCurrentUser() { return currentUser; } + public void setCurrentUser(User currentUser) { this.currentUser = currentUser; } + public Stage getPrimaryStage() { return primaryStage; } + public static void main(String[] args) { - launch(); + launch(args); } -} \ No newline at end of file +} diff --git a/src/main/java/ap/restaurant/restaurant/Util/DatabaseUtil.java b/src/main/java/ap/restaurant/restaurant/Util/DatabaseUtil.java new file mode 100644 index 0000000..d9ab312 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/Util/DatabaseUtil.java @@ -0,0 +1,35 @@ +package ap.restaurant.restaurant.Util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DatabaseUtil { + private static final String DB_URL = "jdbc:postgresql://localhost:5432/restaurant_db"; + private static final String DB_USER = "amirmac"; + private static final String DB_PASSWORD = "Amirrzh84"; + + static { + try { + Class.forName("org.postgresql.Driver"); + } catch (ClassNotFoundException e) { + System.err.println("PostgreSQL JDBC Driver not found. Ensure it's in your project dependencies."); + e.printStackTrace(); + throw new RuntimeException("Failed to load PostgreSQL driver", e); + } + } + + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + } + + public static void closeConnection(Connection connection) { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + System.err.println("Error closing database connection: " + e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/Util/PasswordUtil.java b/src/main/java/ap/restaurant/restaurant/Util/PasswordUtil.java new file mode 100644 index 0000000..3cc99f1 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/Util/PasswordUtil.java @@ -0,0 +1,27 @@ +package ap.restaurant.restaurant.Util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class PasswordUtil { + + // Using SHA-256 for simplicity as per project description. + public static String hashPassword(String password) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hashedBytes = md.digest(password.getBytes()); + return Base64.getEncoder().encodeToString(hashedBytes); + } catch (NoSuchAlgorithmException e) { + System.err.println("Error hashing password: SHA-256 algorithm not found."); + throw new RuntimeException("Error hashing password", e); + } + + } + + public static boolean verifyPassword(String plainPassword, String hashedPassword) { + String newHash = hashPassword(plainPassword); + return newHash.equals(hashedPassword); + + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/controller/AdminMenuManagementController.java b/src/main/java/ap/restaurant/restaurant/controller/AdminMenuManagementController.java new file mode 100644 index 0000000..3921e5a --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/controller/AdminMenuManagementController.java @@ -0,0 +1,237 @@ +package ap.restaurant.restaurant.controller; + +import ap.restaurant.restaurant.HelloApplication; +import ap.restaurant.restaurant.dao.MenuItemDAO; +import ap.restaurant.restaurant.model.MenuItem; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.paint.Color; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +public class AdminMenuManagementController { + + @FXML private TableView menuItemTableView; + @FXML private TableColumn colItemId; + @FXML private TableColumn colItemName; + @FXML private TableColumn colItemPrice; + @FXML private TableColumn colItemCategory; + @FXML private TableColumn colItemDescription; + @FXML private TableColumn colItemImageUrl; + + @FXML private TextField txtItemId; + @FXML private TextField txtItemName; + @FXML private TextField txtItemPrice; + @FXML private ComboBox comboItemCategory; + @FXML private TextArea txtItemDescription; + @FXML private TextField txtItemImageUrl; + + @FXML private Button btnAddItem; + @FXML private Button btnUpdateItem; + @FXML private Button btnDeleteItem; + @FXML private Button btnClearForm; + @FXML private Button adminLogoutButton; + @FXML private Label adminMessageLabel; + + private HelloApplication mainApp; + private MenuItemDAO menuItemDAO; + private ObservableList menuItemsList = FXCollections.observableArrayList(); + + public AdminMenuManagementController() { + this.menuItemDAO = new MenuItemDAO(); + } + + public void setMainApp(HelloApplication mainApp) { + this.mainApp = mainApp; + } + + @FXML + public void initialize() { + colItemId.setCellValueFactory(new PropertyValueFactory<>("id")); + colItemName.setCellValueFactory(new PropertyValueFactory<>("name")); + colItemPrice.setCellValueFactory(new PropertyValueFactory<>("price")); + colItemPrice.setCellFactory(tc -> new TableCell() { + @Override + protected void updateItem(BigDecimal price, boolean empty) { + super.updateItem(price, empty); + setText(empty || price == null ? null : String.format("$%.2f", price)); + } + }); + colItemCategory.setCellValueFactory(new PropertyValueFactory<>("category")); + colItemDescription.setCellValueFactory(new PropertyValueFactory<>("description")); + colItemImageUrl.setCellValueFactory(new PropertyValueFactory<>("imageUrl")); + + menuItemTableView.setItems(menuItemsList); + + populateCategoryComboBox(); + + menuItemTableView.getSelectionModel().selectedItemProperty().addListener( + (obs, oldSelection, newSelection) -> { + if (newSelection != null) { + populateForm(newSelection); + } + } + ); + txtItemId.setDisable(true); // ID is not directly editable for new items + } + + private void populateCategoryComboBox() { + List categories = menuItemDAO.getAllCategories(); + comboItemCategory.setItems(FXCollections.observableArrayList(categories)); + } + + + public void loadMenuItems() { + menuItemsList.setAll(menuItemDAO.getAllMenuItems()); + menuItemTableView.refresh(); + populateCategoryComboBox(); // Refresh categories in case new ones were added + } + + private void populateForm(MenuItem menuItem) { + txtItemId.setText(String.valueOf(menuItem.getId())); + txtItemName.setText(menuItem.getName()); + txtItemPrice.setText(menuItem.getPrice().toString()); + comboItemCategory.setValue(menuItem.getCategory()); + txtItemDescription.setText(menuItem.getDescription()); + txtItemImageUrl.setText(menuItem.getImageUrl()); + txtItemId.setDisable(false); // Enable ID field when editing an existing item + } + + @FXML + private void handleAddItem() { + adminMessageLabel.setText(""); + try { + String name = txtItemName.getText(); + BigDecimal price = new BigDecimal(txtItemPrice.getText()); + String category = comboItemCategory.getValue(); // Allows new category if typed + String description = txtItemDescription.getText(); + String imageUrl = txtItemImageUrl.getText(); + + if (name.isEmpty() || price.compareTo(BigDecimal.ZERO) <= 0) { + setAdminMessage("Name and a valid Price are required.", true); + return; + } + + MenuItem newItem = new MenuItem(name, description, price, category, imageUrl); + if (menuItemDAO.addMenuItem(newItem)) { + setAdminMessage("Item added successfully! ID: " + newItem.getId(), false); + loadMenuItems(); + handleClearForm(); + } else { + setAdminMessage("Failed to add item.", true); + } + } catch (NumberFormatException e) { + setAdminMessage("Invalid price format. Please enter a number.", true); + } catch (Exception e) { + setAdminMessage("Error adding item: " + e.getMessage(), true); + e.printStackTrace(); + } + } + + @FXML + private void handleUpdateItem() { + adminMessageLabel.setText(""); + MenuItem selectedItem = menuItemTableView.getSelectionModel().getSelectedItem(); + if (selectedItem == null && txtItemId.getText().isEmpty()) { + setAdminMessage("Please select an item from the list to update or ensure ID is present.", true); + return; + } + + try { + int id = selectedItem != null ? selectedItem.getId() : Integer.parseInt(txtItemId.getText()); + String name = txtItemName.getText(); + BigDecimal price = new BigDecimal(txtItemPrice.getText()); + String category = comboItemCategory.getValue(); + String description = txtItemDescription.getText(); + String imageUrl = txtItemImageUrl.getText(); + + if (name.isEmpty() || price.compareTo(BigDecimal.ZERO) <= 0) { + setAdminMessage("Name and a valid Price are required for update.", true); + return; + } + + MenuItem updatedItem = new MenuItem(name, description, price, category, imageUrl); + updatedItem.setId(id); // Set ID for update + + if (menuItemDAO.updateMenuItem(updatedItem)) { + setAdminMessage("Item updated successfully!", false); + loadMenuItems(); + handleClearForm(); + } else { + setAdminMessage("Failed to update item. Ensure item ID is correct.", true); + } + } catch (NumberFormatException e) { + setAdminMessage("Invalid ID or Price format.", true); + } catch (Exception e) { + setAdminMessage("Error updating item: " + e.getMessage(), true); + e.printStackTrace(); + } + } + + @FXML + private void handleDeleteItem() { + adminMessageLabel.setText(""); + MenuItem selectedItem = menuItemTableView.getSelectionModel().getSelectedItem(); + if (selectedItem == null) { + setAdminMessage("Please select an item to delete.", true); + return; + } + + Alert confirmationDialog = new Alert(Alert.AlertType.CONFIRMATION, + "Are you sure you want to delete '" + selectedItem.getName() + "'?", + ButtonType.YES, ButtonType.NO); + confirmationDialog.setTitle("Confirm Deletion"); + confirmationDialog.setHeaderText(null); + + Optional result = confirmationDialog.showAndWait(); + if (result.isPresent() && result.get() == ButtonType.YES) { + if (menuItemDAO.deleteMenuItem(selectedItem.getId())) { + setAdminMessage("Item deleted successfully!", false); + loadMenuItems(); + handleClearForm(); + } else { + // DAO's deleteMenuItem should print specific error for FK constraint + setAdminMessage("Failed to delete item. It might be part of existing orders.", true); + } + } + } + + @FXML + private void handleClearForm() { + txtItemId.clear(); + txtItemId.setDisable(true); // Disable ID field for new item entry + txtItemName.clear(); + txtItemPrice.clear(); + comboItemCategory.setValue(null); // Clear ComboBox selection + comboItemCategory.getEditor().clear(); // Clear typed text in editable ComboBox + txtItemDescription.clear(); + txtItemImageUrl.clear(); + menuItemTableView.getSelectionModel().clearSelection(); + adminMessageLabel.setText(""); + } + + @FXML + private void handleAdminLogout() { + try { + if (mainApp != null) { + mainApp.setCurrentUser(null); // Clear admin user + mainApp.showLoginSignUpPage(); + } + } catch (IOException e) { + setAdminMessage("Error during logout: " + e.getMessage(), true); + e.printStackTrace(); + } + } + + private void setAdminMessage(String message, boolean isError) { + adminMessageLabel.setText(message); + adminMessageLabel.setTextFill(isError ? Color.RED : Color.GREEN); + } +} diff --git a/src/main/java/ap/restaurant/restaurant/controller/CheckoutController.java b/src/main/java/ap/restaurant/restaurant/controller/CheckoutController.java new file mode 100644 index 0000000..4a033f4 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/controller/CheckoutController.java @@ -0,0 +1,171 @@ +package ap.restaurant.restaurant.controller; + +import ap.restaurant.restaurant.HelloApplication; +import ap.restaurant.restaurant.dao.MenuItemDAO; // Not strictly needed if OrderDetail has name +import ap.restaurant.restaurant.dao.OrderDAO; +import ap.restaurant.restaurant.model.Order; +import ap.restaurant.restaurant.model.OrderDetail; +import ap.restaurant.restaurant.model.User; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.OffsetDateTime; + +public class CheckoutController { + + @FXML private Label checkoutUserLabel; + @FXML private TableView checkoutTableView; + @FXML private TableColumn itemNameColumn; + @FXML private TableColumn quantityColumn; + @FXML private TableColumn pricePerUnitColumn; + @FXML private TableColumn totalItemPriceColumn; + @FXML private Label finalTotalLabel; + @FXML private Button setOrderButton; + @FXML private Label checkoutMessageLabel; + @FXML private VBox orderDetailsPane; + @FXML private VBox postOrderActionsPane; + + private HelloApplication mainApp; + private Order currentOrder; + private User currentUser; + private OrderDAO orderDAO; + + private ObservableList orderDetailDisplays = FXCollections.observableArrayList(); + + public CheckoutController() { + this.orderDAO = new OrderDAO(); + } + + public void setMainApp(HelloApplication mainApp) { this.mainApp = mainApp; } + public void setCurrentOrder(Order order) { this.currentOrder = order; } + public void setCurrentUser(User user) { + this.currentUser = user; + if (currentUser != null) { + checkoutUserLabel.setText("Order for: " + currentUser.getUsername()); + } + } + + @FXML + public void initialize() { + itemNameColumn.setCellValueFactory(new PropertyValueFactory<>("menuItemName")); + quantityColumn.setCellValueFactory(new PropertyValueFactory<>("quantity")); + pricePerUnitColumn.setCellValueFactory(new PropertyValueFactory<>("formattedPricePerUnit")); + totalItemPriceColumn.setCellValueFactory(new PropertyValueFactory<>("formattedTotalItemPrice")); + checkoutTableView.setItems(orderDetailDisplays); + + postOrderActionsPane.setVisible(false); + postOrderActionsPane.setManaged(false); + } + + public void displayOrderDetails() { + if (currentOrder == null) { + checkoutMessageLabel.setTextFill(Color.RED); + checkoutMessageLabel.setText("Error: No order details to display."); + setOrderButton.setDisable(true); + return; + } + + orderDetailDisplays.clear(); + for (OrderDetail detail : currentOrder.getOrderDetails()) { + orderDetailDisplays.add(new OrderDetailDisplay( + detail.getMenuItemName() != null ? detail.getMenuItemName() : "Item ID: " + detail.getMenuItemId(), + detail.getQuantity(), + detail.getPricePerUnit() + )); + } + finalTotalLabel.setText(String.format("$%.2f", currentOrder.getTotalPrice())); + setOrderButton.setDisable(false); + checkoutMessageLabel.setText(""); // Clear any previous messages + } + + @FXML + private void handleSetOrder() { + if (currentOrder == null || currentUser == null) { + checkoutMessageLabel.setTextFill(Color.RED); + checkoutMessageLabel.setText("Error: Cannot place order. Missing information."); + return; + } + + currentOrder.setUserId(currentUser.getId()); // Ensure user ID is set + currentOrder.setCreatedAt(OffsetDateTime.now()); // Finalize creation time + + if (orderDAO.saveOrder(currentOrder)) { + checkoutMessageLabel.setTextFill(Color.GREEN); + checkoutMessageLabel.setText("Thank you! Your order #" + currentOrder.getId() + " has been placed successfully."); + orderDetailsPane.setVisible(false); + orderDetailsPane.setManaged(false); + postOrderActionsPane.setVisible(true); + postOrderActionsPane.setManaged(true); + } else { + checkoutMessageLabel.setTextFill(Color.RED); + checkoutMessageLabel.setText("Failed to place your order. Please try again."); + } + } + + @FXML + private void handleOrderAgain() { + try { + if (mainApp != null) mainApp.showOrderingPage(); + } catch (IOException e) { + showError("Error loading ordering page: " + e.getMessage()); + } + } + + @FXML + private void handleViewOrderHistory() { + try { + if (mainApp != null) mainApp.showOrderHistoryPage(); + } catch (IOException e) { + showError("Error loading order history: " + e.getMessage()); + } + } + + @FXML + private void handleLogout() { + try { + if (mainApp != null) { + mainApp.setCurrentUser(null); + mainApp.showLoginSignUpPage(); + } + } catch (IOException e) { + showError("Error during logout: " + e.getMessage()); + } + } + + private void showError(String message) { + checkoutMessageLabel.setTextFill(Color.RED); + checkoutMessageLabel.setText(message); + System.err.println(message); + } + + // Inner class for TableView display + public static class OrderDetailDisplay { + private final String menuItemName; + private final int quantity; + private final BigDecimal pricePerUnit; + + public OrderDetailDisplay(String menuItemName, int quantity, BigDecimal pricePerUnit) { + this.menuItemName = menuItemName; + this.quantity = quantity; + this.pricePerUnit = pricePerUnit; + } + + public String getMenuItemName() { return menuItemName; } + public int getQuantity() { return quantity; } + public BigDecimal getPricePerUnit() { return pricePerUnit; } // Raw value + public String getFormattedPricePerUnit() { return String.format("$%.2f", pricePerUnit); } + public BigDecimal getTotalItemPriceRaw() { return pricePerUnit.multiply(new BigDecimal(quantity)); } + public String getFormattedTotalItemPrice() { return String.format("$%.2f", getTotalItemPriceRaw()); } + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/controller/LoginSignUpController.java b/src/main/java/ap/restaurant/restaurant/controller/LoginSignUpController.java new file mode 100644 index 0000000..7b743a3 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/controller/LoginSignUpController.java @@ -0,0 +1,135 @@ +package ap.restaurant.restaurant.controller; + +import ap.restaurant.restaurant.HelloApplication; +import ap.restaurant.restaurant.dao.UserDAO; +import ap.restaurant.restaurant.model.User; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.paint.Color; + +import java.io.IOException; + +public class LoginSignUpController { + + @FXML private TextField loginUsernameField; + @FXML private PasswordField loginPasswordField; + @FXML private Button loginButton; + @FXML private Label loginErrorLabel; + + @FXML private TextField signUpUsernameField; + @FXML private PasswordField signUpPasswordField; + @FXML private PasswordField signUpConfirmPasswordField; + @FXML private TextField signUpEmailField; + @FXML private CheckBox signUpAdminCheckBox; + @FXML private Button signUpButton; + @FXML private Label signUpErrorLabel; + @FXML private Label signUpSuccessLabel; + + private UserDAO userDAO; + private HelloApplication mainApp; + + public LoginSignUpController() { + this.userDAO = new UserDAO(); + } + + public void setMainApp(HelloApplication mainApp) { + this.mainApp = mainApp; + } + + @FXML + private void handleLogin() { + String username = loginUsernameField.getText().trim(); + String password = loginPasswordField.getText(); + loginErrorLabel.setText(""); + + if (username.isEmpty() || password.isEmpty()) { + loginErrorLabel.setText("Username and password cannot be empty."); + return; + } + + if (userDAO.verifyUserCredentials(username, password)) { + User loggedInUser = userDAO.findByUsername(username).orElse(null); + if (loggedInUser != null && mainApp != null) { + mainApp.setCurrentUser(loggedInUser); + loginErrorLabel.setTextFill(Color.GREEN); + loginErrorLabel.setText("Login successful!"); + + try { + if (loggedInUser.isAdmin()) { + mainApp.showAdminPage(); + } else { + mainApp.showOrderingPage(); + } + } catch (IOException e) { + loginErrorLabel.setTextFill(Color.RED); + loginErrorLabel.setText("Error loading next page: " + e.getMessage()); + e.printStackTrace(); + } + } else { + loginErrorLabel.setText("Login failed. User data issue after validation."); + } + } else { + if (userDAO.findByUsername(username).isPresent()) { + loginErrorLabel.setText("Incorrect password."); + } else { + loginErrorLabel.setText("Username not found."); + } + } + } + + @FXML + private void handleSignUp() { + String username = signUpUsernameField.getText().trim(); + String password = signUpPasswordField.getText(); + String confirmPassword = signUpConfirmPasswordField.getText(); + String email = signUpEmailField.getText().trim(); + boolean isAdmin = signUpAdminCheckBox.isSelected(); + + signUpErrorLabel.setText(""); + signUpSuccessLabel.setText(""); + + if (username.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) { + signUpErrorLabel.setText("Username and password fields are required."); + return; + } + if (username.length() < 3) { + signUpErrorLabel.setText("Username must be at least 3 characters."); + return; + } + if (password.length() < 6) { + signUpErrorLabel.setText("Password must be at least 6 characters."); + return; + } + if (!password.equals(confirmPassword)) { + signUpErrorLabel.setText("Passwords do not match."); + return; + } + if (userDAO.findByUsername(username).isPresent()) { + signUpErrorLabel.setText("Username is already taken."); + return; + } + // Basic email validation + if (!email.isEmpty() && !email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) { + signUpErrorLabel.setText("Invalid email format."); + return; + } + + User newUser = new User(); + newUser.setUsername(username); + newUser.setPasswordHash(password); // DAO will hash this plain password + newUser.setEmail(email.isEmpty() ? null : email); + newUser.setAdmin(isAdmin); + + if (userDAO.createUser(newUser)) { + signUpSuccessLabel.setText("Sign up successful! You can now log in."); + // Clear fields + signUpUsernameField.clear(); + signUpPasswordField.clear(); + signUpConfirmPasswordField.clear(); + signUpEmailField.clear(); + signUpAdminCheckBox.setSelected(false); + } else { + signUpErrorLabel.setText("Sign up failed. An unexpected error occurred."); + } + } +} diff --git a/src/main/java/ap/restaurant/restaurant/controller/OrderHistoryController.java b/src/main/java/ap/restaurant/restaurant/controller/OrderHistoryController.java new file mode 100644 index 0000000..59677ab --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/controller/OrderHistoryController.java @@ -0,0 +1,173 @@ +package ap.restaurant.restaurant.controller; + +import ap.restaurant.restaurant.HelloApplication; +import ap.restaurant.restaurant.dao.OrderDAO; +import ap.restaurant.restaurant.model.Order; +import ap.restaurant.restaurant.model.OrderDetail; +import ap.restaurant.restaurant.model.User; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.List; + +public class OrderHistoryController { + + @FXML private Label historyUserLabel; + @FXML private Button backToOrderingButton; + @FXML private Label noOrdersLabel; + @FXML private Accordion orderAccordion; + + private HelloApplication mainApp; + private User currentUser; + private OrderDAO orderDAO; + + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); + + public OrderHistoryController() { + this.orderDAO = new OrderDAO(); + } + + public void setMainApp(HelloApplication mainApp) { this.mainApp = mainApp; } + + public void setCurrentUser(User user) { + this.currentUser = user; + if (currentUser != null) { + historyUserLabel.setText("Order History for: " + currentUser.getUsername()); + } + } + + @FXML + public void initialize() { + orderAccordion.setPadding(new Insets(5)); // Slight padding for the accordion itself + } + + public void loadOrderHistory() { + orderAccordion.getPanes().clear(); + noOrdersLabel.setVisible(false); + + if (currentUser == null) { + noOrdersLabel.setText("Error: User not logged in."); + noOrdersLabel.setVisible(true); + return; + } + + List userOrders = orderDAO.getOrdersByUserId(currentUser.getId()); + + if (userOrders == null || userOrders.isEmpty()) { + noOrdersLabel.setText("No past orders found for " + currentUser.getUsername() + "."); + noOrdersLabel.setVisible(true); + } else { + for (Order order : userOrders) { + orderAccordion.getPanes().add(createOrderTitledPane(order)); + } + if (!orderAccordion.getPanes().isEmpty()) { + orderAccordion.setExpandedPane(orderAccordion.getPanes().get(0)); // Expand first order + } + } + } + + private TitledPane createOrderTitledPane(Order order) { + String title = String.format("Order #%d | %s | Total: $%.2f", + order.getId(), + order.getCreatedAt() != null ? order.getCreatedAt().format(DATETIME_FORMATTER) : "Date N/A", + order.getTotalPrice()); + + TitledPane titledPane = new TitledPane(title, null); + titledPane.setFont(Font.font("System", FontWeight.BOLD, 14)); + titledPane.setStyle("-fx-background-color: #e9ecef; -fx-text-fill: #212529;"); + + + TableView detailsTable = new TableView<>(); + detailsTable.setPrefHeight(180); // Auto-size based on content or set fixed + detailsTable.setStyle("-fx-font-size: 12px;"); + + TableColumn itemNameCol = new TableColumn<>("Item Name"); + itemNameCol.setCellValueFactory(new PropertyValueFactory<>("menuItemName")); + itemNameCol.setPrefWidth(250); + + TableColumn quantityCol = new TableColumn<>("Qty"); + quantityCol.setCellValueFactory(new PropertyValueFactory<>("quantity")); + quantityCol.setPrefWidth(70); + quantityCol.setStyle("-fx-alignment: CENTER;"); + + + TableColumn priceCol = new TableColumn<>("Price/Unit"); + priceCol.setCellValueFactory(new PropertyValueFactory<>("formattedPricePerUnit")); + priceCol.setPrefWidth(120); + priceCol.setStyle("-fx-alignment: CENTER-RIGHT;"); + + TableColumn totalItemPriceCol = new TableColumn<>("Item Total"); + totalItemPriceCol.setCellValueFactory(new PropertyValueFactory<>("formattedTotalItemPrice")); + totalItemPriceCol.setPrefWidth(120); + totalItemPriceCol.setStyle("-fx-alignment: CENTER-RIGHT;"); + + detailsTable.getColumns().addAll(itemNameCol, quantityCol, priceCol, totalItemPriceCol); + detailsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + ObservableList displayableDetails = FXCollections.observableArrayList(); + if (order.getOrderDetails() != null) { + for (OrderDetail detail : order.getOrderDetails()) { + displayableDetails.add(new OrderDetailDisplayWrapper( + detail.getMenuItemName() != null ? detail.getMenuItemName() : "Item ID: " + detail.getMenuItemId(), + detail.getQuantity(), + detail.getPricePerUnit() + )); + } + } + detailsTable.setItems(displayableDetails); + + VBox contentBox = new VBox(10, detailsTable); + contentBox.setPadding(new Insets(15)); + contentBox.setStyle("-fx-background-color: #ffffff;"); + titledPane.setContent(contentBox); + return titledPane; + } + + @FXML + private void handleBackToOrdering() { + try { + if (mainApp != null) mainApp.showOrderingPage(); + } catch (IOException e) { + showErrorAlert("Navigation Error", "Could not load the ordering page: " + e.getMessage()); + } + } + + private void showErrorAlert(String title, String content) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(content); + alert.showAndWait(); + System.err.println(content); // Also log to console + } + + public static class OrderDetailDisplayWrapper { + private final String menuItemName; + private final int quantity; + private final BigDecimal pricePerUnit; + + public OrderDetailDisplayWrapper(String menuItemName, int quantity, BigDecimal pricePerUnit) { + this.menuItemName = menuItemName; + this.quantity = quantity; + this.pricePerUnit = pricePerUnit; + } + + public String getMenuItemName() { return menuItemName; } + public int getQuantity() { return quantity; } + public String getFormattedPricePerUnit() { return String.format("$%.2f", pricePerUnit); } + public BigDecimal getTotalItemPriceRaw() { return pricePerUnit.multiply(new BigDecimal(quantity)); } + public String getFormattedTotalItemPrice() { return String.format("$%.2f", getTotalItemPriceRaw()); } + } +} diff --git a/src/main/java/ap/restaurant/restaurant/controller/OrderingController.java b/src/main/java/ap/restaurant/restaurant/controller/OrderingController.java new file mode 100644 index 0000000..3f95473 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/controller/OrderingController.java @@ -0,0 +1,376 @@ +package ap.restaurant.restaurant.controller; + +import ap.restaurant.restaurant.HelloApplication; +import ap.restaurant.restaurant.dao.MenuItemDAO; +import ap.restaurant.restaurant.model.MenuItem; +import ap.restaurant.restaurant.model.Order; +import ap.restaurant.restaurant.model.OrderDetail; +import ap.restaurant.restaurant.model.User; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.TextAlignment; +import javafx.util.Callback; + + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OrderingController { + + @FXML private Label currentUserLabel; + @FXML private ComboBox categoryComboBox; + @FXML private TilePane menuTilePane; + @FXML private TableView cartTableView; + @FXML private TableColumn cartItemNameColumn; + @FXML private TableColumn cartItemQuantityColumn; + @FXML private TableColumn cartItemPriceColumn; // Changed to String for formatted price + @FXML private TableColumn cartItemRemoveColumn; + @FXML private Label cartTotalLabel; + @FXML private Button checkoutButton; + @FXML private Button logoutButton; + @FXML private Button viewHistoryButton; + @FXML private Label orderingErrorLabel; + + private HelloApplication mainApp; + private User currentUser; + private MenuItemDAO menuItemDAO; + private ObservableList cartItems = FXCollections.observableArrayList(); + private Map cartItemMap = new HashMap<>(); // menuItemId -> CartItemDisplay + + public OrderingController() { + this.menuItemDAO = new MenuItemDAO(); + } + + public void setMainApp(HelloApplication mainApp) { this.mainApp = mainApp; } + + public void setCurrentUser(User user) { + this.currentUser = user; + if (currentUser != null) { + currentUserLabel.setText("Welcome, " + currentUser.getUsername() + "!"); + } else { + currentUserLabel.setText("Welcome, Guest!"); // Fallback + } + } + + @FXML + public void initialize() { + setupCartTableColumns(); + cartTableView.setItems(cartItems); + updateCartTotal(); + } + + public void loadInitialData() { + populateCategoryComboBox(); // Ensure categories are loaded + loadMenuItems(null); // Load all items initially + } + + + private void setupCartTableColumns() { + cartItemNameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); + cartItemQuantityColumn.setCellFactory(param -> new QuantityCell()); // Custom cell for Spinner + cartItemPriceColumn.setCellValueFactory(new PropertyValueFactory<>("formattedUnitPrice")); // Use formatted price + + Callback, TableCell> removeCellFactory = param -> new TableCell<>() { + private final Button btn = new Button("Remove"); + { + btn.setStyle("-fx-background-color: #e74c3c; -fx-text-fill: white; -fx-font-size: 11px; -fx-padding: 3 8 3 8;"); + btn.setOnAction(event -> { + CartItemDisplay item = getTableView().getItems().get(getIndex()); + removeItemFromCart(item.getMenuItemId()); + }); + } + @Override + protected void updateItem(Void item, boolean empty) { + super.updateItem(item, empty); + setGraphic(empty ? null : btn); + } + }; + cartItemRemoveColumn.setCellFactory(removeCellFactory); + } + + private void populateCategoryComboBox() { + List categories = menuItemDAO.getAllCategories(); + categoryComboBox.getItems().clear(); // Clear existing items before adding + categoryComboBox.getItems().add("All Categories"); // Add "All" option + if (categories != null) { + categoryComboBox.getItems().addAll(categories); + } + categoryComboBox.getSelectionModel().selectFirst(); // Default to "All Categories" + } + + @FXML + private void handleCategoryFilter() { + String selectedCategory = categoryComboBox.getSelectionModel().getSelectedItem(); + if ("All Categories".equals(selectedCategory) || selectedCategory == null) { + loadMenuItems(null); + } else { + loadMenuItems(selectedCategory); + } + } + + + private void loadMenuItems(String category) { + menuTilePane.getChildren().clear(); + List menuList; + if (category == null || "All Categories".equals(category)) { + menuList = menuItemDAO.getAllMenuItems(); + } else { + menuList = menuItemDAO.getMenuItemsByCategory(category); + } + + + if (menuList == null || menuList.isEmpty()) { + Label noItemsLabel = new Label(category == null || "All Categories".equals(category) ? "No menu items available." : "No items in category: " + category); + noItemsLabel.setFont(Font.font(16)); + menuTilePane.getChildren().add(noItemsLabel); + return; + } + + for (MenuItem item : menuList) { + menuTilePane.getChildren().add(createMenuItemNode(item)); + } + } + + private VBox createMenuItemNode(MenuItem item) { + VBox itemBox = new VBox(8); + itemBox.setAlignment(Pos.TOP_CENTER); + itemBox.setPrefWidth(220); + itemBox.setPrefHeight(280); + itemBox.setStyle("-fx-background-color: white; -fx-padding: 15; -fx-border-color: #ddd; -fx-border-width: 1; -fx-border-radius: 8; -fx-background-radius: 8; -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 10, 0, 2, 2);"); + + ImageView itemImageView = new ImageView(); + itemImageView.setFitHeight(100); + itemImageView.setFitWidth(150); + itemImageView.setPreserveRatio(true); + if (item.getImageUrl() != null && !item.getImageUrl().isEmpty()) { + try { + String imagePath = item.getImageUrl(); + Image image = new Image(getClass().getResourceAsStream("/" + imagePath)); + if (image.isError()) { + System.err.println("Error loading image: " + imagePath + " - " + image.getException().getMessage()); + image = new Image(getClass().getResourceAsStream("/images/placeholder.png")); // Fallback + } + itemImageView.setImage(image); + } catch (Exception e) { + System.err.println("Cannot load image: " + item.getImageUrl() + " - " + e.getMessage()); + try { + itemImageView.setImage(new Image(getClass().getResourceAsStream("/images/placeholder.png"))); + } catch (Exception ex) { + System.err.println("Cannot load placeholder image." + ex.getMessage()); + } + } + } else { + try { + itemImageView.setImage(new Image(getClass().getResourceAsStream("/images/placeholder.png"))); + } catch (Exception ex) { + System.err.println("Cannot load placeholder image." + ex.getMessage()); + } + } + + + Label nameLabel = new Label(item.getName()); + nameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + nameLabel.setTextAlignment(TextAlignment.CENTER); + nameLabel.setWrapText(true); + + Label priceLabel = new Label(String.format("$%.2f", item.getPrice())); + priceLabel.setFont(Font.font("Arial", FontWeight.SEMI_BOLD, 14)); + priceLabel.setTextFill(Color.valueOf("#e74c3c")); + + Label descLabel = new Label(item.getDescription() != null ? item.getDescription() : ""); + descLabel.setFont(Font.font("Arial", 12)); + descLabel.setWrapText(true); + descLabel.setMaxHeight(60); + VBox.setVgrow(descLabel, Priority.ALWAYS); + + + Button addButton = new Button("Add to Cart"); + addButton.setStyle("-fx-background-color: #5cb85c; -fx-text-fill: white; -fx-font-size: 13px; -fx-padding: 8 15; -fx-background-radius: 5;"); + addButton.setOnAction(event -> addItemToCart(item)); + + itemBox.getChildren().addAll(itemImageView, nameLabel, priceLabel, descLabel, addButton); + return itemBox; + } + + + private void addItemToCart(MenuItem menuItem) { + orderingErrorLabel.setText(""); + CartItemDisplay cartItem = cartItemMap.get(menuItem.getId()); + if (cartItem != null) { + cartItem.setQuantity(cartItem.getQuantity() + 1); + } else { + cartItem = new CartItemDisplay(menuItem.getId(), menuItem.getName(), 1, menuItem.getPrice()); + cartItems.add(cartItem); + cartItemMap.put(menuItem.getId(), cartItem); + } + cartTableView.refresh(); + updateCartTotal(); + } + + private void updateItemQuantityInCart(int menuItemId, int newQuantity) { + if (newQuantity <= 0) { + removeItemFromCart(menuItemId); + } else { + CartItemDisplay cartItem = cartItemMap.get(menuItemId); + if (cartItem != null) { + cartItem.setQuantity(newQuantity); + cartTableView.refresh(); + updateCartTotal(); + } + } + } + + private void removeItemFromCart(int menuItemId) { + CartItemDisplay itemToRemove = cartItemMap.remove(menuItemId); + if (itemToRemove != null) { + cartItems.remove(itemToRemove); + } + updateCartTotal(); + } + + private void updateCartTotal() { + BigDecimal total = BigDecimal.ZERO; + for (CartItemDisplay item : cartItems) { + total = total.add(item.getTotalPrice()); + } + cartTotalLabel.setText(String.format("$%.2f", total)); + checkoutButton.setDisable(cartItems.isEmpty()); + } + + @FXML + private void handleCheckout() { + orderingErrorLabel.setText(""); + if (cartItems.isEmpty()) { + orderingErrorLabel.setText("Your cart is empty. Add items to proceed."); + return; + } + if (currentUser == null) { + orderingErrorLabel.setText("Error: User not logged in. Please logout and login."); + return; + } + + Order currentOrder = new Order(); + currentOrder.setUserId(currentUser.getId()); + BigDecimal orderTotal = BigDecimal.ZERO; + + for (CartItemDisplay cartDisp : cartItems) { + OrderDetail detail = new OrderDetail(); + detail.setMenuItemId(cartDisp.getMenuItemId()); + detail.setQuantity(cartDisp.getQuantity()); + detail.setPricePerUnit(cartDisp.getUnitPrice()); + currentOrder.addOrderDetail(detail); + orderTotal = orderTotal.add(cartDisp.getTotalPrice()); + } + currentOrder.setTotalPrice(orderTotal); + + try { + if (mainApp != null) { + mainApp.showCheckoutPage(currentOrder); + } + } catch (IOException e) { + orderingErrorLabel.setText("Error loading checkout page: " + e.getMessage()); + e.printStackTrace(); + } + } + + @FXML + private void handleViewOrderHistory() { + try { + if (mainApp != null) { + mainApp.showOrderHistoryPage(); + } + } catch (IOException e) { + orderingErrorLabel.setText("Error loading order history: " + e.getMessage()); + e.printStackTrace(); + } + } + + @FXML + private void handleLogout() { + cartItems.clear(); + cartItemMap.clear(); + updateCartTotal(); + if (mainApp != null) { + mainApp.setCurrentUser(null); + try { + mainApp.showLoginSignUpPage(); + } catch (IOException e) { + orderingErrorLabel.setText("Error during logout: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + // Inner class for cart TableView display + public static class CartItemDisplay { + private final int menuItemId; + private final String name; + private int quantity; + private final BigDecimal unitPrice; + + public CartItemDisplay(int menuItemId, String name, int quantity, BigDecimal unitPrice) { + this.menuItemId = menuItemId; + this.name = name; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public int getMenuItemId() { return menuItemId; } + public String getName() { return name; } + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } + public BigDecimal getUnitPrice() { return unitPrice; } + public String getFormattedUnitPrice() { return String.format("$%.2f", unitPrice); } + public BigDecimal getTotalPrice() { return unitPrice.multiply(new BigDecimal(quantity)); } + } + + // Custom TableCell for quantity Spinner + private class QuantityCell extends TableCell { + private final Spinner spinner = new Spinner<>(); + public QuantityCell() { + SpinnerValueFactory valueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 99, 1); + spinner.setValueFactory(valueFactory); + spinner.setEditable(true); + spinner.setPrefWidth(70); + spinner.getEditor().setAlignment(Pos.CENTER_RIGHT); + + spinner.valueProperty().addListener((obs, oldValue, newValue) -> { + if (getTableRow() != null && getTableRow().getItem() != null) { + CartItemDisplay cartItem = getTableRow().getItem(); + if (cartItem != null && newValue != null && !newValue.equals(oldValue)) { + updateItemQuantityInCart(cartItem.getMenuItemId(), newValue); + } + } + }); + } + @Override + protected void updateItem(Integer itemQuantity, boolean empty) { + super.updateItem(itemQuantity, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setGraphic(null); + } else { + // It's important to set the spinner's value based on the current item's quantity + spinner.getValueFactory().setValue(((CartItemDisplay) getTableRow().getItem()).getQuantity()); + setGraphic(spinner); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/dao/MenuItemDAO.java b/src/main/java/ap/restaurant/restaurant/dao/MenuItemDAO.java new file mode 100644 index 0000000..17163e2 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/dao/MenuItemDAO.java @@ -0,0 +1,145 @@ +package ap.restaurant.restaurant.dao; + +import ap.restaurant.restaurant.model.MenuItem; +import ap.restaurant.restaurant.util.DatabaseUtil; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class MenuItemDAO { + + public List getAllMenuItems() { + List menuItems = new ArrayList<>(); + String sql = "SELECT * FROM MenuItem ORDER BY category, name"; // Order by category then name + try (Connection conn = DatabaseUtil.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + menuItems.add(mapRowToMenuItem(rs)); + } + } catch (SQLException e) { + System.err.println("Error fetching all menu items: " + e.getMessage()); + } + return menuItems; + } + + public List getAllCategories() { + List categories = new ArrayList<>(); + String sql = "SELECT DISTINCT category FROM MenuItem WHERE category IS NOT NULL ORDER BY category"; + try (Connection conn = DatabaseUtil.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + categories.add(rs.getString("category")); + } + } catch (SQLException e) { + System.err.println("Error fetching categories: " + e.getMessage()); + } + return categories; + } + + public List getMenuItemsByCategory(String category) { + List menuItems = new ArrayList<>(); + String sql = "SELECT * FROM MenuItem WHERE category = ? ORDER BY name"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, category); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + menuItems.add(mapRowToMenuItem(rs)); + } + } catch (SQLException e) { + System.err.println("Error fetching menu items by category '" + category + "': " + e.getMessage()); + } + return menuItems; + } + + + public Optional findMenuItemById(int id) { + String sql = "SELECT * FROM MenuItem WHERE id = ?"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, id); + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + return Optional.of(mapRowToMenuItem(rs)); + } + } catch (SQLException e) { + System.err.println("Error finding menu item by ID " + id + ": " + e.getMessage()); + } + return Optional.empty(); + } + + // For admin bonus feature + public boolean addMenuItem(MenuItem item) { + String sql = "INSERT INTO MenuItem (name, description, price, category, image_url) VALUES (?, ?, ?, ?, ?)"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + pstmt.setString(1, item.getName()); + pstmt.setString(2, item.getDescription()); + pstmt.setBigDecimal(3, item.getPrice()); + pstmt.setString(4, item.getCategory()); + pstmt.setString(5, item.getImageUrl()); + + int affectedRows = pstmt.executeUpdate(); + if (affectedRows > 0) { + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + item.setId(generatedKeys.getInt(1)); + return true; + } + } + } + } catch (SQLException e) { + System.err.println("Error adding menu item: " + e.getMessage()); + } + return false; + } + + public boolean updateMenuItem(MenuItem item) { + String sql = "UPDATE MenuItem SET name = ?, description = ?, price = ?, category = ?, image_url = ? WHERE id = ?"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, item.getName()); + pstmt.setString(2, item.getDescription()); + pstmt.setBigDecimal(3, item.getPrice()); + pstmt.setString(4, item.getCategory()); + pstmt.setString(5, item.getImageUrl()); + pstmt.setInt(6, item.getId()); + return pstmt.executeUpdate() > 0; + } catch (SQLException e) { + System.err.println("Error updating menu item ID " + item.getId() + ": " + e.getMessage()); + } + return false; + } + + public boolean deleteMenuItem(int itemId) { + String sql = "DELETE FROM MenuItem WHERE id = ?"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, itemId); + return pstmt.executeUpdate() > 0; + } catch (SQLException e) { + // Check for constraint violation if item is in an order detail + if (e.getSQLState().equals("23503")) { // Foreign key violation + System.err.println("Error deleting menu item ID " + itemId + ": Item is part of existing orders and cannot be deleted."); + } else { + System.err.println("Error deleting menu item ID " + itemId + ": " + e.getMessage()); + } + } + return false; + } + + private MenuItem mapRowToMenuItem(ResultSet rs) throws SQLException { + MenuItem item = new MenuItem(); + item.setId(rs.getInt("id")); + item.setName(rs.getString("name")); + item.setDescription(rs.getString("description")); + item.setPrice(rs.getBigDecimal("price")); + item.setCategory(rs.getString("category")); + item.setImageUrl(rs.getString("image_url")); + return item; + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/dao/OrderDAO.java b/src/main/java/ap/restaurant/restaurant/dao/OrderDAO.java new file mode 100644 index 0000000..2c38cdd --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/dao/OrderDAO.java @@ -0,0 +1,135 @@ +package ap.restaurant.restaurant.dao; + +import ap.restaurant.restaurant.model.Order; +import ap.restaurant.restaurant.model.OrderDetail; +import ap.restaurant.restaurant.util.DatabaseUtil; + +import java.sql.*; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +public class OrderDAO { + + public boolean saveOrder(Order order) { + String orderSql = "INSERT INTO \"Order\" (user_id, created_at, total_price) VALUES (?, ?, ?)"; + String orderDetailSql = "INSERT INTO OrderDetail (order_id, menu_item_id, quantity, price_per_unit) VALUES (?, ?, ?, ?)"; + Connection conn = null; + + try { + conn = DatabaseUtil.getConnection(); + conn.setAutoCommit(false); // Start transaction + + // Insert into Order table + try (PreparedStatement orderPstmt = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { + orderPstmt.setInt(1, order.getUserId()); + orderPstmt.setObject(2, order.getCreatedAt() != null ? order.getCreatedAt() : OffsetDateTime.now()); + orderPstmt.setBigDecimal(3, order.getTotalPrice()); + int affectedRows = orderPstmt.executeUpdate(); + + if (affectedRows == 0) { + conn.rollback(); + return false; + } + + try (ResultSet generatedKeys = orderPstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + order.setId(generatedKeys.getInt(1)); // Set the generated ID back to the order object + } else { + conn.rollback(); + return false; + } + } + } + + // Insert into OrderDetail table + try (PreparedStatement detailPstmt = conn.prepareStatement(orderDetailSql)) { + for (OrderDetail detail : order.getOrderDetails()) { + detail.setOrderId(order.getId()); // Ensure OrderDetail has the correct Order ID + detailPstmt.setInt(1, detail.getOrderId()); + detailPstmt.setInt(2, detail.getMenuItemId()); + detailPstmt.setInt(3, detail.getQuantity()); + detailPstmt.setBigDecimal(4, detail.getPricePerUnit()); + detailPstmt.addBatch(); + } + detailPstmt.executeBatch(); + } + + conn.commit(); + return true; + + } catch (SQLException e) { + System.err.println("Error saving order: " + e.getMessage()); + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + System.err.println("Error during rollback: " + ex.getMessage()); + } + } + return false; + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); // Reset auto-commit + } catch (SQLException ex) { + System.err.println("Error resetting auto-commit: " + ex.getMessage()); + } + DatabaseUtil.closeConnection(conn); + } + } + } + + public List getOrdersByUserId(int userId) { + List orders = new ArrayList<>(); + String orderSql = "SELECT * FROM \"Order\" WHERE user_id = ? ORDER BY created_at DESC"; + + Connection conn = null; + try { + conn = DatabaseUtil.getConnection(); + try (PreparedStatement pstmt = conn.prepareStatement(orderSql)) { + pstmt.setInt(1, userId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + Order order = new Order(); + order.setId(rs.getInt("id")); + order.setUserId(rs.getInt("user_id")); + order.setCreatedAt(rs.getObject("created_at", OffsetDateTime.class)); + order.setTotalPrice(rs.getBigDecimal("total_price")); + order.setOrderDetails(getOrderDetailsByOrderId(order.getId(), conn)); + orders.add(order); + } + } + } catch (SQLException e) { + System.err.println("Error fetching orders by user ID " + userId + ": " + e.getMessage()); + } finally { + DatabaseUtil.closeConnection(conn); + } + return orders; + } + + private List getOrderDetailsByOrderId(int orderId, Connection conn) throws SQLException { + List details = new ArrayList<>(); + // Join with MenuItem to get the name directly + String detailSql = "SELECT od.*, mi.name as menu_item_name " + + "FROM OrderDetail od " + + "JOIN MenuItem mi ON od.menu_item_id = mi.id " + + "WHERE od.order_id = ?"; + + try (PreparedStatement pstmt = conn.prepareStatement(detailSql)) { + pstmt.setInt(1, orderId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + OrderDetail detail = new OrderDetail(); + detail.setId(rs.getInt("id")); + detail.setOrderId(rs.getInt("order_id")); + detail.setMenuItemId(rs.getInt("menu_item_id")); + detail.setQuantity(rs.getInt("quantity")); + detail.setPricePerUnit(rs.getBigDecimal("price_per_unit")); + detail.setMenuItemName(rs.getString("menu_item_name")); // Set the joined name + details.add(detail); + } + } + return details; + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/dao/UserDAO.java b/src/main/java/ap/restaurant/restaurant/dao/UserDAO.java new file mode 100644 index 0000000..52fd24f --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/dao/UserDAO.java @@ -0,0 +1,69 @@ +package ap.restaurant.restaurant.dao; + +import ap.restaurant.restaurant.model.User; +import ap.restaurant.restaurant.util.DatabaseUtil; +import ap.restaurant.restaurant.util.PasswordUtil; + +import java.sql.*; +import java.util.Optional; + +public class UserDAO { + + public Optional findByUsername(String username) { + String sql = "SELECT * FROM \"User\" WHERE username = ?"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, username); + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + User user = new User(); + user.setId(rs.getInt("id")); + user.setUsername(rs.getString("username")); + user.setPasswordHash(rs.getString("password_hash")); + user.setEmail(rs.getString("email")); + user.setAdmin(rs.getBoolean("is_admin")); + return Optional.of(user); + } + } catch (SQLException e) { + System.err.println("Error finding user by username: " + e.getMessage()); + } + return Optional.empty(); + } + + public boolean createUser(User user) { + if (findByUsername(user.getUsername()).isPresent()) { + return false; // Username already exists + } + String sql = "INSERT INTO \"User\" (username, password_hash, email, is_admin) VALUES (?, ?, ?, ?)"; + try (Connection conn = DatabaseUtil.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + pstmt.setString(1, user.getUsername()); + // The password passed to User object should be plain text, hashing happens here + pstmt.setString(2, PasswordUtil.hashPassword(user.getPasswordHash())); + pstmt.setString(3, user.getEmail()); + pstmt.setBoolean(4, user.isAdmin()); // Default to false if not set + + int affectedRows = pstmt.executeUpdate(); + if (affectedRows > 0) { + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + user.setId(generatedKeys.getInt(1)); + return true; + } + } + } + } catch (SQLException e) { + System.err.println("Error creating user: " + e.getMessage()); + } + return false; + } + + public boolean verifyUserCredentials(String username, String plainPassword) { + Optional userOpt = findByUsername(username); + if (userOpt.isPresent()) { + User user = userOpt.get(); + return PasswordUtil.verifyPassword(plainPassword, user.getPasswordHash()); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/model/MenuItem.java b/src/main/java/ap/restaurant/restaurant/model/MenuItem.java new file mode 100644 index 0000000..b781032 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/model/MenuItem.java @@ -0,0 +1,40 @@ +package ap.restaurant.restaurant.model; + +import java.math.BigDecimal; + +public class MenuItem { + private int id; + private String name; + private String description; + private BigDecimal price; + private String category; + private String imageUrl; // For bonus image feature + + public MenuItem() {} + + public MenuItem(String name, String description, BigDecimal price, String category, String imageUrl) { + this.name = name; + this.description = description; + this.price = price; + this.category = category; + this.imageUrl = imageUrl; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } + + @Override + public String toString() { + return name + " - $" + price + (category != null ? " (" + category + ")" : ""); + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/model/Order.java b/src/main/java/ap/restaurant/restaurant/model/Order.java new file mode 100644 index 0000000..c184676 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/model/Order.java @@ -0,0 +1,40 @@ +package ap.restaurant.restaurant.model; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +public class Order { + private int id; + private int userId; + private OffsetDateTime createdAt; + private BigDecimal totalPrice; + private List orderDetails; + + public Order() { + this.orderDetails = new ArrayList<>(); + this.createdAt = OffsetDateTime.now(); // Default to current time + } + + // Getters and Setters + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public int getUserId() { return userId; } + public void setUserId(int userId) { this.userId = userId; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + public BigDecimal getTotalPrice() { return totalPrice; } + public void setTotalPrice(BigDecimal totalPrice) { this.totalPrice = totalPrice; } + public List getOrderDetails() { return orderDetails; } + public void setOrderDetails(List orderDetails) { this.orderDetails = orderDetails; } + + public void addOrderDetail(OrderDetail detail) { + this.orderDetails.add(detail); + } + + @Override + public String toString() { + return "Order{id=" + id + ", userId=" + userId + ", createdAt=" + createdAt + ", totalPrice=" + totalPrice + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/model/OrderDetail.java b/src/main/java/ap/restaurant/restaurant/model/OrderDetail.java new file mode 100644 index 0000000..2583c20 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/model/OrderDetail.java @@ -0,0 +1,39 @@ +package ap.restaurant.restaurant.model; + +import java.math.BigDecimal; + +public class OrderDetail { + private int id; + private int orderId; + private int menuItemId; + private int quantity; + private BigDecimal pricePerUnit; + private String menuItemName; + + public OrderDetail() {} + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public int getOrderId() { return orderId; } + public void setOrderId(int orderId) { this.orderId = orderId; } + public int getMenuItemId() { return menuItemId; } + public void setMenuItemId(int menuItemId) { this.menuItemId = menuItemId; } + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } + public BigDecimal getPricePerUnit() { return pricePerUnit; } + public void setPricePerUnit(BigDecimal pricePerUnit) { this.pricePerUnit = pricePerUnit; } + public String getMenuItemName() { return menuItemName; } + public void setMenuItemName(String menuItemName) { this.menuItemName = menuItemName; } + + public BigDecimal getTotalItemPrice() { + if (pricePerUnit == null || quantity <= 0) { + return BigDecimal.ZERO; + } + return pricePerUnit.multiply(new BigDecimal(quantity)); + } + + @Override + public String toString() { + return "OrderDetail{id=" + id + ", orderId=" + orderId + ", menuItemId=" + menuItemId + ", quantity=" + quantity + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/ap/restaurant/restaurant/model/User.java b/src/main/java/ap/restaurant/restaurant/model/User.java new file mode 100644 index 0000000..eaa60e2 --- /dev/null +++ b/src/main/java/ap/restaurant/restaurant/model/User.java @@ -0,0 +1,34 @@ +package ap.restaurant.restaurant.model; + +public class User { + private int id; + private String username; + private String passwordHash; + private String email; + private boolean isAdmin; + + public User() {} + + public User(String username, String passwordHash, String email, boolean isAdmin) { + this.username = username; + this.passwordHash = passwordHash; + this.email = email; + this.isAdmin = isAdmin; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPasswordHash() { return passwordHash; } + public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public boolean isAdmin() { return isAdmin; } + public void setAdmin(boolean admin) { isAdmin = admin; } + + @Override + public String toString() { + return "User{id=" + id + ", username='" + username + '\'' + ", isAdmin=" + isAdmin + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 359f4ee..bae71f8 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,8 +1,18 @@ module ap.restaurant.restaurant { requires javafx.controls; requires javafx.fxml; + requires javafx.graphics; // Added javafx.graphics explicitly + requires java.sql; // For JDBC + requires org.postgresql.jdbc; // For PostgreSQL driver - + // Open packages to JavaFX FXML loader opens ap.restaurant.restaurant to javafx.fxml; + opens ap.restaurant.restaurant.controller to javafx.fxml; + + // Open model package to javafx.base for PropertyValueFactory and other reflective access + opens ap.restaurant.restaurant.model to javafx.base; + exports ap.restaurant.restaurant; + exports ap.restaurant.restaurant.controller; + exports ap.restaurant.restaurant.model; } \ No newline at end of file diff --git a/src/main/resources/ap/restaurant/restaurant/admin-menu-management-view.fxml b/src/main/resources/ap/restaurant/restaurant/admin-menu-management-view.fxml new file mode 100644 index 0000000..1b02c20 --- /dev/null +++ b/src/main/resources/ap/restaurant/restaurant/admin-menu-management-view.fxml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + +