commit dd3e811cdf3a84d3a8672b697f9b2c4f2cb3e6f1 Author: Abiram070207 Date: Thu Apr 9 14:14:57 2026 +0530 Vendor Dashboard updated diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..66d7014d Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6845aa9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ordering-site/* +ordering_site/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e0120650 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 00000000..3b41682a --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..667aaef0 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..c595b009 --- /dev/null +++ b/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip diff --git a/backend/backend_startup.log b/backend/backend_startup.log new file mode 100644 index 00000000..55c48f7f --- /dev/null +++ b/backend/backend_startup.log @@ -0,0 +1,120 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------< com.example:backend >------------------------- +[INFO] Building backend 0.0.1-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] >>> spring-boot:3.5.0:run (default-cli) > test-compile @ backend >>> +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ backend --- +[INFO] Copying 1 resource from src/main/resources to target/classes +[INFO] Copying 0 resource from src/main/resources to target/classes +[INFO] +[INFO] --- compiler:3.14.0:compile (default-compile) @ backend --- +[INFO] Recompiling the module because of added or removed source files. +[INFO] Compiling 21 source files with javac [debug parameters release 17] to target/classes +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ backend --- +[INFO] skip non existing resourceDirectory /Users/sidharth/development/Positeasy clone/backend/src/test/resources +[INFO] +[INFO] --- compiler:3.14.0:testCompile (default-testCompile) @ backend --- +[INFO] Recompiling the module because of changed dependency. +[INFO] Compiling 1 source file with javac [debug parameters release 17] to target/test-classes +[INFO] +[INFO] <<< spring-boot:3.5.0:run (default-cli) < test-compile @ backend <<< +[INFO] +[INFO] +[INFO] --- spring-boot:3.5.0:run (default-cli) @ backend --- +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + + :: Spring Boot :: (v3.5.0) + +2026-04-07T19:05:00.341+05:30 INFO 10378 --- [backend] [ main] c.rit.canteen.sales.BackendApplication : Starting BackendApplication using Java 17.0.18 with PID 10378 (/Users/sidharth/development/Positeasy clone/backend/target/classes started by sidharth in /Users/sidharth/development/Positeasy clone/backend) +2026-04-07T19:05:00.342+05:30 INFO 10378 --- [backend] [ main] c.rit.canteen.sales.BackendApplication : No active profile set, falling back to 1 default profile: "default" +2026-04-07T19:05:00.576+05:30 INFO 10378 --- [backend] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-04-07T19:05:00.602+05:30 INFO 10378 --- [backend] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 16 ms. Found 3 JPA repository interfaces. +2026-04-07T19:05:00.781+05:30 INFO 10378 --- [backend] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2026-04-07T19:05:00.787+05:30 INFO 10378 --- [backend] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-04-07T19:05:00.787+05:30 INFO 10378 --- [backend] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.41] +2026-04-07T19:05:00.810+05:30 INFO 10378 --- [backend] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-04-07T19:05:00.810+05:30 INFO 10378 --- [backend] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 453 ms +2026-04-07T19:05:00.870+05:30 INFO 10378 --- [backend] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-04-07T19:05:00.890+05:30 INFO 10378 --- [backend] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.15.Final +2026-04-07T19:05:00.901+05:30 INFO 10378 --- [backend] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-04-07T19:05:00.989+05:30 INFO 10378 --- [backend] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-04-07T19:05:01.000+05:30 INFO 10378 --- [backend] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-04-07T19:05:01.067+05:30 INFO 10378 --- [backend] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@108b121f +2026-04-07T19:05:01.067+05:30 INFO 10378 --- [backend] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-04-07T19:05:01.083+05:30 WARN 10378 --- [backend] [ main] org.hibernate.orm.deprecation : HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-04-07T19:05:01.089+05:30 INFO 10378 --- [backend] [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info: + Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)'] + Database driver: undefined/unknown + Database version: 18.1 + Autocommit mode: undefined/unknown + Isolation level: undefined/unknown + Minimum pool size: undefined/unknown + Maximum pool size: undefined/unknown +2026-04-07T19:05:01.398+05:30 INFO 10378 --- [backend] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-04-07T19:05:01.487+05:30 INFO 10378 --- [backend] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-04-07T19:05:01.614+05:30 WARN 10378 --- [backend] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning +2026-04-07T19:05:01.779+05:30 INFO 10378 --- [backend] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' +2026-04-07T19:05:01.783+05:30 INFO 10378 --- [backend] [ main] c.rit.canteen.sales.BackendApplication : Started BackendApplication in 1.584 seconds (process running for 1.69) +Hibernate: select count(*) from base_items bi1_0 +Hibernate: select count(*) from products p1_0 +2026-04-07T19:06:48.144+05:30 INFO 10378 --- [backend] [0.0-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' +2026-04-07T19:06:48.146+05:30 INFO 10378 --- [backend] [0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' +2026-04-07T19:06:48.147+05:30 INFO 10378 --- [backend] [0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 where p1_0.id=? +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: update products set active=?,attributes_optional=?,barcode=?,base_price=?,category=?,counter=?,description=?,discount_amount=?,discount_percent=?,has_allergy=?,image_data=?,is_veg=?,name=?,offer_price=?,parcel_charges=?,parcel_not_allowed=?,price=?,product_id=?,session_optional=?,stock=?,tag=? where id=? +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select p1_0.id,p1_0.active,p1_0.attributes_optional,p1_0.barcode,p1_0.base_price,p1_0.category,p1_0.counter,p1_0.description,p1_0.discount_amount,p1_0.discount_percent,p1_0.has_allergy,p1_0.image_data,p1_0.is_veg,p1_0.name,p1_0.offer_price,p1_0.parcel_charges,p1_0.parcel_not_allowed,p1_0.price,p1_0.product_id,p1_0.session_optional,p1_0.stock,p1_0.tag from products p1_0 +Hibernate: select s1_0.product_id,s1_0.id,s1_0.active,s1_0.day_of_week,s1_0.end_time,s1_0.start_time from product_sessions s1_0 where s1_0.product_id=? +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +Hibernate: select bi1_0.id,bi1_0.active,bi1_0.description,bi1_0.name from base_items bi1_0 +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 08:44 min +[INFO] Finished at: 2026-04-07T19:13:43+05:30 +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.5.0:run (default-cli) on project backend: Process terminated with exit code: 137 -> [Help 1] +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException diff --git a/backend/mvnw b/backend/mvnw new file mode 100644 index 00000000..bd8896bf --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 00000000..92450f93 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 00000000..a695789e --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.example + backend + 0.0.1-SNAPSHOT + backend + + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/backend/src/main/java/com/rit/canteen/sales/BackendApplication.java b/backend/src/main/java/com/rit/canteen/sales/BackendApplication.java new file mode 100644 index 00000000..3cb14c90 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/BackendApplication.java @@ -0,0 +1,19 @@ +package com.rit.canteen.sales; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; + +@SpringBootApplication(exclude = { + SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class +}) +@org.springframework.scheduling.annotation.EnableScheduling +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } + +} diff --git a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java new file mode 100644 index 00000000..fab3f20c --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java @@ -0,0 +1,54 @@ +package com.rit.canteen.sales.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(Customizer.withDefaults()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/**").permitAll() + .anyRequest().permitAll() + ); + return http.build(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + // Allow all origins from the network to enable cross-device development/testing + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java new file mode 100644 index 00000000..59de2d85 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java @@ -0,0 +1,24 @@ +package com.rit.canteen.sales.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/BaseItemController.java b/backend/src/main/java/com/rit/canteen/sales/controller/BaseItemController.java new file mode 100644 index 00000000..77f00add --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/BaseItemController.java @@ -0,0 +1,40 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.BaseItem; +import com.rit.canteen.sales.repository.BaseItemRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/base-items") +public class BaseItemController { + + @Autowired + private BaseItemRepository baseItemRepository; + + @GetMapping + public List getAllBaseItems() { + return baseItemRepository.findAll(); + } + + @PostMapping + public BaseItem createBaseItem(@Valid @RequestBody BaseItem baseItem) { + return baseItemRepository.save(baseItem); + } + + @PutMapping("/{id}") + public ResponseEntity updateBaseItem(@PathVariable Long id, @Valid @RequestBody BaseItem baseItemDetails) { + return baseItemRepository.findById(id) + .map(item -> { + item.setName(baseItemDetails.getName()); + item.setDescription(baseItemDetails.getDescription()); + item.setActive(baseItemDetails.isActive()); + return ResponseEntity.ok(baseItemRepository.save(item)); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/DashboardController.java b/backend/src/main/java/com/rit/canteen/sales/controller/DashboardController.java new file mode 100644 index 00000000..69545ba8 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/DashboardController.java @@ -0,0 +1,29 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.GeneralDashboardData; +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.service.DashboardService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + + @Autowired + private DashboardService dashboardService; + + @GetMapping("/stats") + public GeneralDashboardData getStats() { + return dashboardService.getGeneralDashboardData(); + } + + @GetMapping("/recent-orders") + public List getRecentOrders() { + return dashboardService.getRecentOrders(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java new file mode 100644 index 00000000..3cc7a299 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -0,0 +1,215 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.model.OrderItem; +import com.rit.canteen.sales.model.User; +import com.rit.canteen.sales.repository.OrderRepository; +import com.rit.canteen.sales.repository.ProductRepository; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.*; + +@RestController +@RequestMapping("/api/orders") +public class OrderController { + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private com.rit.canteen.sales.service.OrderArchiverService orderArchiverService; + + @GetMapping("/all") + public ResponseEntity getAllOrders( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate, + @RequestParam(required = false) String status, + @RequestParam(required = false) String paymentType, + @RequestParam(required = false) String search, + @RequestParam(defaultValue = "false") boolean archived, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + try { + System.out.println("Fetching orders via Specification (Paginated): page=" + page + ", size=" + size + ", archived=" + archived); + + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // Add archive filter + predicates.add(cb.equal(root.get("isArchived"), archived)); + + if (startDate != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startDate)); + } + if (endDate != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), endDate)); + } + if (status != null && !status.isEmpty()) { + predicates.add(cb.equal(root.get("status"), status)); + } + if (paymentType != null && !paymentType.isEmpty()) { + predicates.add(cb.equal(root.get("paymentMethod"), paymentType)); + } + if (search != null && !search.isEmpty()) { + String searchLower = "%" + search.toLowerCase() + "%"; + Join userJoin = root.join("user", JoinType.LEFT); + + Predicate searchPredicate = cb.or( + cb.like(cb.lower(root.get("displayOrderId")), searchLower), + cb.like(cb.lower(root.get("orderNumber")), searchLower), + cb.like(cb.lower(userJoin.get("name")), searchLower), + cb.like(cb.lower(userJoin.get("mobileNumber")), searchLower) + ); + predicates.add(searchPredicate); + } + + // Fetch join for actual data queries to avoid N+1 + if (query != null && !Long.class.equals(query.getResultType()) && !long.class.equals(query.getResultType())) { + root.fetch("user", JoinType.LEFT); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + Page orderPage = orderRepository.findAll(spec, pageable); + return ResponseEntity.ok(orderPage); + } catch (Exception e) { + System.err.println("Error fetching orders with Specification: " + e.getMessage()); + e.printStackTrace(); + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.status(500).body(error); + } + } + + @PostMapping("/archive-now") + public ResponseEntity triggerArchival() { + try { + int count = orderArchiverService.archiveNow(); + return ResponseEntity.ok(Map.of("success", true, "archivedCount", count)); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); + } + } + + @PostMapping + public ResponseEntity> placeOrder(@RequestBody Order order) { + // Link items to the order for bidirectional relationship + if (order.getItems() != null) { + for (OrderItem item : order.getItems()) { + System.out.println("πŸ›’ RECEIVED ITEM: " + item.getProductName() + " | StallID: " + item.getStallId() + " | StallName: " + item.getStallName()); + item.setOrder(order); + } + } + + // Use the actual creation time or current time for counting + LocalDateTime now = LocalDateTime.now(); + order.setCreatedAt(now); + + // Calculate start of current day to find how many orders placed today + LocalDateTime startOfDay = now.toLocalDate().atStartOfDay(); + long todaysOrderCount = orderRepository.countByCreatedAtGreaterThanEqual(startOfDay); + + // Generate formatted display ID (#001, #002...) resetting daily + String displayId = String.format("%03d", todaysOrderCount + 1); + order.setDisplayOrderId(displayId); + + // Save the complete order first + Order savedOrder = orderRepository.save(order); + + // --- Reduct Stock Logic --- + if (savedOrder.getItems() != null) { + for (OrderItem item : savedOrder.getItems()) { + Long productId = item.getProductId(); + if (productId != null) { + productRepository.findById(productId).ifPresent(product -> { + int currentStock = product.getStock() != null ? product.getStock() : 0; + product.setStock(currentStock - item.getQuantity()); + productRepository.save(product); + System.out.println("Updating Stock for " + product.getName() + ": " + currentStock + " -> " + product.getStock()); + }); + } + } + } + + System.out.println("Placed Daily Order: " + savedOrder.getId() + " -> Display ID: #" + displayId); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("orderNumber", savedOrder.getOrderNumber()); // Secure ID for QR + response.put("displayOrderId", savedOrder.getDisplayOrderId()); // Sequential ID (#001) + response.put("message", "Order placed successfully"); + + return ResponseEntity.ok(response); + } + + @GetMapping("/user/{userId}") + public List getUserOrders(@PathVariable Long userId) { + return orderRepository.findByUserIdOrderByCreatedAtDesc(userId); + } + + @PatchMapping("/{id}/status") + public ResponseEntity updateOrderStatus( + @PathVariable Long id, + @RequestBody Map statusUpdate) { + try { + String newStatus = statusUpdate.get("status"); + if (newStatus == null || newStatus.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Status is required")); + } + + return orderRepository.findById(id) + .map(order -> { + order.setStatus(newStatus.toUpperCase()); + orderRepository.save(order); + return ResponseEntity.ok(Map.of("success", true, "message", "Order status updated to " + newStatus)); + }) + .orElse(ResponseEntity.notFound().build()); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); + } + } + + @PutMapping("/{id}") + public ResponseEntity updateOrder(@PathVariable Long id, @RequestBody Order updatedOrder) { + try { + return orderRepository.findById(id) + .map(existingOrder -> { + // Update basic fields + existingOrder.setTotalAmount(updatedOrder.getTotalAmount()); + existingOrder.setPaymentMethod(updatedOrder.getPaymentMethod()); + + // Clear and replace items for a clean update + existingOrder.getItems().clear(); + if (updatedOrder.getItems() != null) { + for (OrderItem newItem : updatedOrder.getItems()) { + newItem.setOrder(existingOrder); + existingOrder.getItems().add(newItem); + } + } + + Order saved = orderRepository.save(existingOrder); + return ResponseEntity.ok(saved); + }) + .orElse(ResponseEntity.notFound().build()); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); + } + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java new file mode 100644 index 00000000..0c823b6f --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java @@ -0,0 +1,92 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.Product; +import com.rit.canteen.sales.repository.ProductRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/products") +public class ProductController { + + @Autowired + private ProductRepository productRepository; + + @GetMapping + public List getAllProducts() { + return productRepository.findAllWithStalls(); + } + + @GetMapping("/category/{categoryName}") + public List getProductsByCategory(@PathVariable String categoryName) { + return productRepository.findAll().stream() + .filter(p -> categoryName.equals(p.getCategory())) + .toList(); + } + + @PostMapping + public Product createProduct(@Valid @RequestBody Product product) { + return productRepository.save(product); + } + + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable Long id, @Valid @RequestBody Product productDetails) { + return productRepository.findById(id) + .map(product -> { + product.setProductId(productDetails.getProductId()); + product.setName(productDetails.getName()); + product.setCategory(productDetails.getCategory()); + product.setDescription(productDetails.getDescription()); + product.setBasePrice(productDetails.getBasePrice()); + product.setPrice(productDetails.getPrice()); + product.setOfferPrice(productDetails.getOfferPrice()); + product.setDiscountPercent(productDetails.getDiscountPercent()); + product.setDiscountAmount(productDetails.getDiscountAmount()); + product.setCounter(productDetails.getCounter()); + product.setTag(productDetails.getTag()); + product.setParcelCharges(productDetails.getParcelCharges()); + product.setBarcode(productDetails.getBarcode()); + product.setAttributesOptional(productDetails.isAttributesOptional()); + product.setVeg(productDetails.isVeg()); + product.setHasAllergy(productDetails.isHasAllergy()); + product.setParcelNotAllowed(productDetails.isParcelNotAllowed()); + product.setSessionOptional(productDetails.isSessionOptional()); + + product.getSessions().clear(); + if (productDetails.getSessions() != null) { + product.getSessions().addAll(productDetails.getSessions()); + } + + product.setImageData(productDetails.getImageData()); + product.setActive(productDetails.isActive()); + product.setStock(productDetails.getStock()); + return ResponseEntity.ok(productRepository.save(product)); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteProduct(@PathVariable Long id) { + return productRepository.findById(id) + .map(product -> { + productRepository.delete(product); + return ResponseEntity.ok().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @PatchMapping("/{id}/toggle-stock") + public ResponseEntity toggleStock(@PathVariable Long id) { + return productRepository.findById(id) + .map(product -> { + Integer currentStock = product.getStock() != null ? product.getStock() : 0; + product.setStock(currentStock > 0 ? 0 : 99); // Toggle between Out of Stock and In Stock + return ResponseEntity.ok(productRepository.save(product)); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java b/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java new file mode 100644 index 00000000..4d3d45e3 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java @@ -0,0 +1,74 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.PurchaseOrder; +import com.rit.canteen.sales.model.PurchaseOrderHistory; +import com.rit.canteen.sales.model.Vendor; +import com.rit.canteen.sales.service.PurchaseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/purchases") +public class PurchaseController { + + private static final Logger logger = LoggerFactory.getLogger(PurchaseController.class); + + @Autowired + private PurchaseService purchaseService; + + @GetMapping("/orders") + public List getAllOrders() { + return purchaseService.getAllOrders(); + } + + @PostMapping("/orders") + public PurchaseOrder createOrder(@RequestBody PurchaseOrder order) { + logger.info("Received request to create/update order: {}", order.getPurchaseId()); + return purchaseService.createOrder(order); + } + + @GetMapping("/orders/{id}/history") + public List getOrderHistory(@PathVariable Long id) { + return purchaseService.getOrderHistory(id); + } + + @GetMapping("/vendors") + public List getAllVendors() { + return purchaseService.getAllVendors(); + } + + @PostMapping("/vendors") + public ResponseEntity createVendor(@RequestBody Vendor vendor) { + try { + logger.info("Received request to create/update vendor: {}", vendor.getName()); + Vendor savedVendor = purchaseService.createVendor(vendor); + return ResponseEntity.ok(savedVendor); + } catch (Exception e) { + logger.error("Error creating/updating vendor: ", e); + return ResponseEntity.status(500).body("Error: " + e.getMessage()); + } + } + + @DeleteMapping("/vendors/{id}") + public ResponseEntity deleteVendor(@PathVariable Long id) { + purchaseService.deleteVendor(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/orders/{id}") + public ResponseEntity deleteOrder(@PathVariable Long id) { + purchaseService.deleteOrder(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/summary") + public Map getSummary() { + return purchaseService.getPurchaseSummary(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/StallController.java b/backend/src/main/java/com/rit/canteen/sales/controller/StallController.java new file mode 100644 index 00000000..71fdf04f --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/StallController.java @@ -0,0 +1,133 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.BaseItem; +import com.rit.canteen.sales.model.Product; +import com.rit.canteen.sales.model.Stall; +import com.rit.canteen.sales.repository.BaseItemRepository; +import com.rit.canteen.sales.repository.ProductRepository; +import com.rit.canteen.sales.repository.StallRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@RequestMapping("/api/stalls") +public class StallController { + + @Autowired + private StallRepository stallRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private BaseItemRepository baseItemRepository; + + @GetMapping + @Transactional(readOnly = true) + public List getAllStalls() { + return stallRepository.findAll(); + } + + @GetMapping("/active") + @Transactional(readOnly = true) + public List getActiveStalls() { + return stallRepository.findByActiveTrue(); + } + + @GetMapping("/{id}") + @Transactional(readOnly = true) + public ResponseEntity getStallById(@PathVariable Long id) { + return stallRepository.findById(id) + .map(stall -> { + // Start with explicitly linked products + Set allProducts = new HashSet<>(stall.getProducts()); + + // Add products from linked BaseItems (categories) + if (stall.getBaseItems() != null) { + for (BaseItem baseItem : stall.getBaseItems()) { + List categoryProducts = productRepository.findByCategory(baseItem.getName()); + if (categoryProducts != null) { + allProducts.addAll(categoryProducts); + } + } + } + + // Update the products list in the object before returning + // Note: This is transient as the transaction is read-only + stall.setProducts(new ArrayList<>(allProducts)); + + return ResponseEntity.ok(stall); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createStall(@RequestBody Stall stall) { + Stall savedStall = stallRepository.save(stall); + return ResponseEntity.ok(savedStall); + } + + @PutMapping("/{id}") + @Transactional + public ResponseEntity updateStall(@PathVariable Long id, @RequestBody Stall updatedStall) { + return stallRepository.findById(id) + .map(stall -> { + stall.setName(updatedStall.getName()); + stall.setDescription(updatedStall.getDescription()); + if (updatedStall.getImageData() != null) { + stall.setImageData(updatedStall.getImageData()); + } + stall.setActive(updatedStall.isActive()); + stall.setTemporarilyClosed(updatedStall.isTemporarilyClosed()); + stall.setSessionOptional(updatedStall.isSessionOptional()); + + // Update sessions + stall.getSessions().clear(); + if (updatedStall.getSessions() != null) { + stall.getSessions().addAll(updatedStall.getSessions()); + } + + return ResponseEntity.ok(stallRepository.save(stall)); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteStall(@PathVariable Long id) { + return stallRepository.findById(id) + .map(stall -> { + stallRepository.delete(stall); + return ResponseEntity.ok(Map.of("success", true, "message", "Stall deleted successfully")); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @PutMapping("/{id}/items") + @Transactional + public ResponseEntity updateStallItems( + @PathVariable Long id, + @RequestBody Map> itemIds) { + + return stallRepository.findById(id) + .map(stall -> { + if (itemIds.containsKey("productIds")) { + List products = productRepository.findAllById(itemIds.get("productIds")); + stall.setProducts(products); + } + if (itemIds.containsKey("baseItemIds")) { + List baseItems = baseItemRepository.findAllById(itemIds.get("baseItemIds")); + stall.setBaseItems(baseItems); + } + return ResponseEntity.ok(stallRepository.save(stall)); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java b/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java new file mode 100644 index 00000000..e50942ae --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java @@ -0,0 +1,65 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.SystemUser; +import com.rit.canteen.sales.service.SystemUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api/system") +public class SystemAuthController { + + @Autowired + private SystemUserService userService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map credentials) { + String email = credentials.get("email"); + String password = credentials.get("password"); + + Optional user = userService.authenticate(email, password); + + if (user.isPresent()) { + return ResponseEntity.ok(user.get()); + } else { + return ResponseEntity.status(401).body("Invalid credentials"); + } + } + + @GetMapping("/managers") + public ResponseEntity> getManagers() { + return ResponseEntity.ok(userService.getAllManagers()); + } + + @PostMapping("/managers") + public ResponseEntity addManager(@RequestBody SystemUser manager) { + return ResponseEntity.ok(userService.createManager(manager)); + } + + @DeleteMapping("/managers/{id}") + public ResponseEntity deleteManager(@PathVariable Long id) { + userService.deleteManager(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/staff") + public ResponseEntity> getStaff() { + return ResponseEntity.ok(userService.getAllStaff()); + } + + @PostMapping("/staff") + public ResponseEntity addStaff(@RequestBody SystemUser staff) { + return ResponseEntity.ok(userService.createStaff(staff)); + } + + @DeleteMapping("/staff/{id}") + public ResponseEntity deleteStaff(@PathVariable Long id) { + userService.deleteManager(id); // Using existing delete logic + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java new file mode 100644 index 00000000..5751bb4e --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java @@ -0,0 +1,100 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.model.Terminal; +import com.rit.canteen.sales.model.TerminalDTO; +import com.rit.canteen.sales.service.TerminalService; +import com.rit.canteen.sales.repository.TerminalRepository; +import com.rit.canteen.sales.repository.OrderRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api/terminals") +public class TerminalController { + + @Autowired + private TerminalService terminalService; + + @Autowired + private TerminalRepository terminalRepository; + + @Autowired + private OrderRepository orderRepository; + + @GetMapping + public List getAllTerminals() { + return terminalService.getAllTerminals().stream() + .map(t -> new TerminalDTO( + t.getId(), + t.getName(), + t.getLocation(), + "********", + "****" + )) + .toList(); + } + + @PostMapping + public ResponseEntity createTerminal(@RequestBody Terminal terminal) { + Terminal created = terminalService.createTerminal(terminal); + return new ResponseEntity<>(created, HttpStatus.CREATED); + } + + @PostMapping("/{id}/verify-pin") + public ResponseEntity verifyPinAndGetDetails(@PathVariable Long id, @RequestBody Map request) { + String pin = request.get("pin"); + if (terminalService.verifyPin(id, pin)) { + Optional terminal = terminalService.getTerminalById(id); + return ResponseEntity.ok(terminal.get()); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid Security PIN")); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteTerminal(@PathVariable Long id) { + terminalService.deleteTerminal(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/orders/{orderNumber}") + public ResponseEntity getOrderForTerminal( + @PathVariable String orderNumber, + @RequestHeader("X-API-KEY") String apiKey) { + + // 1. Verify API Key + Optional terminal = terminalRepository.findByApiKey(apiKey); + if (terminal.isEmpty()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key")); + } + + // 2. Fetch Order + Optional orderOpt = orderRepository.findByOrderNumber(orderNumber); + if (orderOpt.isPresent()) { + Order order = orderOpt.get(); + + // Check if order is expired/archived + if (order.isArchived()) { + return ResponseEntity.status(HttpStatus.GONE) + .body(Map.of("message", "This order has expired and cannot be processed.")); + } + + // Check if order is already fulfilled + if ("COMPLETED".equalsIgnoreCase(order.getStatus())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", "This order has already been fulfilled and cannot be printed again.")); + } + + return ResponseEntity.ok(order); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found")); + } + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java new file mode 100644 index 00000000..31f9f4d2 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java @@ -0,0 +1,140 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.*; +import com.rit.canteen.sales.service.UserService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/auth") +public class UserController { + + @Autowired + private UserService userService; + + /** + * Check if a user exists by mobile number. + * POST /api/auth/check + */ + @PostMapping("/check") + public ResponseEntity checkUserExists(@Valid @RequestBody LoginRequest request) { + LoginResponse response = userService.checkUserExists(request.getMobileNumber()); + return ResponseEntity.ok(response); + } + + /** + * Register a new user with mobile number, name and PIN. + * POST /api/auth/register + */ + @PostMapping("/register") + public ResponseEntity registerUser(@Valid @RequestBody PinVerificationRequest request) { + LoginResponse response = userService.registerUser(request.getMobileNumber(), request.getName(), request.getPin()); + if (response.isSuccess()) { + return ResponseEntity.ok(response); + } else { + return ResponseEntity.badRequest().body(response); + } + } + + /** + * Verify PIN and login an existing user. + * POST /api/auth/login + */ + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody PinVerificationRequest request) { + LoginResponse response = userService.verifyPinAndLogin(request.getMobileNumber(), request.getPin()); + if (response.isSuccess()) { + return ResponseEntity.ok(response); + } else { + return ResponseEntity.badRequest().body(response); + } + } + + /** + * Logout a user. + * POST /api/auth/logout + */ + @PostMapping("/logout") + public ResponseEntity logout(@Valid @RequestBody LoginRequest request) { + LoginResponse response = userService.logout(request.getMobileNumber()); + if (response.isSuccess()) { + return ResponseEntity.ok(response); + } else { + return ResponseEntity.badRequest().body(response); + } + } + + /** + * Change user PIN. + * POST /api/auth/change-pin + */ + @PostMapping("/change-pin") + public ResponseEntity changePin(@Valid @RequestBody ChangePinRequest request) { + LoginResponse response = userService.changePin(request.getMobileNumber(), request.getCurrentPin(), request.getNewPin()); + if (response.isSuccess()) { + return ResponseEntity.ok(response); + } else { + return ResponseEntity.badRequest().body(response); + } + } + + /** + * Get user details by mobile number. + * GET /api/auth/user/{mobileNumber} + */ + @GetMapping("/user/{mobileNumber}") + public ResponseEntity getUser(@PathVariable String mobileNumber) { + LoginResponse.UserDto userDto = userService.getUserByMobile(mobileNumber); + if (userDto != null) { + return ResponseEntity.ok(userDto); + } else { + return ResponseEntity.notFound().build(); + } + } + + /** + * Get all registered users for administration dashboard. + * GET /api/auth/users + */ + /** + * Get all registered users for administration dashboard. + * GET /api/auth/users + */ + @GetMapping("/users") + public ResponseEntity> getAllUsers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + Page users = userService.getAllUsers(PageRequest.of(page, size)); + return ResponseEntity.ok(users); + } + + /** + * Update user details as an administrator. + * PUT /api/auth/users/{id} + */ + @PutMapping("/users/{id}") + public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) { + LoginResponse.UserDto updated = userService.updateUser(id, request.getName(), request.getMobileNumber(), request.getPin()); + if (updated != null) { + return ResponseEntity.ok(updated); + } else { + return ResponseEntity.notFound().build(); + } + } + + /** + * Delete a user from the system. + * DELETE /api/auth/users/{id} + */ + @DeleteMapping("/users/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/rit/canteen/sales/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..b609da26 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/exception/GlobalExceptionHandler.java @@ -0,0 +1,31 @@ +package com.rit.canteen.sales.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex, WebRequest request) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", ex.getMessage()); + body.put("success", false); + + // Include stack trace first few lines for easier debugging in browser + StackTraceElement[] stackTrace = ex.getStackTrace(); + if (stackTrace.length > 0) { + body.put("details", stackTrace[0].toString()); + } + + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/BaseItem.java b/backend/src/main/java/com/rit/canteen/sales/model/BaseItem.java new file mode 100644 index 00000000..4dfe2e70 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/BaseItem.java @@ -0,0 +1,64 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; + +@Entity +@Table(name = "base_items") +public class BaseItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Item name is required") + private String name; + + private String description; + + private boolean active; + + public BaseItem() {} + + public BaseItem(String name, String description, boolean active) { + this.name = name; + this.description = description; + this.active = active; + } + + public Long getId() { + return id; + } + + public void setId(Long 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 boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/ChangePinRequest.java b/backend/src/main/java/com/rit/canteen/sales/model/ChangePinRequest.java new file mode 100644 index 00000000..299fdff4 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/ChangePinRequest.java @@ -0,0 +1,43 @@ +package com.rit.canteen.sales.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public class ChangePinRequest { + + @NotBlank(message = "Mobile number is required") + private String mobileNumber; + + @NotBlank(message = "Current PIN is required") + @Pattern(regexp = "^[0-9]{6}$", message = "PIN must be 6 digits") + private String currentPin; + + @NotBlank(message = "New PIN is required") + @Pattern(regexp = "^[0-9]{6}$", message = "PIN must be 6 digits") + private String newPin; + + // Getters and Setters + public String getMobileNumber() { + return mobileNumber; + } + + public void setMobileNumber(String mobileNumber) { + this.mobileNumber = mobileNumber; + } + + public String getCurrentPin() { + return currentPin; + } + + public void setCurrentPin(String currentPin) { + this.currentPin = currentPin; + } + + public String getNewPin() { + return newPin; + } + + public void setNewPin(String newPin) { + this.newPin = newPin; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java b/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java new file mode 100644 index 00000000..ad4ccc32 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java @@ -0,0 +1,28 @@ +package com.rit.canteen.sales.model; + +public class DashboardStats { + private long totalSales; + private int activeOrders; + private int dailyCustomers; + private double revenueGrowth; + + public DashboardStats(long totalSales, int activeOrders, int dailyCustomers, double revenueGrowth) { + this.totalSales = totalSales; + this.activeOrders = activeOrders; + this.dailyCustomers = dailyCustomers; + this.revenueGrowth = revenueGrowth; + } + + // Getters and Setters + public long getTotalSales() { return totalSales; } + public void setTotalSales(long totalSales) { this.totalSales = totalSales; } + + public int getActiveOrders() { return activeOrders; } + public void setActiveOrders(int activeOrders) { this.activeOrders = activeOrders; } + + public int getDailyCustomers() { return dailyCustomers; } + public void setDailyCustomers(int dailyCustomers) { this.dailyCustomers = dailyCustomers; } + + public double getRevenueGrowth() { return revenueGrowth; } + public void setRevenueGrowth(double revenueGrowth) { this.revenueGrowth = revenueGrowth; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/GeneralDashboardData.java b/backend/src/main/java/com/rit/canteen/sales/model/GeneralDashboardData.java new file mode 100644 index 00000000..7e81e9cb --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/GeneralDashboardData.java @@ -0,0 +1,24 @@ +package com.rit.canteen.sales.model; + +import java.util.List; +import java.util.Map; + +public class GeneralDashboardData { + private DashboardStats stats; + private List> storeOverview; + private List> hourlySales; + private List insights; + + public GeneralDashboardData(DashboardStats stats, List> storeOverview, List> hourlySales, List insights) { + this.stats = stats; + this.storeOverview = storeOverview; + this.hourlySales = hourlySales; + this.insights = insights; + } + + // Getters + public DashboardStats getStats() { return stats; } + public List> getStoreOverview() { return storeOverview; } + public List> getHourlySales() { return hourlySales; } + public List getInsights() { return insights; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/LoginRequest.java b/backend/src/main/java/com/rit/canteen/sales/model/LoginRequest.java new file mode 100644 index 00000000..f0890152 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/LoginRequest.java @@ -0,0 +1,20 @@ +package com.rit.canteen.sales.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public class LoginRequest { + + @NotBlank(message = "Mobile number is required") + @Pattern(regexp = "^[0-9]{10}$", message = "Mobile number must be 10 digits") + private String mobileNumber; + + public LoginRequest() {} + + public LoginRequest(String mobileNumber) { + this.mobileNumber = mobileNumber; + } + + public String getMobileNumber() { return mobileNumber; } + public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java new file mode 100644 index 00000000..35ed2828 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java @@ -0,0 +1,68 @@ +package com.rit.canteen.sales.model; + +public class LoginResponse { + + private boolean success; + private String message; + private boolean userExists; + private UserDto user; + + public LoginResponse() {} + + public LoginResponse(boolean success, String message) { + this.success = success; + this.message = message; + } + + public LoginResponse(boolean success, String message, boolean userExists) { + this.success = success; + this.message = message; + this.userExists = userExists; + } + + public LoginResponse(boolean success, String message, UserDto user) { + this.success = success; + this.message = message; + this.user = user; + } + + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public boolean isUserExists() { return userExists; } + public void setUserExists(boolean userExists) { this.userExists = userExists; } + + public UserDto getUser() { return user; } + public void setUser(UserDto user) { this.user = user; } + + public static class UserDto { + private Long id; + private String mobileNumber; + private String name; + private boolean isLoggedIn; + + public UserDto() {} + + public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn) { + this.id = id; + this.mobileNumber = mobileNumber; + this.name = name; + this.isLoggedIn = isLoggedIn; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getMobileNumber() { return mobileNumber; } + public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public boolean isLoggedIn() { return isLoggedIn; } + public void setLoggedIn(boolean loggedIn) { isLoggedIn = loggedIn; } + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Order.java b/backend/src/main/java/com/rit/canteen/sales/model/Order.java new file mode 100644 index 00000000..38bf2be1 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Order.java @@ -0,0 +1,102 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "canteen_orders") +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String orderNumber; // Internal secure unique ID (e.g., ORD-X7Y9Z) + + @Column(nullable = true) + private String displayOrderId; // Sequential human-readable ID (e.g., 001) + + @Column(name = "user_id", nullable = false) + private Long userId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + @com.fasterxml.jackson.annotation.JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) + private User user; + + @Column(nullable = false) + private BigDecimal totalAmount; + + @Column(nullable = false) + private String status; // PENDING, COMPLETED, CANCELLED + + @Column(nullable = false) + private String paymentMethod; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(name = "is_archived", nullable = false) + private boolean isArchived = false; + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) + private List items = new ArrayList<>(); + + public Order() { + this.orderNumber = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + this.status = "PAID"; + this.createdAt = LocalDateTime.now(); + this.items = new ArrayList<>(); + } + + @PrePersist + protected void onCreate() { + if (createdAt == null) createdAt = LocalDateTime.now(); + if (orderNumber == null) orderNumber = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + if (status == null) status = "PAID"; + this.isArchived = false; + if (items != null) { + for (OrderItem item : items) { + item.setOrder(this); + } + } + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getOrderNumber() { return orderNumber; } + public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; } + + public String getDisplayOrderId() { return displayOrderId; } + public void setDisplayOrderId(String displayOrderId) { this.displayOrderId = displayOrderId; } + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getPaymentMethod() { return paymentMethod; } + public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public boolean isArchived() { return isArchived; } + public void setArchived(boolean archived) { isArchived = archived; } + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/OrderItem.java b/backend/src/main/java/com/rit/canteen/sales/model/OrderItem.java new file mode 100644 index 00000000..fff83560 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/OrderItem.java @@ -0,0 +1,60 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "canteen_order_items") +public class OrderItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id", nullable = false) + @JsonIgnore + private Order order; + + @Column(nullable = false) + private Long productId; + + @Column(nullable = false) + private String productName; + + @Column(nullable = false) + private BigDecimal price; + + @Column(nullable = false) + private int quantity; + + private Long stallId; + private String stallName; + + public OrderItem() {} + + public Order getOrder() { return order; } + public void setOrder(Order order) { this.order = order; } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getProductId() { return productId; } + public void setProductId(Long productId) { this.productId = productId; } + + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } + + public Long getStallId() { return stallId; } + public void setStallId(Long stallId) { this.stallId = stallId; } + + public String getStallName() { return stallName; } + public void setStallName(String stallName) { this.stallName = stallName; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/PinVerificationRequest.java b/backend/src/main/java/com/rit/canteen/sales/model/PinVerificationRequest.java new file mode 100644 index 00000000..80638c48 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/PinVerificationRequest.java @@ -0,0 +1,36 @@ +package com.rit.canteen.sales.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public class PinVerificationRequest { + + @NotBlank(message = "Mobile number is required") + @Pattern(regexp = "^[0-9]{10}$", message = "Mobile number must be 10 digits") + private String mobileNumber; + + private String name; + + @NotBlank(message = "PIN is required") + @Size(min = 6, max = 6, message = "PIN must be exactly 6 digits") + @Pattern(regexp = "^[0-9]{6}$", message = "PIN must be exactly 6 digits") + private String pin; + + public PinVerificationRequest() {} + + public PinVerificationRequest(String mobileNumber, String name, String pin) { + this.mobileNumber = mobileNumber; + this.name = name; + this.pin = pin; + } + + public String getMobileNumber() { return mobileNumber; } + public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getPin() { return pin; } + public void setPin(String pin) { this.pin = pin; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Product.java b/backend/src/main/java/com/rit/canteen/sales/model/Product.java new file mode 100644 index 00000000..0b4ec5a2 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Product.java @@ -0,0 +1,144 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "products") +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String productId; + + @NotBlank(message = "Product name is required") + private String name; + + private String category; + + private String description; + + private BigDecimal basePrice; + + private BigDecimal price; + + private BigDecimal offerPrice; + + private Double discountPercent; + + private BigDecimal discountAmount; + + private String counter; + + private String tag; + + private BigDecimal parcelCharges; + + private String barcode; + + private boolean attributesOptional; + + // Additional Attributes + private boolean isVeg; + private boolean hasAllergy; + + private boolean parcelNotAllowed; + + private boolean sessionOptional; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "product_id") + private List sessions = new ArrayList<>(); + + @Column(columnDefinition = "TEXT") + private String imageData; // Base64 image data + + private boolean active = true; + + private Integer stock = 0; + + @ManyToMany(mappedBy = "products", fetch = FetchType.EAGER) + @JsonIgnoreProperties({"products", "baseItems", "sessions", "imageData"}) + private List stalls = new ArrayList<>(); + + public Product() {} + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Integer getStock() { return stock; } + public void setStock(Integer stock) { this.stock = stock; } + + public String getProductId() { return productId; } + public void setProductId(String productId) { this.productId = productId; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public BigDecimal getBasePrice() { return basePrice; } + public void setBasePrice(BigDecimal basePrice) { this.basePrice = basePrice; } + + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + + public BigDecimal getOfferPrice() { return offerPrice; } + public void setOfferPrice(BigDecimal offerPrice) { this.offerPrice = offerPrice; } + + public Double getDiscountPercent() { return discountPercent; } + public void setDiscountPercent(Double discountPercent) { this.discountPercent = discountPercent; } + + public BigDecimal getDiscountAmount() { return discountAmount; } + public void setDiscountAmount(BigDecimal discountAmount) { this.discountAmount = discountAmount; } + + public String getCounter() { return counter; } + public void setCounter(String counter) { this.counter = counter; } + + public String getTag() { return tag; } + public void setTag(String tag) { this.tag = tag; } + + public BigDecimal getParcelCharges() { return parcelCharges; } + public void setParcelCharges(BigDecimal parcelCharges) { this.parcelCharges = parcelCharges; } + + public String getBarcode() { return barcode; } + public void setBarcode(String barcode) { this.barcode = barcode; } + + public boolean isAttributesOptional() { return attributesOptional; } + public void setAttributesOptional(boolean attributesOptional) { this.attributesOptional = attributesOptional; } + + public boolean isVeg() { return isVeg; } + public void setVeg(boolean veg) { isVeg = veg; } + + public boolean isHasAllergy() { return hasAllergy; } + public void setHasAllergy(boolean hasAllergy) { this.hasAllergy = hasAllergy; } + + public boolean isParcelNotAllowed() { return parcelNotAllowed; } + public void setParcelNotAllowed(boolean parcelNotAllowed) { this.parcelNotAllowed = parcelNotAllowed; } + + public boolean isSessionOptional() { return sessionOptional; } + public void setSessionOptional(boolean sessionOptional) { this.sessionOptional = sessionOptional; } + + public List getSessions() { return sessions; } + public void setSessions(List sessions) { this.sessions = sessions; } + + public String getImageData() { return imageData; } + public void setImageData(String imageData) { this.imageData = imageData; } + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + + public List getStalls() { return stalls; } + public void setStalls(List stalls) { this.stalls = stalls; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/ProductSession.java b/backend/src/main/java/com/rit/canteen/sales/model/ProductSession.java new file mode 100644 index 00000000..06672ba7 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/ProductSession.java @@ -0,0 +1,46 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.time.LocalTime; + +@Entity +@Table(name = "product_sessions") +public class ProductSession { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String dayOfWeek; + + private boolean active; + + private LocalTime startTime; + + private LocalTime endTime; + + public ProductSession() {} + + public ProductSession(String dayOfWeek, boolean active, LocalTime startTime, LocalTime endTime) { + this.dayOfWeek = dayOfWeek; + this.active = active; + this.startTime = startTime; + this.endTime = endTime; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getDayOfWeek() { return dayOfWeek; } + public void setDayOfWeek(String dayOfWeek) { this.dayOfWeek = dayOfWeek; } + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + + public LocalTime getStartTime() { return startTime; } + public void setStartTime(LocalTime startTime) { this.startTime = startTime; } + + public LocalTime getEndTime() { return endTime; } + public void setEndTime(LocalTime endTime) { this.endTime = endTime; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrder.java b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrder.java new file mode 100644 index 00000000..21f7745a --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrder.java @@ -0,0 +1,80 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "purchase_orders") +public class PurchaseOrder { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String purchaseId; // PO_1001, etc. + + private LocalDateTime date; + + @ManyToOne + @JoinColumn(name = "vendor_id") + private Vendor vendor; + + @Column(precision = 19, scale = 2) + private BigDecimal amount; + + @Column(precision = 19, scale = 2) + private BigDecimal discount = BigDecimal.ZERO; + + @Column(precision = 19, scale = 2) + private BigDecimal paidTotal = BigDecimal.ZERO; + + private String status; // OPEN, RECEIVED, BILLED, CLOSED + + private String referenceId; + + private String instruction; + + @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL, orphanRemoval = true) + private List items; + + public PurchaseOrder() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getPurchaseId() { return purchaseId; } + public void setPurchaseId(String purchaseId) { this.purchaseId = purchaseId; } + + public LocalDateTime getDate() { return date; } + public void setDate(LocalDateTime date) { this.date = date; } + + public Vendor getVendor() { return vendor; } + public void setVendor(Vendor vendor) { this.vendor = vendor; } + + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + + public BigDecimal getDiscount() { return discount; } + public void setDiscount(BigDecimal discount) { this.discount = discount; } + + public BigDecimal getPaidTotal() { return paidTotal; } + public void setPaidTotal(BigDecimal paidTotal) { this.paidTotal = paidTotal; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getReferenceId() { return referenceId; } + public void setReferenceId(String referenceId) { this.referenceId = referenceId; } + + public String getInstruction() { return instruction; } + public void setInstruction(String instruction) { this.instruction = instruction; } + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } + + public BigDecimal getPayables() { + return amount != null ? amount.subtract(paidTotal) : BigDecimal.ZERO; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderHistory.java b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderHistory.java new file mode 100644 index 00000000..795e3aec --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderHistory.java @@ -0,0 +1,51 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "purchase_order_history") +public class PurchaseOrderHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "purchase_order_id", nullable = false) + private PurchaseOrder purchaseOrder; + + private BigDecimal oldAmount; + private BigDecimal newAmount; + private LocalDateTime changeDate; + private String reason; + + public PurchaseOrderHistory() {} + + public PurchaseOrderHistory(PurchaseOrder purchaseOrder, BigDecimal oldAmount, BigDecimal newAmount, String reason) { + this.purchaseOrder = purchaseOrder; + this.oldAmount = oldAmount; + this.newAmount = newAmount; + this.changeDate = LocalDateTime.now(); + this.reason = reason; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public PurchaseOrder getPurchaseOrder() { return purchaseOrder; } + public void setPurchaseOrder(PurchaseOrder purchaseOrder) { this.purchaseOrder = purchaseOrder; } + + public BigDecimal getOldAmount() { return oldAmount; } + public void setOldAmount(BigDecimal oldAmount) { this.oldAmount = oldAmount; } + + public BigDecimal getNewAmount() { return newAmount; } + public void setNewAmount(BigDecimal newAmount) { this.newAmount = newAmount; } + + public LocalDateTime getChangeDate() { return changeDate; } + public void setChangeDate(LocalDateTime changeDate) { this.changeDate = changeDate; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderItem.java b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderItem.java new file mode 100644 index 00000000..260bac82 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/PurchaseOrderItem.java @@ -0,0 +1,48 @@ +package com.rit.canteen.sales.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import java.math.BigDecimal; + +@Entity +@Table(name = "purchase_order_items") +public class PurchaseOrderItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "purchase_order_id", nullable = false) + @JsonIgnore + private PurchaseOrder purchaseOrder; + + private String productName; + + private Double quantity; + + @Column(precision = 19, scale = 2) + private BigDecimal rate; + + @Column(precision = 19, scale = 2) + private BigDecimal total; + + public PurchaseOrderItem() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public PurchaseOrder getPurchaseOrder() { return purchaseOrder; } + public void setPurchaseOrder(PurchaseOrder purchaseOrder) { this.purchaseOrder = purchaseOrder; } + + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + + public Double getQuantity() { return quantity; } + public void setQuantity(Double quantity) { this.quantity = quantity; } + + public BigDecimal getRate() { return rate; } + public void setRate(BigDecimal rate) { this.rate = rate; } + + public BigDecimal getTotal() { return total; } + public void setTotal(BigDecimal total) { this.total = total; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Stall.java b/backend/src/main/java/com/rit/canteen/sales/model/Stall.java new file mode 100644 index 00000000..03aaaaac --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Stall.java @@ -0,0 +1,91 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "stalls") +public class Stall { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Stall title is required") + private String name; + + private String description; + + @Column(columnDefinition = "TEXT") + private String imageData; // Base64 image data + + private boolean active = true; + + private boolean temporarilyClosed = false; + + private boolean sessionOptional = false; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "stall_id") + private List sessions = new ArrayList<>(); + + @ManyToMany + @JoinTable( + name = "stall_products", + joinColumns = @JoinColumn(name = "stall_id"), + inverseJoinColumns = @JoinColumn(name = "product_id") + ) + @JsonIgnoreProperties({"stalls", "sessions"}) + private List products = new ArrayList<>(); + + @ManyToMany + @JoinTable( + name = "stall_base_items", + joinColumns = @JoinColumn(name = "stall_id"), + inverseJoinColumns = @JoinColumn(name = "base_item_id") + ) + @JsonIgnoreProperties({"imageData", "description"}) + private List baseItems = new ArrayList<>(); + + public Stall() {} + + public Stall(String name, String description, String imageData) { + this.name = name; + this.description = description; + this.imageData = imageData; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long 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 String getImageData() { return imageData; } + public void setImageData(String imageData) { this.imageData = imageData; } + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + + public List getProducts() { return products; } + public void setProducts(List products) { this.products = products; } + + public List getBaseItems() { return baseItems; } + public void setBaseItems(List baseItems) { this.baseItems = baseItems; } + + public boolean isTemporarilyClosed() { return temporarilyClosed; } + public void setTemporarilyClosed(boolean temporarilyClosed) { this.temporarilyClosed = temporarilyClosed; } + + public boolean isSessionOptional() { return sessionOptional; } + public void setSessionOptional(boolean sessionOptional) { this.sessionOptional = sessionOptional; } + + public List getSessions() { return sessions; } + public void setSessions(List sessions) { this.sessions = sessions; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/StallSession.java b/backend/src/main/java/com/rit/canteen/sales/model/StallSession.java new file mode 100644 index 00000000..3afac615 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/StallSession.java @@ -0,0 +1,46 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.time.LocalTime; + +@Entity +@Table(name = "stall_sessions") +public class StallSession { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String dayOfWeek; + + private boolean active; + + private LocalTime startTime; + + private LocalTime endTime; + + public StallSession() {} + + public StallSession(String dayOfWeek, boolean active, LocalTime startTime, LocalTime endTime) { + this.dayOfWeek = dayOfWeek; + this.active = active; + this.startTime = startTime; + this.endTime = endTime; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getDayOfWeek() { return dayOfWeek; } + public void setDayOfWeek(String dayOfWeek) { this.dayOfWeek = dayOfWeek; } + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + + public LocalTime getStartTime() { return startTime; } + public void setStartTime(LocalTime startTime) { this.startTime = startTime; } + + public LocalTime getEndTime() { return endTime; } + public void setEndTime(LocalTime endTime) { this.endTime = endTime; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/SystemUser.java b/backend/src/main/java/com/rit/canteen/sales/model/SystemUser.java new file mode 100644 index 00000000..702ff0fc --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/SystemUser.java @@ -0,0 +1,55 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "system_users") +public class SystemUser { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String role; // MASTER, MANAGER + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "system_user_permissions", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "permission") + private List permissions; + + @Column(nullable = false) + private boolean viewOnly = false; + + public SystemUser() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + + public List getPermissions() { return permissions; } + public void setPermissions(List permissions) { this.permissions = permissions; } + + public boolean isViewOnly() { return viewOnly; } + public void setViewOnly(boolean viewOnly) { this.viewOnly = viewOnly; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java new file mode 100644 index 00000000..76d8c994 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java @@ -0,0 +1,50 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; + +@Entity +@Table(name = "terminals") +public class Terminal { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Terminal name is required") + private String name; + + @NotBlank(message = "Location is required") + private String location; + + @NotBlank(message = "Security PIN is required") + private String pin; + + @Column(unique = true) + private String apiKey; + + public Terminal() {} + + public Terminal(String name, String location, String pin, String apiKey) { + this.name = name; + this.location = location; + this.pin = pin; + this.apiKey = apiKey; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getLocation() { return location; } + public void setLocation(String location) { this.location = location; } + + public String getPin() { return pin; } + public void setPin(String pin) { this.pin = pin; } + + public String getApiKey() { return apiKey; } + public void setApiKey(String apiKey) { this.apiKey = apiKey; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java new file mode 100644 index 00000000..ca4ad9e2 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java @@ -0,0 +1,35 @@ +package com.rit.canteen.sales.model; + +public class TerminalDTO { + private Long id; + private String name; + private String location; + private String apiKey; + private String pin; + + public TerminalDTO() {} + + public TerminalDTO(Long id, String name, String location, String apiKey, String pin) { + this.id = id; + this.name = name; + this.location = location; + this.apiKey = apiKey; + this.pin = pin; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getLocation() { return location; } + public void setLocation(String location) { this.location = location; } + + public String getApiKey() { return apiKey; } + public void setApiKey(String apiKey) { this.apiKey = apiKey; } + + public String getPin() { return pin; } + public void setPin(String pin) { this.pin = pin; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/User.java b/backend/src/main/java/com/rit/canteen/sales/model/User.java new file mode 100644 index 00000000..4b8d5114 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/User.java @@ -0,0 +1,76 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import java.time.LocalDateTime; + +@Entity +@Table(name = "app_users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Mobile number is required") + @Pattern(regexp = "^[0-9]{10}$", message = "Mobile number must be 10 digits") + @Column(unique = true, nullable = false, length = 10) + private String mobileNumber; + + @Column(nullable = true) + private String name; + + @Column(nullable = false) + @com.fasterxml.jackson.annotation.JsonIgnore + private String pinHash; + + @Column(nullable = false) + private boolean isLoggedIn = false; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + + @Column(nullable = true) + private LocalDateTime lastLoginAt; + + public User() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getMobileNumber() { return mobileNumber; } + public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getPinHash() { return pinHash; } + public void setPinHash(String pinHash) { this.pinHash = pinHash; } + + public boolean isLoggedIn() { return isLoggedIn; } + public void setLoggedIn(boolean loggedIn) { isLoggedIn = loggedIn; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + public LocalDateTime getLastLoginAt() { return lastLoginAt; } + public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; } + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/UserUpdateRequest.java b/backend/src/main/java/com/rit/canteen/sales/model/UserUpdateRequest.java new file mode 100644 index 00000000..dd4d6113 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/UserUpdateRequest.java @@ -0,0 +1,41 @@ +package com.rit.canteen.sales.model; + +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +/** + * DTO for administrative user updates. + */ +public class UserUpdateRequest { + private String name; + + @Pattern(regexp = "^[0-9]{10}$", message = "Mobile number must be 10 digits") + private String mobileNumber; + + @Pattern(regexp = "^[0-9]{6}$|", message = "PIN must be exactly 6 digits if provided") + private String pin; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMobileNumber() { + return mobileNumber; + } + + public void setMobileNumber(String mobileNumber) { + this.mobileNumber = mobileNumber; + } + + public String getPin() { + return pin; + } + + public void setPin(String pin) { + this.pin = pin; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Vendor.java b/backend/src/main/java/com/rit/canteen/sales/model/Vendor.java new file mode 100644 index 00000000..ddcd40a6 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Vendor.java @@ -0,0 +1,50 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; + +@Entity +@Table(name = "vendors") +public class Vendor { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + private String companyName; + + private String contactNumber; + + private String email; + + @Column(precision = 19, scale = 2, nullable = false) + private BigDecimal totalAmount = BigDecimal.ZERO; + + @Column(nullable = false) + private Integer totalOrders = 0; + + public Vendor() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getCompanyName() { return companyName; } + public void setCompanyName(String companyName) { this.companyName = companyName; } + + public String getContactNumber() { return contactNumber; } + public void setContactNumber(String contactNumber) { this.contactNumber = contactNumber; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + + public Integer getTotalOrders() { return totalOrders; } + public void setTotalOrders(Integer totalOrders) { this.totalOrders = totalOrders; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/BaseItemRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/BaseItemRepository.java new file mode 100644 index 00000000..9f354ba0 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/BaseItemRepository.java @@ -0,0 +1,9 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.BaseItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BaseItemRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java new file mode 100644 index 00000000..e6bfc9fc --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java @@ -0,0 +1,43 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Order; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface OrderRepository extends JpaRepository, JpaSpecificationExecutor { + List findByUserIdOrderByCreatedAtDesc(Long userId); + long countByCreatedAtGreaterThanEqual(LocalDateTime startOfDay); + List findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp); + Optional findByOrderNumber(String orderNumber); + + @Query("SELECT SUM(o.totalAmount) FROM Order o") + BigDecimal getTotalRevenue(); + + @Query("SELECT COUNT(DISTINCT o.userId) FROM Order o WHERE o.createdAt >= :startOfDay") + long countUniqueUsersToday(@Param("startOfDay") LocalDateTime startOfDay); + + @Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt >= :start AND o.createdAt <= :end") + BigDecimal getRevenuePerPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); + + List findTop5ByOrderByCreatedAtDesc(); + + @Query("SELECT i.stallName, SUM(i.price * i.quantity), COUNT(DISTINCT o.id) " + + "FROM Order o JOIN o.items i " + + "WHERE o.createdAt >= :start AND o.createdAt <= :end " + + "GROUP BY i.stallName") + List getStoreOverview(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); + + @Query("SELECT HOUR(o.createdAt), SUM(o.totalAmount) " + + "FROM Order o " + + "WHERE o.createdAt >= :start AND o.createdAt <= :end " + + "GROUP BY HOUR(o.createdAt) " + + "ORDER BY HOUR(o.createdAt)") + List getHourlySales(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java new file mode 100644 index 00000000..4cb0f002 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java @@ -0,0 +1,16 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProductRepository extends JpaRepository { + @Query("SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.stalls") + List findAllWithStalls(); + + List findByCategory(String category); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderHistoryRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderHistoryRepository.java new file mode 100644 index 00000000..acf998a5 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderHistoryRepository.java @@ -0,0 +1,9 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.PurchaseOrderHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PurchaseOrderHistoryRepository extends JpaRepository { + List findByPurchaseOrderIdOrderByChangeDateDesc(Long purchaseOrderId); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java new file mode 100644 index 00000000..735c74d1 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java @@ -0,0 +1,25 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.PurchaseOrder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PurchaseOrderRepository extends JpaRepository { + Optional findByPurchaseId(String purchaseId); + + @org.springframework.data.jpa.repository.Query("SELECT SUM(p.amount) FROM PurchaseOrder p") + java.math.BigDecimal getTotalPurchaseAmount(); + + @org.springframework.data.jpa.repository.Query("SELECT SUM(COALESCE(p.amount, 0) - COALESCE(p.paidTotal, 0)) FROM PurchaseOrder p") + java.math.BigDecimal getTotalBalanceAmount(); + + @org.springframework.data.jpa.repository.Query("SELECT COUNT(p) FROM PurchaseOrder p WHERE p.status != 'PAID'") + long countUnpaidBills(); + + @org.springframework.data.jpa.repository.Query("SELECT p.date, SUM(p.amount) FROM PurchaseOrder p GROUP BY p.date ORDER BY p.date ASC") + List getPurchaseTrend(); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/StallRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/StallRepository.java new file mode 100644 index 00000000..e662de1b --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/StallRepository.java @@ -0,0 +1,12 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Stall; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StallRepository extends JpaRepository { + List findByActiveTrue(); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/SystemUserRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/SystemUserRepository.java new file mode 100644 index 00000000..60326d4d --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/SystemUserRepository.java @@ -0,0 +1,11 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.SystemUser; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.List; + +public interface SystemUserRepository extends JpaRepository { + Optional findByEmail(String email); + List findByRole(String role); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/TerminalRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/TerminalRepository.java new file mode 100644 index 00000000..cde5af12 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/TerminalRepository.java @@ -0,0 +1,12 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Terminal; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TerminalRepository extends JpaRepository { + Optional findByApiKey(String apiKey); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java new file mode 100644 index 00000000..4cecb491 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByMobileNumber(String mobileNumber); + boolean existsByMobileNumber(String mobileNumber); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/VendorRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/VendorRepository.java new file mode 100644 index 00000000..cefab71d --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/VendorRepository.java @@ -0,0 +1,9 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Vendor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VendorRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java new file mode 100644 index 00000000..2a1b3354 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java @@ -0,0 +1,108 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.DashboardStats; +import com.rit.canteen.sales.model.GeneralDashboardData; +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.repository.OrderRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; + +@Service +public class DashboardService { + + @Autowired + private OrderRepository orderRepository; + + public GeneralDashboardData getGeneralDashboardData() { + LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); + LocalDateTime endOfToday = LocalDate.now().atTime(LocalTime.MAX); + + DashboardStats stats = getDashboardStats(); + + // 1. Store Overview + List storeData = orderRepository.getStoreOverview(startOfToday, endOfToday); + List> storeOverview = new ArrayList<>(); + for (Object[] row : storeData) { + Map store = new HashMap<>(); + store.put("name", row[0] != null ? row[0] : "Unknown Stall"); + store.put("sale", row[1] != null ? row[1] : 0); + store.put("orders", row[2] != null ? row[2] : 0); + store.put("taxes", 0); // Placeholder + store.put("purchase", 0); // Placeholder + storeOverview.add(store); + } + + // 2. Hourly Sales + List hourlyData = orderRepository.getHourlySales(startOfToday, endOfToday); + List> hourlySales = new ArrayList<>(); + // Initialize 24 hours + for (int i = 0; i < 24; i += 2) { + Map hourMap = new HashMap<>(); + String label = i == 0 ? "12am" : (i > 12 ? (i-12) + "pm" : i + (i == 12 ? "pm" : "am")); + hourMap.put("time", label); + hourMap.put("value", 0); + + for (Object[] row : hourlyData) { + int h = (int) row[0]; + if (h >= i && h < i + 2) { + BigDecimal val = (BigDecimal) row[1]; + hourMap.put("value", ((Number) hourMap.get("value")).doubleValue() + val.doubleValue()); + } + } + hourlySales.add(hourMap); + } + + // 3. Dynamic Insights + List insights = new ArrayList<>(); + if (stats.getActiveOrders() > 0) { + insights.add(stats.getActiveOrders() + " orders today! Clearly the crowd's found their happy place πŸ’ƒπŸ•Ί"); + BigDecimal avg = stats.getTotalSales() > 0 ? BigDecimal.valueOf(stats.getTotalSales()).divide(BigDecimal.valueOf(stats.getActiveOrders()), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO; + insights.add("β‚Ή" + avg + " average order value! Proof that happy bellies don't need heavy bills 🀀πŸ”₯"); + } else { + insights.add("Waiting for the first orders of the day to roll in... β˜•"); + } + + return new GeneralDashboardData(stats, storeOverview, hourlySales, insights); + } + + public DashboardStats getDashboardStats() { + LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); + LocalDateTime endOfToday = LocalDate.now().atTime(LocalTime.MAX); + + LocalDateTime startOfYesterday = LocalDate.now().minusDays(1).atStartOfDay(); + LocalDateTime endOfYesterday = LocalDate.now().minusDays(1).atTime(LocalTime.MAX); + + BigDecimal totalRevenueRaw = orderRepository.getTotalRevenue(); + long totalSales = totalRevenueRaw != null ? totalRevenueRaw.longValue() : 0; + + int activeOrders = (int) orderRepository.countByCreatedAtGreaterThanEqual(startOfToday); + int dailyCustomers = (int) orderRepository.countUniqueUsersToday(startOfToday); + + BigDecimal todayRevenue = orderRepository.getRevenuePerPeriod(startOfToday, endOfToday); + BigDecimal yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday); + + double growth = 0; + if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO; + growth = today.subtract(yesterdayRevenue) + .divide(yesterdayRevenue, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)) + .doubleValue(); + } else if (todayRevenue != null && todayRevenue.compareTo(BigDecimal.ZERO) > 0) { + growth = 100.0; + } + + return new DashboardStats(totalSales, activeOrders, dailyCustomers, growth); + } + + public List getRecentOrders() { + return orderRepository.findTop5ByOrderByCreatedAtDesc(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java new file mode 100644 index 00000000..1c451c9f --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java @@ -0,0 +1,119 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.BaseItem; +import com.rit.canteen.sales.model.Product; +import com.rit.canteen.sales.repository.BaseItemRepository; +import com.rit.canteen.sales.repository.ProductRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; + +import java.math.BigDecimal; + +@Component +public class DatabaseSeeder implements CommandLineRunner { + + @Autowired + private BaseItemRepository baseItemRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Override + public void run(String... args) throws Exception { + repairStallsSchema(); + repairLobColumns(); + if (baseItemRepository.count() == 0) { + seedCategories(); + } + if (productRepository.count() == 0) { + seedProducts(); + } + } + + private void repairStallsSchema() { + System.out.println("Checking schema consistency for 'stalls' table..."); + try { + jdbcTemplate.execute("ALTER TABLE stalls ADD COLUMN IF NOT EXISTS session_optional BOOLEAN DEFAULT FALSE"); + jdbcTemplate.execute("ALTER TABLE stalls ADD COLUMN IF NOT EXISTS temporarily_closed BOOLEAN DEFAULT FALSE"); + System.out.println("Schema consistency confirmed."); + } catch (Exception e) { + System.err.println("Schema repair notice (this may happen if columns already exist): " + e.getMessage()); + } + } + + private void repairLobColumns() { + System.out.println("Checking image columns for LOB-to-TEXT migration..."); + try { + // Repair products table + if (!isColumnTextType("products", "image_data")) { + System.out.println("Migrating products.image_data to TEXT..."); + try { jdbcTemplate.execute("ALTER TABLE products DROP COLUMN image_data"); } catch (Exception e) {} + jdbcTemplate.execute("ALTER TABLE products ADD COLUMN image_data TEXT"); + } + + // Repair stalls table + if (!isColumnTextType("stalls", "image_data")) { + System.out.println("Migrating stalls.image_data to TEXT..."); + try { jdbcTemplate.execute("ALTER TABLE stalls DROP COLUMN image_data"); } catch (Exception e) {} + jdbcTemplate.execute("ALTER TABLE stalls ADD COLUMN image_data TEXT"); + } + + System.out.println("Image schema check complete."); + } catch (Exception e) { + System.err.println("Database migration notice: " + e.getMessage()); + } + } + + private boolean isColumnTextType(String tableName, String columnName) { + try { + String query = "SELECT data_type FROM information_schema.columns WHERE table_name = ? AND column_name = ?"; + String dataType = jdbcTemplate.queryForObject(query, String.class, tableName, columnName); + return "text".equalsIgnoreCase(dataType); + } catch (Exception e) { + return false; // Column might not exist or other error + } + } + + private void seedCategories() { + BaseItem breakfast = new BaseItem("Breakfast", "Morning favorites", true); + BaseItem lunch = new BaseItem("Lunch", "Hearty meals", true); + BaseItem snacks = new BaseItem("Snacks", "Quick bites", true); + BaseItem beverages = new BaseItem("Beverages", "Refreshing drinks", true); + BaseItem desserts = new BaseItem("Desserts", "Sweet treats", true); + + baseItemRepository.saveAll(List.of(breakfast, lunch, snacks, beverages, desserts)); + System.out.println("Categories seeded!"); + } + + private void seedProducts() { + Product dosa = createProduct("Masala Dosa", "Breakfast", "Crispy dosa with potato filling", 60.0, 50, true); + Product poha = createProduct("Poha", "Breakfast", "Flattened rice with onions and spices", 30.0, 40, true); + Product thali = createProduct("Veg Thali", "Lunch", "Full meal with roti, sabzi, dal and rice", 120.0, 30, true); + Product biryani = createProduct("Chicken Biryani", "Lunch", "Aromatic rice with spiced chicken", 150.0, 20, false); + Product samosa = createProduct("Samosa", "Snacks", "Deep fried pastry with potato filling", 15.0, 100, true); + Product tea = createProduct("Masala Tea", "Beverages", "Traditional Indian spiced tea", 10.0, 200, true); + Product iceCream = createProduct("Vanilla Ice Cream", "Desserts", "Classic vanilla flavor", 30.0, 0, true); // Out of stock + + productRepository.saveAll(List.of(dosa, poha, thali, biryani, samosa, tea, iceCream)); + System.out.println("Products seeded!"); + } + + private Product createProduct(String name, String category, String desc, Double price, Integer stock, boolean isVeg) { + Product p = new Product(); + p.setName(name); + p.setCategory(category); + p.setDescription(desc); + p.setPrice(BigDecimal.valueOf(price)); + p.setBasePrice(BigDecimal.valueOf(price * 0.8)); + p.setStock(stock); + p.setVeg(isVeg); + p.setActive(true); + return p; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/OrderArchiverService.java b/backend/src/main/java/com/rit/canteen/sales/service/OrderArchiverService.java new file mode 100644 index 00000000..7f148df9 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/OrderArchiverService.java @@ -0,0 +1,57 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.repository.OrderRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class OrderArchiverService { + + @Autowired + private OrderRepository orderRepository; + + /** + * Automatically archives orders from previous days at midnight (12:00 AM). + * Cron: 0 0 0 * * * (Second Minute Hour Day Month DayOfWeek) + */ + @Scheduled(cron = "0 0 0 * * *") + @Transactional + public void archivePreviousDayOrders() { + LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); + System.out.println("Starting automated archival process for orders before: " + startOfToday); + + List ordersToArchive = orderRepository.findByIsArchivedFalseAndCreatedAtBefore(startOfToday); + + if (!ordersToArchive.isEmpty()) { + for (Order order : ordersToArchive) { + order.setArchived(true); + } + orderRepository.saveAll(ordersToArchive); + System.out.println("Successfully archived " + ordersToArchive.size() + " orders."); + } else { + System.out.println("No orders found to archive for today."); + } + } + + /** + * Manual trigger for archival logic (useful for testing or initial setup). + */ + @Transactional + public int archiveNow() { + LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); + List ordersToArchive = orderRepository.findByIsArchivedFalseAndCreatedAtBefore(startOfToday); + + for (Order order : ordersToArchive) { + order.setArchived(true); + } + orderRepository.saveAll(ordersToArchive); + return ordersToArchive.size(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java b/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java new file mode 100644 index 00000000..0da4e136 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java @@ -0,0 +1,123 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.PurchaseOrder; +import com.rit.canteen.sales.model.Vendor; +import com.rit.canteen.sales.repository.PurchaseOrderRepository; +import com.rit.canteen.sales.repository.VendorRepository; +import com.rit.canteen.sales.repository.PurchaseOrderHistoryRepository; +import com.rit.canteen.sales.model.PurchaseOrderHistory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Service +@Transactional +public class PurchaseService { + + @Autowired + private PurchaseOrderRepository purchaseOrderRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Autowired + private PurchaseOrderHistoryRepository historyRepository; + + public List getAllOrders() { + return purchaseOrderRepository.findAll(); + } + + public List getAllVendors() { + return vendorRepository.findAll(); + } + + public PurchaseOrder createOrder(PurchaseOrder order) { + if (order == null) throw new IllegalArgumentException("Order cannot be null"); + + // Check for history if this is an update + if (order.getId() != null) { + purchaseOrderRepository.findById(order.getId()).ifPresent(existing -> { + BigDecimal oldAmount = existing.getAmount() != null ? existing.getAmount() : BigDecimal.ZERO; + BigDecimal newAmount = order.getAmount() != null ? order.getAmount() : BigDecimal.ZERO; + + if (oldAmount.compareTo(newAmount) != 0) { + historyRepository.save(new PurchaseOrderHistory(existing, oldAmount, newAmount, "Amount updated during processing")); + } + }); + } + + // Link items to the parent order + if (order.getItems() != null) { + order.getItems().forEach(item -> item.setPurchaseOrder(order)); + } + + PurchaseOrder savedOrder = purchaseOrderRepository.save(order); + + // Update vendor statistics (only for NEW orders to avoid double counting, + // OR we should adjust the difference for updates - but user mostly wants new orders tracked) + if (order.getId() == null && savedOrder.getVendor() != null && savedOrder.getVendor().getId() != null) { + vendorRepository.findById(savedOrder.getVendor().getId()).ifPresent(vendor -> { + vendor.setTotalOrders((vendor.getTotalOrders() != null ? vendor.getTotalOrders() : 0) + 1); + BigDecimal currentAmount = vendor.getTotalAmount() != null ? vendor.getTotalAmount() : BigDecimal.ZERO; + BigDecimal orderAmount = savedOrder.getAmount() != null ? savedOrder.getAmount() : BigDecimal.ZERO; + vendor.setTotalAmount(currentAmount.add(orderAmount)); + vendorRepository.save(vendor); + }); + } + + return savedOrder; + } + + public List getOrderHistory(Long orderId) { + return historyRepository.findByPurchaseOrderIdOrderByChangeDateDesc(orderId); + } + + public Vendor createVendor(Vendor vendor) { + if (vendor == null) throw new IllegalArgumentException("Vendor cannot be null"); + return vendorRepository.save(vendor); + } + + public Optional getOrderById(Long id) { + if (id == null) return Optional.empty(); + return purchaseOrderRepository.findById(id); + } + + public void deleteVendor(Long id) { + if (id != null) vendorRepository.deleteById(id); + } + + public void deleteOrder(Long id) { + if (id != null) purchaseOrderRepository.deleteById(id); + } + + public Map getPurchaseSummary() { + Map summary = new HashMap<>(); + BigDecimal total = purchaseOrderRepository.getTotalPurchaseAmount(); + BigDecimal balance = purchaseOrderRepository.getTotalBalanceAmount(); + long unpaidCount = purchaseOrderRepository.countUnpaidBills(); + + summary.put("totalAmount", total != null ? total : BigDecimal.ZERO); + summary.put("balanceAmount", balance != null ? balance : BigDecimal.ZERO); + summary.put("paidAmount", (total != null ? total : BigDecimal.ZERO).subtract(balance != null ? balance : BigDecimal.ZERO)); + summary.put("unpaidCount", unpaidCount); + + List trendData = purchaseOrderRepository.getPurchaseTrend(); + List> trend = new ArrayList<>(); + for (Object[] row : trendData) { + Map point = new HashMap<>(); + point.put("date", row[0].toString()); + point.put("amount", row[1]); + trend.add(point); + } + summary.put("trend", trend); + + return summary; + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java b/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java new file mode 100644 index 00000000..a68d2eaa --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java @@ -0,0 +1,99 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.SystemUser; +import com.rit.canteen.sales.repository.SystemUserRepository; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class SystemUserService { + + @Autowired + private SystemUserRepository repository; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @PostConstruct + public void init() { + // Handle migration from old credentials if they exist + repository.findByEmail("admin@ritcanteen.com").ifPresent(oldUser -> { + boolean hasNewAdmin = repository.findByEmail("admin").isPresent(); + if (hasNewAdmin) { + repository.delete(oldUser); + System.out.println(">>> REMOVED OBSOLETE MASTER USER: admin@ritcanteen.com"); + } else { + oldUser.setEmail("admin"); + oldUser.setPassword(passwordEncoder.encode("admin")); + repository.save(oldUser); + System.out.println(">>> MIGRATED MASTER USER: admin / admin"); + } + }); + + // Ensure 'admin' user exists and has the correct password + Optional adminUser = repository.findByEmail("admin"); + if (adminUser.isPresent()) { + SystemUser admin = adminUser.get(); + admin.setPassword(passwordEncoder.encode("admin")); + admin.setRole("MASTER"); + repository.save(admin); + System.out.println(">>> UPDATED MASTER USER PASSWORD: admin / admin"); + } else { + SystemUser master = new SystemUser(); + master.setName("Admin Master"); + master.setEmail("admin"); + master.setPassword(passwordEncoder.encode("admin")); + master.setRole("MASTER"); + master.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); + master.setViewOnly(false); + repository.save(master); + System.out.println(">>> SEEDED MASTER USER: admin / admin"); + } + } + + public List getAllManagers() { + return repository.findByRole("MANAGER"); + } + + public SystemUser createManager(SystemUser manager) { + manager.setRole("MANAGER"); + manager.setPassword(passwordEncoder.encode(manager.getPassword())); + return repository.save(manager); + } + + public List getAllStaff() { + return repository.findByRole("STAFF"); + } + + public SystemUser createStaff(SystemUser staff) { + staff.setRole("STAFF"); + staff.setPassword(passwordEncoder.encode(staff.getPassword())); + return repository.save(staff); + } + + public void deleteManager(Long id) { + if (id != null) { + repository.deleteById(id); + } + } + + public Optional authenticate(String email, String password) { + System.out.println(">>> Attempting authentication for: " + email); + Optional user = repository.findByEmail(email); + if (user.isPresent()) { + boolean matches = passwordEncoder.matches(password, user.get().getPassword()); + System.out.println(">>> User found. Password match: " + matches); + if (matches) { + return user; + } + } else { + System.out.println(">>> User NOT found: " + email); + } + return Optional.empty(); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java new file mode 100644 index 00000000..6316de65 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java @@ -0,0 +1,46 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.Terminal; +import com.rit.canteen.sales.repository.TerminalRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class TerminalService { + + @Autowired + private TerminalRepository terminalRepository; + + public List getAllTerminals() { + return terminalRepository.findAll(); + } + + public Terminal createTerminal(Terminal terminal) { + // Generate a secure API Key + terminal.setApiKey("POS-" + UUID.randomUUID().toString().replace("-", "").toUpperCase()); + return terminalRepository.save(terminal); + } + + public boolean verifyPin(Long id, String pin) { + if (id == null) return false; + Optional terminalOpt = terminalRepository.findById(id); + return terminalOpt.map(terminal -> + terminal.getPin().trim().equals(pin.trim()) + ).orElse(false); + } + + public Optional getTerminalById(Long id) { + if (id == null) return Optional.empty(); + return terminalRepository.findById(id); + } + + public void deleteTerminal(Long id) { + if (id != null) { + terminalRepository.deleteById(id); + } + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/UserService.java b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java new file mode 100644 index 00000000..54a77206 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java @@ -0,0 +1,203 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.*; +import com.rit.canteen.sales.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + /** + * Check if a user exists by mobile number. + * Returns a LoginResponse indicating whether the user exists. + */ + public LoginResponse checkUserExists(String mobileNumber) { + boolean exists = userRepository.existsByMobileNumber(mobileNumber); + if (exists) { + return new LoginResponse(true, "User found. Please enter your PIN.", true); + } else { + return new LoginResponse(true, "User not found. Please enter your name and create a PIN.", false); + } + } + + /** + * Register a new user with name, mobile number and a 6-digit PIN. + * Encrypts the PIN using BCrypt and marks the user as logged in. + */ + public LoginResponse registerUser(String mobileNumber, String name, String pin) { + if (userRepository.existsByMobileNumber(mobileNumber)) { + return new LoginResponse(false, "User already exists. Please login instead."); + } + + if (name == null || name.isBlank()) { + return new LoginResponse(false, "Name is required for registration."); + } + + if (pin == null || pin.length() != 6 || !pin.matches("^[0-9]{6}$")) { + return new LoginResponse(false, "PIN must be exactly 6 digits."); + } + + User user = new User(); + user.setMobileNumber(mobileNumber); + user.setName(name); + user.setPinHash(passwordEncoder.encode(pin)); + user.setLoggedIn(true); + user.setLastLoginAt(LocalDateTime.now()); + + userRepository.save(user); + + LoginResponse.UserDto userDto = new LoginResponse.UserDto( + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn() + ); + + return new LoginResponse(true, "Registration successful. You are now logged in.", userDto); + } + + /** + * Verify the PIN for an existing user and log them in. + */ + public LoginResponse verifyPinAndLogin(String mobileNumber, String pin) { + Optional userOpt = userRepository.findByMobileNumber(mobileNumber); + + if (userOpt.isEmpty()) { + return new LoginResponse(false, "User not found. Please register first."); + } + + User user = userOpt.get(); + + if (!passwordEncoder.matches(pin, user.getPinHash())) { + return new LoginResponse(false, "Incorrect PIN. Please try again."); + } + + user.setLoggedIn(true); + user.setLastLoginAt(LocalDateTime.now()); + userRepository.save(user); + + LoginResponse.UserDto userDto = new LoginResponse.UserDto( + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn() + ); + + return new LoginResponse(true, "Login successful.", userDto); + } + + /** + * Logout a user by updating their logged-in status. + */ + public LoginResponse logout(String mobileNumber) { + Optional userOpt = userRepository.findByMobileNumber(mobileNumber); + + if (userOpt.isEmpty()) { + return new LoginResponse(false, "User not found."); + } + + User user = userOpt.get(); + user.setLoggedIn(false); + userRepository.save(user); + + return new LoginResponse(true, "Logged out successfully."); + } + + /** + * Change a user's PIN after verifying the current one. + */ + public LoginResponse changePin(String mobileNumber, String currentPin, String newPin) { + Optional userOpt = userRepository.findByMobileNumber(mobileNumber); + + if (userOpt.isEmpty()) { + return new LoginResponse(false, "User not found."); + } + + User user = userOpt.get(); + + // Verify current PIN + if (!passwordEncoder.matches(currentPin, user.getPinHash())) { + return new LoginResponse(false, "Incorrect current PIN. Check and try again."); + } + + // Validate new PIN format + if (newPin == null || newPin.length() != 6 || !newPin.matches("^[0-9]{6}$")) { + return new LoginResponse(false, "New PIN must be exactly 6 digits."); + } + + // Check if new PIN is the same as current + if (currentPin.equals(newPin)) { + return new LoginResponse(false, "New PIN cannot be the same as the current one."); + } + + // Update PIN + user.setPinHash(passwordEncoder.encode(newPin)); + userRepository.save(user); + + return new LoginResponse(true, "Security PIN updated successfully."); + } + + /** + * Get user details by mobile number. + */ + public LoginResponse.UserDto getUserByMobile(String mobileNumber) { + Optional userOpt = userRepository.findByMobileNumber(mobileNumber); + if (userOpt.isEmpty()) { + return null; + } + User user = userOpt.get(); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()); + } + + /** + * Get all registered users for administration dashboard (Paginated). + */ + public Page getAllUsers(Pageable pageable) { + return userRepository.findAll(pageable) + .map(user -> new LoginResponse.UserDto( + user.getId(), + user.getMobileNumber(), + user.getName(), + user.isLoggedIn() + )); + } + + /** + * Update user details as an administrator. + */ + public LoginResponse.UserDto updateUser(Long userId, String name, String mobileNumber, String newPin) { + Optional userOpt = userRepository.findById(userId); + if (userOpt.isEmpty()) { + return null; + } + + User user = userOpt.get(); + if (name != null) user.setName(name); + if (mobileNumber != null) user.setMobileNumber(mobileNumber); + + // Only update PIN if it is provided and valid + if (newPin != null && !newPin.isBlank()) { + if (newPin.length() == 6 && newPin.matches("^[0-9]{6}$")) { + user.setPinHash(passwordEncoder.encode(newPin)); + } + } + + userRepository.save(user); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()); + } + + /** + * Delete a user from the system. + */ + public void deleteUser(Long userId) { + userRepository.deleteById(userId); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 00000000..0b4598ba --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.application.name=backend +server.address=0.0.0.0 + +spring.datasource.url=jdbc:postgresql://localhost:5432/canteen_automation +spring.datasource.username=postgres +spring.datasource.password=Abiram@07 +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect diff --git a/backend/src/test/java/com/rit/canteen/sales/BackendApplicationTests.java b/backend/src/test/java/com/rit/canteen/sales/BackendApplicationTests.java new file mode 100644 index 00000000..17ba4e38 --- /dev/null +++ b/backend/src/test/java/com/rit/canteen/sales/BackendApplicationTests.java @@ -0,0 +1,13 @@ +package com.rit.canteen.sales; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackendApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..7dbf7ebf --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..5e6b472f --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..0fca6f04 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..f10cb77e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3973 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@types/date-fns": "^2.5.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^1.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.2", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.2", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz", + "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz", + "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz", + "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.1", + "@emnapi/runtime": "1.9.1", + "@napi-rs/wasm-runtime": "^1.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/date-fns": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz", + "integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", + "integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", + "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.123.0", + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-x64": "1.0.0-rc.13", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz", + "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.13", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..2314d961 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@types/date-fns": "^2.5.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^1.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.2", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.2", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..1c878468 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 00000000..f90339d8 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..3e558e88 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,97 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import Layout from './components/Layout.tsx'; +import Dashboard from './pages/Dashboard.tsx'; +import StoreDashboard from './pages/StoreDashboard.tsx'; +import BaseMenu from './pages/BaseMenu.tsx'; +import Products from './pages/Products.tsx'; +import Orders from './pages/Orders.tsx'; +import ArchivedOrders from './pages/ArchivedOrders.tsx'; +import Customers from './pages/Customers.tsx'; +import Login from './pages/Login.tsx'; +import Managers from './pages/Managers.tsx'; +import Stalls from './pages/Stalls.tsx'; +import Staff from './pages/Staff.tsx'; +import Terminals from './pages/Terminals.tsx'; +import Vendors from './pages/Vendors.tsx'; +import Purchases from './pages/Purchases.tsx'; +import VendorDashboard from './pages/VendorDashboard.tsx'; +import PurchaseAnalytics from './pages/PurchaseAnalytics.tsx'; +import Bills from './pages/Bills.tsx'; +import PurchaseSummary from './pages/PurchaseSummary.tsx'; + +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true'; + return isLoggedIn ? <>{children} : ; +}; + +const PlaceholderPage = ({ title }: { title: string }) => ( +
+

{title}

+
+

This page is under development

+

We're working hard to bring you the best experience for {title}.

+
+
+); + +function App() { + return ( + + + } /> + + + + }> + } /> + } /> + } /> + + {/* Sale */} + } /> + } /> + + {/* Customers */} + } /> + + {/* Purchases */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Inventory */} + } /> + } /> + } /> + + {/* Expense */} + } /> + } /> + + {/* Others */} + } /> + } /> + } /> + } /> + + {/* Stores */} + } /> + } /> + } /> + } /> + + {/* Promotions */} + } /> + } /> + } /> + + + + ); +} + +export default App; diff --git a/frontend/src/assets/college-logo.png b/frontend/src/assets/college-logo.png new file mode 100644 index 00000000..876b707f Binary files /dev/null and b/frontend/src/assets/college-logo.png differ diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 00000000..cc51a3d2 Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/ritchennai.webp b/frontend/src/assets/ritchennai.webp new file mode 100644 index 00000000..7682db22 Binary files /dev/null and b/frontend/src/assets/ritchennai.webp differ diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 00000000..5101b674 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AddTerminalModal.tsx b/frontend/src/components/AddTerminalModal.tsx new file mode 100644 index 00000000..b38816cc --- /dev/null +++ b/frontend/src/components/AddTerminalModal.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Monitor, MapPin, Lock } from 'lucide-react'; +import Numpad from './Numpad.tsx'; + +interface AddTerminalModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +const AddTerminalModal: React.FC = ({ isOpen, onClose, onSuccess }) => { + const [name, setName] = useState(''); + const [location, setLocation] = useState(''); + const [pin, setPin] = useState(''); + const [step, setStep] = useState(1); // 1: Details, 2: PIN + const [loading, setLoading] = useState(false); + + const handleSubmit = async () => { + setLoading(true); + try { + const response = await fetch('/api/terminals', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, location, pin }), + }); + if (response.ok) { + onSuccess(); + reset(); + onClose(); + } + } catch (error) { + console.error('Failed to create terminal:', error); + } finally { + setLoading(false); + } + }; + + const reset = () => { + setName(''); + setLocation(''); + setPin(''); + setStep(1); + }; + + if (!isOpen) return null; + + return ( +
+ +
+

Register New Terminal

+ +
+ +
+ + {step === 1 ? ( + +
+ + setName(e.target.value)} + placeholder="e.g. Counter 1" + className="w-full h-12 px-4 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#231651]/20 focus:border-[#231651] outline-none transition-all" + /> +
+ +
+ + setLocation(e.target.value)} + placeholder="e.g. Main Entrance" + className="w-full h-12 px-4 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#231651]/20 focus:border-[#231651] outline-none transition-all" + /> +
+ + +
+ ) : ( + +
+
+ +
+

Create Security PIN

+

This PIN is required to access the API Key

+
+ + + +
+ + +
+
+ )} +
+
+
+
+ ); +}; + +export default AddTerminalModal; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 00000000..3a321a54 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,49 @@ +import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react'; + +const Header = () => { + const currentDate = new Date().toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + return ( +
+
+
+ + +
+
+ +
+
+ + {currentDate} +
+ +
+ + + + + +
+
+
+ ); +}; + +export default Header; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 00000000..cd6b0878 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,21 @@ +import { Outlet } from 'react-router-dom'; +import Sidebar from './Sidebar.tsx'; +import Header from './Header.tsx'; + +const Layout = () => { + return ( +
+ +
+
+
+
+ +
+
+
+
+ ); +}; + +export default Layout; diff --git a/frontend/src/components/Numpad.tsx b/frontend/src/components/Numpad.tsx new file mode 100644 index 00000000..2a8c0cf0 --- /dev/null +++ b/frontend/src/components/Numpad.tsx @@ -0,0 +1,81 @@ +import { motion } from 'framer-motion'; +import { Delete } from 'lucide-react'; +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +interface NumpadProps { + value: string; + onChange: (value: string) => void; + maxLength?: number; +} + +const Numpad: React.FC = ({ value, onChange, maxLength = 4 }) => { + const handleNumberClick = (num: string) => { + if (value.length < maxLength) { + onChange(value + num); + } + }; + + const handleDelete = () => { + onChange(value.slice(0, -1)); + }; + + const handleClear = () => { + onChange(''); + }; + + const buttons = [ + '1', '2', '3', + '4', '5', '6', + '7', '8', '9', + 'C', '0', 'DEL' + ]; + + return ( +
+ {/* PIN Display Dots */} +
+ {[...Array(maxLength)].map((_, i) => ( + + ))} +
+ + {/* Grid */} +
+ {buttons.map((btn) => ( + { + if (btn === 'C') handleClear(); + else if (btn === 'DEL') handleDelete(); + else handleNumberClick(btn); + }} + className={cn( + "h-14 flex items-center justify-center rounded-xl text-lg font-bold transition-all", + btn === 'C' ? "text-red-500 hover:text-red-600" : + btn === 'DEL' ? "text-gray-500" : "text-[#231651]" + )} + > + {btn === 'DEL' ? : btn} + + ))} +
+
+ ); +}; + +export default Numpad; diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 00000000..a2fce1bc --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { + ChevronLeft, + ChevronRight, + ChevronDown +} from 'lucide-react'; + +interface PaginationProps { + currentPage: number; // 0-indexed + pageSize: number; + totalElements: number; + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; +} + +const Pagination: React.FC = ({ + currentPage, + pageSize, + totalElements, + onPageChange, + onPageSizeChange +}) => { + const totalPages = Math.ceil(totalElements / pageSize); + const startRange = totalElements === 0 ? 0 : currentPage * pageSize + 1; + const endRange = Math.min((currentPage + 1) * pageSize, totalElements); + + return ( +
+ {/* Rows per page */} +
+ Rows per page +
+ +
+ +
+
+
+ + {/* Range display */} +
+ {startRange}–{endRange} of {totalElements} +
+ + {/* Navigation buttons */} +
+ + +
+
+ ); +}; + +export default Pagination; diff --git a/frontend/src/components/PinVerificationModal.tsx b/frontend/src/components/PinVerificationModal.tsx new file mode 100644 index 00000000..42cd4855 --- /dev/null +++ b/frontend/src/components/PinVerificationModal.tsx @@ -0,0 +1,182 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Lock, Copy, CheckCircle2, AlertCircle } from 'lucide-react'; +import Numpad from './Numpad.tsx'; + +interface PinVerificationModalProps { + isOpen: boolean; + onClose: () => void; + terminalId: number | null; + terminalName: string; +} + +const PinVerificationModal: React.FC = ({ + isOpen, + onClose, + terminalId, + terminalName +}) => { + const [pin, setPin] = useState(''); + const [apiKey, setApiKey] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [copied, setCopied] = useState(false); + + const handleVerify = async (freshPin?: string) => { + if (!terminalId) return; + const pinToVerify = freshPin || pin; + if (!pinToVerify || pinToVerify.length < 4) return; + + setLoading(true); + setError(null); + try { + const response = await fetch(`/api/terminals/${terminalId}/verify-pin`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pin: pinToVerify }), + }); + + const data = await response.json(); + + if (response.ok) { + setApiKey(data.apiKey); + } else { + setError(data.message || 'Verification failed'); + setPin(''); + } + } catch (err) { + setError('Connection error'); + } finally { + setLoading(false); + } + }; + + const copyToClipboard = () => { + if (apiKey) { + navigator.clipboard.writeText(apiKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const handleClose = () => { + setPin(''); + setApiKey(null); + setError(null); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+ +
+

Security Authentication

+ +
+ +
+ + {!apiKey ? ( + +
+ +
+
+

{terminalName}

+

Enter your 4-digit security PIN to view the API Key

+
+ + {error && ( + + + {error} + + )} + + { + setPin(v); + if (v.length === 4) { + // Slight delay for visual feedback of the last dot + setTimeout(() => handleVerify(v), 100); + } + }} maxLength={4} /> + + +
+ ) : ( + +
+ +
+ +
+

Authentication Successful

+

Below is the API key for physical device integration

+
+ +
+
+
+ {apiKey} + +
+
+ +
+ +

+ This API key allows the terminal to communicate with the central server. Keep it secure and never share it publicly. +

+
+ + +
+ )} +
+
+
+
+ ); +}; + +export default PinVerificationModal; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 00000000..a8b8b459 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,312 @@ +import React, { useState } from 'react'; +import { NavLink, useLocation, useNavigate } from 'react-router-dom'; +import { + BarChart3, + ChevronRight, + CreditCard, + Gauge, + LayoutGrid, + Megaphone, + MessageSquare, + ShoppingCart, + Store, + Table2, + Users, + Wallet, + ShoppingBag, + Receipt, + Search, + LogOut +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; +import collegeLogo from '../assets/college-logo.png'; + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +interface MenuItem { + title: string; + icon: React.ElementType; + path?: string; + subMenu?: { title: string; path: string }[]; +} + +const menuItems: MenuItem[] = [ + { title: 'Dashboard', icon: Gauge, path: '/dashboard' }, + { title: 'Store Dashboard', icon: LayoutGrid, path: '/store-dashboard' }, + { + title: 'Sale', + icon: Receipt, + subMenu: [ + { title: 'Orders', path: '/sale/orders' }, + { title: 'Archived Bills', path: '/sale/archived-bills' } + ] + }, + { + title: 'Customers', + icon: Users, + subMenu: [ + { title: 'Orders', path: '/customers/orders' } + ] + }, + { + title: 'Vendors', + icon: ShoppingBag, + subMenu: [ + { title: 'Dashboard', path: '/purchases/dashboard' }, + { title: 'Orders', path: '/purchases/orders' }, + { title: 'Summary', path: '/purchases/summary' }, + { title: 'Bills', path: '/purchases/bills' }, + { title: 'Vendors', path: '/purchases/vendor' }, + { title: 'Purchase Analytics', path: '/purchases/analytics' } + ] + }, + { + title: 'Inventory', + icon: ShoppingCart, + subMenu: [ + { title: 'Base Items', path: '/inventory/base' }, + { title: 'Products', path: '/inventory/products' } + ] + }, + { + title: 'Expense', + icon: Wallet, + subMenu: [ + { title: 'Overview', path: '/expense/overview' }, + { title: 'Category', path: '/expense/category' } + ] + }, + { title: 'Reports', icon: BarChart3, path: '/reports' }, + { + title: 'Stores', + icon: Store, + subMenu: [ + { title: 'Terminals', path: '/stores/terminals' }, + { title: 'Managers', path: '/stores/managers' }, + { title: 'Staffs', path: '/stores/staffs' }, + { title: 'Stalls', path: '/stores/stalls' } + ] + }, + { title: 'Table', icon: Table2, path: '/table' }, + { title: 'Wallet', icon: CreditCard, path: '/wallet' }, + { + title: 'Promotions', + icon: Megaphone, + subMenu: [ + { title: 'Discounts', path: '/promotions/discounts' }, + { title: 'Coupon', path: '/promotions/coupon' }, + { title: 'CouponTemplate', path: '/promotions/coupon-template' } + ] + }, + { title: 'Feedback', icon: MessageSquare, path: '/feedback' }, +]; + +const Sidebar = () => { + const [openMenus, setOpenMenus] = useState([]); + const location = useLocation(); + const navigate = useNavigate(); + + // Permission Logic + const userRole = sessionStorage.getItem('userRole') || 'master'; + const userPermissions = JSON.parse(sessionStorage.getItem('userPermissions') || '[]'); + + const filteredMenuItems = menuItems.filter(item => { + if (userRole === 'master') return true; + + // Map title to permission ID + const permissionMap: Record = { + 'Dashboard': 'dashboard', + 'Store Dashboard': 'dashboard', + 'Sale': 'sale', + 'Customers': 'customers', + 'Vendors': 'purchases', + 'Inventory': 'inventory', + 'Expense': 'expense', + 'Reports': 'reports', + 'Stores': 'stores', + 'Table': 'table', + 'Wallet': 'wallet', + 'Promotions': 'promotions', + 'Feedback': 'feedback' + }; + + return userPermissions.includes(permissionMap[item.title]); + }); + + const handleLogout = () => { + sessionStorage.removeItem('isLoggedIn'); + sessionStorage.removeItem('userRole'); + sessionStorage.removeItem('userPermissions'); + navigate('/login'); + }; + + const toggleMenu = (title: string) => { + setOpenMenus((prev) => + prev.includes(title) + ? prev.filter((item) => item !== title) + : [...prev, title] + ); + }; + + // Check if a sub-menu should be open based on the current location + React.useEffect(() => { + const currentMenu = menuItems.find(item => + item.subMenu?.some(sub => location.pathname.startsWith(sub.path)) + ); + if (currentMenu && !openMenus.includes(currentMenu.title)) { + setOpenMenus(prev => [...prev, currentMenu.title]); + } + }, [location.pathname]); + + return ( +
+ {/* Branding Section */} +
+ Branding +
+ + {/* Premium Search Integration (Modern Dashboards usually have this) */} +
+
+
+ +
+ +
+
+ + {/* The main dashboard list starts here */} + +
+
    + {filteredMenuItems.map((item) => ( +
  • + {item.subMenu ? ( +
    + + + + {openMenus.includes(item.title) && ( + + {item.subMenu.map((sub) => ( +
  • + cn( + "flex items-center gap-3 py-2 px-4 rounded-lg text-[13px] font-medium transition-all group relative", + isActive + ? "text-white bg-[#521c4b] shadow-md shadow-[#521c4b]/10" + : "text-[#64748b] hover:text-[#1e293b] hover:translate-x-1" + )} + > + {({ isActive }) => ( + <> +
    + {sub.title} + {isActive && ( + + )} + + )} + +
  • + ))} + + )} + +
+ ) : ( + cn( + "flex items-center gap-3 p-2.5 rounded-xl text-sm font-semibold transition-all duration-200 group", + isActive + ? "bg-[#521c4b] text-white shadow-lg shadow-[#521c4b]/15" + : "text-[#475569] hover:bg-gray-50 hover:text-[#1e293b]" + )} + > + {({ isActive }) => ( + <> + + {item.title} + + )} + + )} + + ))} + +
+ +
+ +
+ + + + ); +}; + +export default ArchivedOrders; diff --git a/frontend/src/pages/BaseMenu.tsx b/frontend/src/pages/BaseMenu.tsx new file mode 100644 index 00000000..465e4367 --- /dev/null +++ b/frontend/src/pages/BaseMenu.tsx @@ -0,0 +1,400 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Plus, X, Search, Filter, MoreVertical, RefreshCw, Edit2, Power, PowerOff, ShoppingCart, Package, ExternalLink } from 'lucide-react'; + +interface BaseItem { + id: number; + name: string; + description: string; + active: boolean; +} + +interface Product { + id: number; + name: string; + price: number; + category: string; + active: boolean; +} + +const BaseMenu = () => { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [newItem, setNewItem] = useState({ name: '', description: '', active: true }); + const [openMenuId, setOpenMenuId] = useState(null); + + // Associated Products State + const [selectedBaseItem, setSelectedBaseItem] = useState(null); + const [associatedProducts, setAssociatedProducts] = useState([]); + const [productsLoading, setProductsLoading] = useState(false); + const [showProductsModal, setShowProductsModal] = useState(false); + + const menuRef = useRef(null); + + useEffect(() => { + fetchItems(); + + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpenMenuId(null); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const fetchItems = async () => { + setLoading(true); + try { + const response = await fetch('http://localhost:8080/api/base-items'); + const data = await response.json(); + setItems(data); + } catch (error) { + console.error('Error fetching items:', error); + } finally { + setLoading(false); + } + }; + + const fetchAssociatedProducts = async (baseItem: BaseItem) => { + setSelectedBaseItem(baseItem); + setShowProductsModal(true); + setProductsLoading(true); + try { + const response = await fetch(`http://localhost:8080/api/products/category/${encodeURIComponent(baseItem.name)}`); + const data = await response.json(); + setAssociatedProducts(data); + } catch (error) { + console.error('Error fetching associated products:', error); + } finally { + setProductsLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const url = editingItem + ? `http://localhost:8080/api/base-items/${editingItem.id}` + : 'http://localhost:8080/api/base-items'; + const method = editingItem ? 'PUT' : 'POST'; + + try { + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newItem), + }); + if (response.ok) { + setShowModal(false); + setEditingItem(null); + setNewItem({ name: '', description: '', active: true }); + fetchItems(); + } + } catch (error) { + console.error('Error saving item:', error); + } + }; + + const handleEdit = (item: BaseItem) => { + setEditingItem(item); + setNewItem({ name: item.name, description: item.description, active: item.active }); + setShowModal(true); + setOpenMenuId(null); + }; + + const handleToggleActive = async (item: BaseItem) => { + try { + const response = await fetch(`http://localhost:8080/api/base-items/${item.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...item, active: !item.active }), + }); + if (response.ok) { + fetchItems(); + setOpenMenuId(null); + } + } catch (error) { + console.error('Error toggling active status:', error); + } + }; + + return ( +
+
+
+

Base Items

+

Manage your base inventory items

+
+ +
+ + {/* Filters Bar */} +
+
+
+ + +
+ +
+ +
+ + {/* Table */} +
+
+ + + + + + + + + + + + + {loading ? ( + + + + ) : items.length === 0 ? ( + + + + ) : ( + items.map((item) => ( + + + + + + + + + )) + )} + +
IDItem NameDescriptionProductsStatusActions
+
+ +

Loading items...

+
+
+

No items found

+

Click "Add New Item" to create your first item.

+
#{item.id} + {item.name} + {item.description} + + + +
+ {item.active ? 'Active' : 'Inactive'} + +
+ + + {openMenuId === item.id && ( +
+ + +
+ )} +
+
+
+ + {/* Associated Products Modal */} + {showProductsModal && ( +
+
+
+
+

Products for: {selectedBaseItem?.name}

+

Showing all associated products in this category

+
+ +
+ +
+ {productsLoading ? ( +
+ +

Fetching associated products...

+
+ ) : associatedProducts.length === 0 ? ( +
+
+ +
+

No Products Found

+

There are no products currently linked to the "{selectedBaseItem?.name}" category.

+
+ ) : ( +
+ {associatedProducts.map(product => ( +
+
+
+ +
+
+

{product.name}

+ + {product.active ? 'Active' : 'Inactive'} + +
+
+
+

β‚Ή{product.price}

+

Sale Price

+
+
+ ))} +
+ )} +
+ +
+ +
+
+
+ )} + + {/* Standard Modal (Add/Edit) */} + {showModal && ( +
+
+
+

+ {editingItem ? 'Edit Base Item' : 'Add New Base Item'} +

+ +
+
+
+ + setNewItem({ ...newItem, name: e.target.value })} + placeholder="e.g. Fresh Tomato" + className="w-full px-4 py-2.5 border border-[#e2e8f0] rounded-xl focus:outline-none focus:border-[#231651] focus:ring-1 focus:ring-[#231651] transition-all text-sm" + /> +
+
+ +