Replace submodules with full folder contents
20
Machine-Backend/.env
Normal file
@ -0,0 +1,20 @@
|
||||
SECRET_KEY=9d80e4410a5572c153bc8059e23b7fb6e2eeeb0bf1c4aff851cd60a3220efc6a
|
||||
JWT_SECRET_KEY=e57e8ed9d7af16276c128027d3c381380f39446fbe16bbd6fa8c73b0f64ac023
|
||||
|
||||
PAYU_MERCHANT_KEY=VqvG3m
|
||||
PAYU_MERCHANT_SALT=DusXSSjqqSMTPSpw32hlFXF6LKY2Zm3y
|
||||
PAYU_SUCCESS_URL=http://localhost:4200/payment-success
|
||||
PAYU_FAILURE_URL=http://localhost:4200/payment-failure
|
||||
|
||||
FLASK_ENV=production
|
||||
|
||||
MYSQL_HOST=db
|
||||
MYSQL_USER=vendinguser
|
||||
MYSQL_PASSWORD=vendingpass
|
||||
MYSQL_DATABASE=vending
|
||||
|
||||
SQLITE_DB_PATH=machines
|
||||
|
||||
BREVO_SMTP_EMAIL=smukeshsn2000@gmail.com
|
||||
BREVO_SMTP_KEY=your-brevo-smtp-key
|
||||
BREVO_SENDER_EMAIL=smukeshsn2000@gmail.com
|
||||
23
Machine-Backend/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
# Backend Dockerfile
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (for mysqlclient or pymysql)
|
||||
RUN apt-get update && apt-get install -y gcc python3-dev default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy dependencies
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Expose Flask port
|
||||
EXPOSE 5000
|
||||
|
||||
# Environment (set via docker-compose)
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Start the Flask app
|
||||
CMD ["python", "run.py"]
|
||||
86
Machine-Backend/app/__init__.py
Normal file
@ -0,0 +1,86 @@
|
||||
import os
|
||||
from flask import Flask, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
db = SQLAlchemy()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
# CORS Configuration
|
||||
CORS(app, resources={
|
||||
r"/*": {
|
||||
"origins": ["http://localhost:4200", "http://localhost:4300"],
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
"allow_headers": ["Content-Type", "Authorization"],
|
||||
"supports_credentials": True
|
||||
}
|
||||
})
|
||||
|
||||
# Ensure directories exist
|
||||
instance_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'instance'))
|
||||
uploads_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Uploads'))
|
||||
|
||||
for path in [instance_path, uploads_path]:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
app.config['UPLOAD_FOLDER'] = uploads_path
|
||||
|
||||
# Database configuration
|
||||
flask_env = os.getenv('FLASK_ENV', 'development').lower()
|
||||
|
||||
if flask_env == 'production':
|
||||
mysql_host = os.getenv('MYSQL_HOST')
|
||||
mysql_user = os.getenv('MYSQL_USER')
|
||||
mysql_password = os.getenv('MYSQL_PASSWORD')
|
||||
mysql_db = os.getenv('MYSQL_DATABASE')
|
||||
|
||||
if not all([mysql_host, mysql_user, mysql_password, mysql_db]):
|
||||
raise ValueError("Missing required MySQL environment variables")
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
||||
f'mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:3306/{mysql_db}'
|
||||
)
|
||||
else:
|
||||
sqlite_db_path = os.getenv('SQLITE_DB_PATH', 'machines.db')
|
||||
db_path = os.path.join(instance_path, sqlite_db_path)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SQLALCHEMY_ECHO'] = os.getenv('SQLALCHEMY_ECHO', 'False').lower() == 'true'
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Import models
|
||||
from app.models.models import Machine, User, Product, VendingSlot, Transaction
|
||||
|
||||
# Create tables
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
print(f"✓ Database initialized ({flask_env})")
|
||||
|
||||
# FIXED: Serial service connection BEFORE return
|
||||
from app.services.services import serial_service
|
||||
try:
|
||||
connected = serial_service.connect_to_machine('/dev/ttyUSB0', 9600)
|
||||
if connected:
|
||||
print("✓ Vending machine connected successfully")
|
||||
else:
|
||||
print("✗ Failed to connect to vending machine")
|
||||
except Exception as e:
|
||||
print(f"⚠ Serial connection error: {e}")
|
||||
|
||||
# Register blueprints
|
||||
from app.routes.routes import bp
|
||||
app.register_blueprint(bp, url_prefix='/')
|
||||
|
||||
@app.route('/uploads/<filename>')
|
||||
def uploaded_file(filename):
|
||||
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
return app
|
||||
BIN
Machine-Backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
12
Machine-Backend/app/instance/client_secrets.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"web": {
|
||||
"issuer": "http://localhost:8080/realms/myrealm",
|
||||
"auth_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
|
||||
"client_id": "my-flask-app",
|
||||
"client_secret": "7kAD4BvMWbE7TP5f79sVX4NzzvsGyrFd",
|
||||
"redirect_uris": ["http://localhost:5000/oidc/callback"],
|
||||
"userinfo_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo",
|
||||
"token_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
BIN
Machine-Backend/app/instance/machines.db
Normal file
1
Machine-Backend/app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .models import Machine, User, Product, VendingSlot
|
||||
BIN
Machine-Backend/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/machine.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/models.cpython-313.pyc
Normal file
213
Machine-Backend/app/models/models.py
Normal file
@ -0,0 +1,213 @@
|
||||
from app import db
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Machine Model
|
||||
class Machine(db.Model):
|
||||
__tablename__ = 'machines'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
machine_id = db.Column(db.String(10), unique=True, nullable=False)
|
||||
machine_model = db.Column(db.String(100), nullable=False)
|
||||
machine_type = db.Column(db.String(100), nullable=False)
|
||||
client_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
client_name = db.Column(db.String(100), nullable=True)
|
||||
branch_id = db.Column(db.String(10), nullable=False)
|
||||
branch_name = db.Column(db.String(100), nullable=False)
|
||||
operation_status = db.Column(db.String(50), nullable=False)
|
||||
connection_status = db.Column(db.String(50), nullable=False)
|
||||
created_on = db.Column(db.String(20), nullable=False)
|
||||
password = db.Column(db.String(128), nullable=False)
|
||||
slots = db.relationship('VendingSlot', backref='machine', lazy=True)
|
||||
client = db.relationship('User', backref='machines')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'machine_id': self.machine_id,
|
||||
'machine_model': self.machine_model,
|
||||
'machine_type': self.machine_type,
|
||||
'client_id': self.client_id,
|
||||
'client_name': self.client.username if self.client else self.client_name,
|
||||
'branch_id': self.branch_id,
|
||||
'branch_name': self.branch_name,
|
||||
'operation_status': self.operation_status,
|
||||
'connection_status': self.connection_status,
|
||||
'created_on': self.created_on,
|
||||
'password': self.password
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
|
||||
# User Model - UPDATED with proper password hashing
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
username = db.Column(db.String(50), nullable=False)
|
||||
email = db.Column(db.String(100), nullable=False, unique=True)
|
||||
contact = db.Column(db.String(20), nullable=False)
|
||||
roles = db.Column(db.String(50), nullable=False)
|
||||
user_status = db.Column(db.String(50), nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False) # Increased length for hash
|
||||
|
||||
# File storage fields
|
||||
photo = db.Column(db.String(255), nullable=True)
|
||||
company_logo = db.Column(db.String(255), nullable=True)
|
||||
documents = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert user object to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'contact': self.contact,
|
||||
'roles': self.roles,
|
||||
'user_status': self.user_status,
|
||||
'photo': self.photo,
|
||||
'company_logo': self.company_logo,
|
||||
'documents': json.loads(self.documents) if self.documents else [],
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None,
|
||||
'machines': [m.to_dict() for m in self.machines] if hasattr(self, 'machines') else []
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the password"""
|
||||
self.password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
print(f"Password hashed for user {self.username}")
|
||||
|
||||
def check_password(self, password):
|
||||
"""Verify password against hash"""
|
||||
result = check_password_hash(self.password, password)
|
||||
print(f"Password check for {self.username}: {result}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def generate_user_id(username, email):
|
||||
"""Generate unique user ID from username and email"""
|
||||
username_part = ''.join(filter(str.isalnum, username[:4])).upper()
|
||||
email_part = ''.join(filter(str.isalnum, email.split('@')[0][:4])).upper()
|
||||
timestamp = str(int(time.time()))[-4:]
|
||||
user_id = f"USR-{username_part}{email_part}{timestamp}"
|
||||
|
||||
# Ensure uniqueness
|
||||
counter = 1
|
||||
original_id = user_id
|
||||
while User.query.filter_by(user_id=user_id).first():
|
||||
user_id = f"{original_id}{counter}"
|
||||
counter += 1
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
# Product Model
|
||||
class Product(db.Model):
|
||||
__tablename__ = 'products'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(db.String(10), unique=True, nullable=False)
|
||||
product_name = db.Column(db.String(100), nullable=False)
|
||||
price = db.Column(db.Float, nullable=False)
|
||||
product_image = db.Column(db.String(255), nullable=False)
|
||||
created_date = db.Column(db.String(20), nullable=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product_name,
|
||||
'price': str(self.price),
|
||||
'product_image': self.product_image,
|
||||
'created_date': self.created_date
|
||||
}
|
||||
|
||||
|
||||
# VendingSlot Model
|
||||
class VendingSlot(db.Model):
|
||||
__tablename__ = 'vending_slots'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
|
||||
row_id = db.Column(db.String(1), nullable=False)
|
||||
slot_name = db.Column(db.String(3), nullable=False)
|
||||
enabled = db.Column(db.Boolean, default=True)
|
||||
product_id = db.Column(db.String(10), db.ForeignKey('products.product_id'), nullable=True)
|
||||
units = db.Column(db.Integer, default=0)
|
||||
price = db.Column(db.String(10), default="N/A")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'name': self.slot_name,
|
||||
'enabled': self.enabled,
|
||||
'productId': self.product_id,
|
||||
'units': self.units,
|
||||
'price': self.price
|
||||
}
|
||||
|
||||
# Transaction Model
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
transaction_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
|
||||
product_name = db.Column(db.String(100), nullable=False)
|
||||
quantity = db.Column(db.Integer, nullable=False)
|
||||
amount = db.Column(db.Float, nullable=False)
|
||||
payment_type = db.Column(db.String(50), nullable=False) # wallet/qr/card
|
||||
status = db.Column(db.String(50), nullable=False) # Success/Dispense failed/Unprocessed
|
||||
amount_receiving_status = db.Column(db.String(50), nullable=True) # Dispense failed refund
|
||||
return_amount = db.Column(db.Float, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
machine = db.relationship('Machine', backref='transactions')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'machine_id': self.machine_id,
|
||||
'product_name': self.product_name,
|
||||
'quantity': self.quantity,
|
||||
'amount': self.amount,
|
||||
'payment_type': self.payment_type,
|
||||
'status': self.status,
|
||||
'amount_receiving_status': self.amount_receiving_status,
|
||||
'return_amount': self.return_amount,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None
|
||||
}
|
||||
# Add to your models.py
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.String(255))
|
||||
permissions = db.Column(db.Text) # JSON string of permission IDs
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'permissions': json.loads(self.permissions) if self.permissions else [],
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
1
Machine-Backend/app/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import bp
|
||||
BIN
Machine-Backend/app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc
Normal file
1724
Machine-Backend/app/routes/routes.py
Normal file
1
Machine-Backend/app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .services import MachineService, UserService, ProductService
|
||||
967
Machine-Backend/app/services/services.py
Normal file
@ -0,0 +1,967 @@
|
||||
import uuid
|
||||
import datetime
|
||||
import datetime
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, Dict, Any
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from app import db
|
||||
from app.models.models import Machine, User, Product, VendingSlot, Transaction, Role
|
||||
from flask import current_app
|
||||
import secrets
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
class SerialCommunicationService:
|
||||
def __init__(self):
|
||||
self.serial_port: Optional[serial.Serial] = None
|
||||
self.is_connected = False
|
||||
self.response_buffer = ""
|
||||
self.last_response = None
|
||||
self.response_lock = threading.Lock()
|
||||
|
||||
def connect_to_machine(self, port: str = '/dev/ttyUSB0', baudrate: int = 9600) -> bool:
|
||||
"""
|
||||
Connect to vending machine via serial port
|
||||
Common ports:
|
||||
- Linux: /dev/ttyUSB0, /dev/ttyACM0
|
||||
- Windows: COM3, COM4, etc.
|
||||
- macOS: /dev/cu.usbserial-*
|
||||
"""
|
||||
try:
|
||||
self.serial_port = serial.Serial(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=1 # 1 second timeout for reads
|
||||
)
|
||||
|
||||
# Test connection with a status command
|
||||
time.sleep(2) # Allow time for Arduino to reset
|
||||
self.serial_port.write(b'STATUS\n')
|
||||
time.sleep(1)
|
||||
|
||||
if self.serial_port.in_waiting > 0:
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
print(f"Machine connected successfully. Status: {response}")
|
||||
self.is_connected = True
|
||||
return True
|
||||
else:
|
||||
print("No response from machine")
|
||||
self.serial_port.close()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to connect to machine: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def send_dispense_command(self, slot_id: str, timeout: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
Send dispense command and wait for drop sensor response
|
||||
"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Not connected to machine',
|
||||
'response': 'NO_CONNECTION'
|
||||
}
|
||||
|
||||
try:
|
||||
# Clear any pending data
|
||||
self.serial_port.reset_input_buffer()
|
||||
|
||||
# Send dispense command
|
||||
command = f"DISPENSE:{slot_id}\n"
|
||||
self.serial_port.write(command.encode('utf-8'))
|
||||
print(f"Sent command: {command.strip()}")
|
||||
|
||||
# Wait for response with timeout
|
||||
start_time = time.time()
|
||||
response = ""
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
if self.serial_port.in_waiting > 0:
|
||||
line = self.serial_port.readline().decode('utf-8').strip()
|
||||
print(f"Received: {line}")
|
||||
|
||||
if line in ['PRODUCT_DISPENSED', 'DISPENSE_FAILED', 'SLOT_EMPTY', 'MOTOR_ERROR']:
|
||||
response = line
|
||||
break
|
||||
|
||||
time.sleep(0.1) # Small delay to prevent busy waiting
|
||||
|
||||
if not response:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Timeout waiting for drop sensor',
|
||||
'response': 'TIMEOUT'
|
||||
}
|
||||
|
||||
success = response == 'PRODUCT_DISPENSED'
|
||||
return {
|
||||
'success': success,
|
||||
'response': response,
|
||||
'slot_id': slot_id,
|
||||
'message': self._get_response_message(response, slot_id)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during dispensing: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'response': 'COMMUNICATION_ERROR'
|
||||
}
|
||||
|
||||
def _get_response_message(self, response: str, slot_id: str) -> str:
|
||||
"""Get user-friendly message for response codes"""
|
||||
messages = {
|
||||
'PRODUCT_DISPENSED': f'Product successfully dispensed from slot {slot_id}',
|
||||
'DISPENSE_FAILED': f'Dispensing failed - no product detected from slot {slot_id}',
|
||||
'SLOT_EMPTY': f'Slot {slot_id} is empty',
|
||||
'MOTOR_ERROR': f'Motor malfunction in slot {slot_id}',
|
||||
'TIMEOUT': f'Timeout - no response from slot {slot_id}',
|
||||
'COMMUNICATION_ERROR': 'Communication error with machine'
|
||||
}
|
||||
return messages.get(response, f'Unknown response: {response}')
|
||||
|
||||
def check_machine_status(self) -> Dict[str, Any]:
|
||||
"""Check overall machine status"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {'status': 'disconnected', 'message': 'Not connected to machine'}
|
||||
|
||||
try:
|
||||
self.serial_port.reset_input_buffer()
|
||||
self.serial_port.write(b'STATUS\n')
|
||||
time.sleep(1)
|
||||
|
||||
if self.serial_port.in_waiting > 0:
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
return {
|
||||
'status': 'connected',
|
||||
'response': response,
|
||||
'message': 'Machine is responding'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'no_response',
|
||||
'message': 'Machine not responding'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'message': 'Communication error'
|
||||
}
|
||||
|
||||
def reset_machine(self) -> Dict[str, Any]:
|
||||
"""Send reset command to clear any error states"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {'success': False, 'error': 'Not connected to machine'}
|
||||
|
||||
try:
|
||||
self.serial_port.write(b'RESET\n')
|
||||
time.sleep(2)
|
||||
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
return {
|
||||
'success': True,
|
||||
'response': response,
|
||||
'message': 'Reset command sent'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from serial port"""
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
self.serial_port.close()
|
||||
self.is_connected = False
|
||||
print("Disconnected from vending machine")
|
||||
|
||||
# Global instance
|
||||
serial_service = SerialCommunicationService()
|
||||
|
||||
# Machine Service (UNCHANGED)
|
||||
class MachineService:
|
||||
@staticmethod
|
||||
def generate_id(prefix):
|
||||
return f"{prefix}{uuid.uuid4().hex[:8].upper()}"
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_password():
|
||||
while True:
|
||||
password = secrets.token_hex(4).upper() # e.g., "A1B2C3D4"
|
||||
if not Machine.query.filter_by(password=password).first():
|
||||
return password
|
||||
|
||||
@staticmethod
|
||||
def validate_machine_data(data):
|
||||
required_fields = ['machine_model', 'machine_type', 'client_name', 'branch_name', 'operation_status', 'connection_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
status_mapping = {
|
||||
'connected': 'connected',
|
||||
'disconnected': 'disconnected',
|
||||
'maintenance':'maintenance',
|
||||
'active': 'active',
|
||||
'inactive': 'inactive'
|
||||
}
|
||||
|
||||
if data['operation_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['operation_status'] = status_mapping[data['operation_status']]
|
||||
|
||||
if data['connection_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['connection_status'] = status_mapping[data['connection_status']]
|
||||
|
||||
@staticmethod
|
||||
def get_all_machines():
|
||||
return Machine.query.all()
|
||||
|
||||
@staticmethod
|
||||
def get_machine_slots(machine_id: str):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first()
|
||||
if not machine:
|
||||
return None
|
||||
|
||||
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
|
||||
|
||||
product_ids = [slot.product_id for slot in slots if slot.product_id]
|
||||
products = {p.product_id: p for p in Product.query.filter(Product.product_id.in_(product_ids)).all()} if product_ids else {}
|
||||
|
||||
return [{
|
||||
'slot_name': slot.row_id,
|
||||
'column': int(slot.slot_name[1:]) if slot.slot_name[1:].isdigit() else 1,
|
||||
'enabled': slot.enabled,
|
||||
'product_id': slot.product_id,
|
||||
'units': slot.units or 0,
|
||||
'price': slot.price or '0.0',
|
||||
'product_name': products.get(slot.product_id, {'product_name': 'N/A'}).product_name if slot.product_id else 'N/A',
|
||||
'product_image': products.get(slot.product_id, {'product_image': None}).product_image if slot.product_id else None
|
||||
} for slot in slots]
|
||||
|
||||
@staticmethod
|
||||
def create_machine(data):
|
||||
# Modified validation to accept client_id instead of client_name
|
||||
required_fields = ['machine_model', 'machine_type', 'client_id', 'branch_name', 'operation_status', 'connection_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
status_mapping = {
|
||||
'connected': 'connected',
|
||||
'disconnected': 'disconnected',
|
||||
'maintenance':'maintenance',
|
||||
'active': 'active',
|
||||
'inactive': 'inactive'
|
||||
}
|
||||
|
||||
if data['operation_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['operation_status'] = status_mapping[data['operation_status']]
|
||||
|
||||
if data['connection_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['connection_status'] = status_mapping[data['connection_status']]
|
||||
|
||||
machine_id = MachineService.generate_id("M")
|
||||
branch_id = MachineService.generate_id("B")
|
||||
created_on = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
password = MachineService.generate_unique_password()
|
||||
|
||||
# Validate client_id exists in users
|
||||
client_id = data.get('client_id')
|
||||
if not client_id or not User.query.get(client_id):
|
||||
raise ValueError("Invalid or missing client_id")
|
||||
|
||||
# Get client_name from user
|
||||
user = User.query.get(client_id)
|
||||
client_name = user.username
|
||||
|
||||
new_machine = Machine(
|
||||
machine_id=machine_id,
|
||||
machine_model=data['machine_model'],
|
||||
machine_type=data['machine_type'],
|
||||
client_id=client_id,
|
||||
client_name=client_name, # Set client_name from user record
|
||||
branch_id=branch_id,
|
||||
branch_name=data['branch_name'],
|
||||
operation_status=data['operation_status'],
|
||||
connection_status=data['connection_status'],
|
||||
created_on=created_on,
|
||||
password=password
|
||||
)
|
||||
|
||||
db.session.add(new_machine)
|
||||
db.session.commit()
|
||||
return new_machine
|
||||
|
||||
@staticmethod
|
||||
def update_machine(id, data):
|
||||
machine = Machine.query.get_or_404(id)
|
||||
|
||||
if any(field in data for field in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']):
|
||||
MachineService.validate_machine_data({k: data.get(k, getattr(machine, k)) for k in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']})
|
||||
|
||||
machine.machine_model = data.get('machine_model', machine.machine_model)
|
||||
machine.machine_type = data.get('machine_type', machine.machine_type)
|
||||
if 'client_id' in data:
|
||||
client_id = data['client_id']
|
||||
if not User.query.get(client_id):
|
||||
raise ValueError("Invalid client_id")
|
||||
machine.client_id = client_id
|
||||
machine.branch_name = data.get('branch_name', machine.branch_name)
|
||||
machine.operation_status = data.get('operation_status', machine.operation_status)
|
||||
machine.connection_status = data.get('connection_status', machine.connection_status)
|
||||
|
||||
db.session.commit()
|
||||
return machine
|
||||
|
||||
@staticmethod
|
||||
def delete_machine(id):
|
||||
machine = Machine.query.get_or_404(id)
|
||||
db.session.delete(machine)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_machine_by_id(id):
|
||||
return Machine.query.get_or_404(id)
|
||||
|
||||
@staticmethod
|
||||
def get_vending_state(machine_id):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
|
||||
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
|
||||
|
||||
vending_rows = {}
|
||||
for slot in slots:
|
||||
if slot.row_id not in vending_rows:
|
||||
vending_rows[slot.row_id] = []
|
||||
vending_rows[slot.row_id].append(slot.to_dict())
|
||||
|
||||
return {
|
||||
'vendingRows': [{'rowId': row_id, 'slots': slots} for row_id, slots in vending_rows.items()],
|
||||
'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_vending_state(machine_id, vending_rows):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
|
||||
VendingSlot.query.filter_by(machine_id=machine_id).delete()
|
||||
|
||||
for row in vending_rows:
|
||||
row_id = row['rowId']
|
||||
for slot_data in row['slots']:
|
||||
new_slot = VendingSlot(
|
||||
machine_id=machine_id,
|
||||
row_id=row_id,
|
||||
slot_name=slot_data['name'],
|
||||
enabled=slot_data['enabled'],
|
||||
product_id=slot_data['productId'],
|
||||
units=slot_data['units'],
|
||||
price=slot_data['price']
|
||||
)
|
||||
db.session.add(new_slot)
|
||||
|
||||
db.session.commit()
|
||||
return {'success': True, 'message': 'Vending state updated', 'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
||||
# User Service (MODIFIED FOR BREVO SMTP WITH ENV VARIABLES)
|
||||
# Updated UserService in services.py
|
||||
class UserService:
|
||||
@staticmethod
|
||||
def generate_unique_password():
|
||||
"""Generate a secure random password"""
|
||||
return secrets.token_hex(4).upper() # 8 character hex password
|
||||
|
||||
@staticmethod
|
||||
def save_file(file, folder='user_uploads'):
|
||||
"""Save uploaded file and return path"""
|
||||
if not file:
|
||||
return None
|
||||
|
||||
filename = f"{uuid.uuid4().hex}_{file.filename}"
|
||||
upload_folder = os.path.join(current_app.config['UPLOAD_FOLDER'], folder)
|
||||
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
return f"/uploads/{folder}/{filename}"
|
||||
|
||||
@staticmethod
|
||||
def save_multiple_documents(files, metadata_json=None):
|
||||
"""Save multiple document files with metadata"""
|
||||
document_paths = []
|
||||
|
||||
metadata_list = []
|
||||
if metadata_json:
|
||||
try:
|
||||
metadata_list = json.loads(metadata_json)
|
||||
print(f"Parsed metadata: {metadata_list}")
|
||||
except Exception as e:
|
||||
print(f"Error parsing metadata: {e}")
|
||||
metadata_list = []
|
||||
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for idx, file in enumerate(files):
|
||||
if file and hasattr(file, 'filename'):
|
||||
try:
|
||||
path = UserService.save_file(file, 'user_documents')
|
||||
if path:
|
||||
metadata = metadata_list[idx] if idx < len(metadata_list) else {}
|
||||
|
||||
document_paths.append({
|
||||
'filename': file.filename,
|
||||
'path': path,
|
||||
'uploaded_at': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'document_type': metadata.get('document_type', 'other'),
|
||||
'document_type_other': metadata.get('document_type_other', '')
|
||||
})
|
||||
print(f"Saved document: {file.filename}")
|
||||
except Exception as e:
|
||||
print(f"Error saving file {file.filename}: {e}")
|
||||
continue
|
||||
|
||||
return document_paths
|
||||
|
||||
@staticmethod
|
||||
def send_email_notification(username, email, contact, password, user_id):
|
||||
"""Send email notification with user credentials"""
|
||||
subject = "Your Account Credentials"
|
||||
body = f"""
|
||||
Dear {username},
|
||||
|
||||
Your account has been created successfully!
|
||||
|
||||
Login Credentials:
|
||||
==================
|
||||
User ID: {user_id}
|
||||
Username: {username}
|
||||
Email: {email}
|
||||
Contact: {contact}
|
||||
Password: {password}
|
||||
|
||||
Important Security Notice:
|
||||
- Please change your password after first login
|
||||
- Keep this information secure and confidential
|
||||
- Do not share your credentials with anyone
|
||||
|
||||
You can login using either your email or username along with your password.
|
||||
|
||||
Best regards,
|
||||
System Administration Team
|
||||
"""
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = os.getenv('BREVO_SENDER_EMAIL')
|
||||
msg['To'] = email
|
||||
|
||||
smtp_email = os.getenv('BREVO_SMTP_EMAIL')
|
||||
smtp_key = os.getenv('BREVO_SMTP_KEY')
|
||||
|
||||
if not all([smtp_email, smtp_key, msg['From']]):
|
||||
print("Error: Brevo SMTP configuration missing in .env")
|
||||
return False
|
||||
|
||||
try:
|
||||
with smtplib.SMTP('smtp-relay.brevo.com', 587) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_email, smtp_key)
|
||||
server.send_message(msg)
|
||||
print(f"✓ Email sent successfully to {email}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to send email to {email}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def log_sms_notification(contact, username, email, password, user_id):
|
||||
"""Log SMS notification (for future SMS integration)"""
|
||||
notification = {
|
||||
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"contact": contact,
|
||||
"password": password,
|
||||
"message": f"Your account credentials: User ID: {user_id}, Username: {username}, Email: {email}, Password: {password}"
|
||||
}
|
||||
|
||||
notifications_file = os.path.join(current_app.root_path, "sms_notifications.json")
|
||||
|
||||
try:
|
||||
notifications = []
|
||||
if os.path.exists(notifications_file):
|
||||
with open(notifications_file, "r") as f:
|
||||
notifications = json.load(f)
|
||||
|
||||
notifications.append(notification)
|
||||
|
||||
with open(notifications_file, "w") as f:
|
||||
json.dump(notifications, f, indent=4)
|
||||
|
||||
print(f"✓ SMS notification logged for {contact}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to log SMS notification: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_user_data(data):
|
||||
"""Validate user data before creating/updating"""
|
||||
import re
|
||||
|
||||
required_fields = ['username', 'email', 'contact', 'roles', 'user_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
# Email validation - FIXED: Added closing quote
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_regex, data['email']):
|
||||
raise ValueError("Invalid email format")
|
||||
|
||||
@staticmethod
|
||||
def get_all_users():
|
||||
"""Get all users from database"""
|
||||
return User.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_user(data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
|
||||
"""
|
||||
Create new user with proper password hashing
|
||||
Password must be provided in data
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"CREATING NEW USER")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Validate user data
|
||||
UserService.validate_user_data(data)
|
||||
|
||||
# CHECK: Password must be provided
|
||||
if 'password' not in data or not data['password']:
|
||||
raise ValueError("Password is required")
|
||||
|
||||
# Generate user ID only
|
||||
user_id = User.generate_user_id(data['username'], data['email'])
|
||||
password = data['password'] # Use provided password, not auto-generated
|
||||
|
||||
print(f"Generated User ID: {user_id}")
|
||||
print(f"Password provided by admin")
|
||||
|
||||
# Handle file uploads
|
||||
photo_path = UserService.save_file(photo_file, 'user_photos') if photo_file else None
|
||||
logo_path = UserService.save_file(logo_file, 'company_logos') if logo_file else None
|
||||
documents = UserService.save_multiple_documents(document_files, documents_metadata) if document_files else []
|
||||
|
||||
# Create new user
|
||||
new_user = User(
|
||||
user_id=user_id,
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
contact=data['contact'],
|
||||
roles=data['roles'],
|
||||
user_status=data['user_status'],
|
||||
photo=photo_path,
|
||||
company_logo=logo_path,
|
||||
documents=json.dumps(documents)
|
||||
)
|
||||
|
||||
# Set password (this will hash it automatically)
|
||||
new_user.set_password(password)
|
||||
print(f"Password hashed and set for user")
|
||||
|
||||
# Save to database
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
print(f"✓ User created successfully with ID: {new_user.id}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Optional: Send email notification with the password they set
|
||||
# UserService.send_email_notification(...)
|
||||
|
||||
return new_user
|
||||
|
||||
@staticmethod
|
||||
def update_user(id, data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
|
||||
"""
|
||||
Update existing user
|
||||
Password is only updated if explicitly provided in data
|
||||
"""
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"UPDATING USER: {user.username}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Validate only if fields are being updated
|
||||
if any(field in data for field in ['username', 'email', 'contact', 'roles', 'user_status']):
|
||||
UserService.validate_user_data({
|
||||
'username': data.get('username', user.username),
|
||||
'email': data.get('email', user.email),
|
||||
'contact': data.get('contact', user.contact),
|
||||
'roles': data.get('roles', user.roles),
|
||||
'user_status': data.get('user_status', user.user_status)
|
||||
})
|
||||
|
||||
# Update basic fields
|
||||
user.username = data.get('username', user.username)
|
||||
user.email = data.get('email', user.email)
|
||||
user.contact = data.get('contact', user.contact)
|
||||
user.roles = data.get('roles', user.roles)
|
||||
user.user_status = data.get('user_status', user.user_status)
|
||||
|
||||
# Update password if provided (will be hashed automatically)
|
||||
if 'password' in data and data['password']:
|
||||
print(f"Updating password for user")
|
||||
user.set_password(data['password'])
|
||||
|
||||
# Handle file updates
|
||||
if photo_file:
|
||||
if user.photo:
|
||||
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.photo.replace('/uploads/', ''))
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
user.photo = UserService.save_file(photo_file, 'user_photos')
|
||||
|
||||
if logo_file:
|
||||
if user.company_logo:
|
||||
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.company_logo.replace('/uploads/', ''))
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
user.company_logo = UserService.save_file(logo_file, 'company_logos')
|
||||
|
||||
if document_files:
|
||||
existing_docs = json.loads(user.documents) if user.documents else []
|
||||
new_docs = UserService.save_multiple_documents(document_files, documents_metadata)
|
||||
existing_docs.extend(new_docs)
|
||||
user.documents = json.dumps(existing_docs)
|
||||
|
||||
user.updated_at = datetime.datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print(f"✓ User updated successfully")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def delete_user_document(user_id, document_path):
|
||||
"""Delete a specific document from a user"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.documents:
|
||||
documents = json.loads(user.documents)
|
||||
documents = [doc for doc in documents if doc['path'] != document_path]
|
||||
user.documents = json.dumps(documents)
|
||||
|
||||
# Delete physical file
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
document_path.replace('/uploads/', ''))
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def delete_user(id):
|
||||
"""Delete user and all associated files"""
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
# Delete all associated files
|
||||
if user.photo:
|
||||
photo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.photo.replace('/uploads/', ''))
|
||||
if os.path.exists(photo_path):
|
||||
os.remove(photo_path)
|
||||
|
||||
if user.company_logo:
|
||||
logo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.company_logo.replace('/uploads/', ''))
|
||||
if os.path.exists(logo_path):
|
||||
os.remove(logo_path)
|
||||
|
||||
if user.documents:
|
||||
documents = json.loads(user.documents)
|
||||
for doc in documents:
|
||||
doc_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
doc['path'].replace('/uploads/', ''))
|
||||
if os.path.exists(doc_path):
|
||||
os.remove(doc_path)
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def validate_file(file, maxSizeMB=10):
|
||||
"""Validate file size"""
|
||||
max_size_bytes = maxSizeMB * 1024 * 1024
|
||||
if file.size > max_size_bytes:
|
||||
return f"File size exceeds {maxSizeMB}MB limit"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_valid_image_type(file):
|
||||
"""Check if file is valid image type"""
|
||||
valid_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||
return valid_types.includes(file.type) if hasattr(file, 'type') else False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_document_type(file):
|
||||
"""Check if file is valid document type"""
|
||||
valid_types = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png'
|
||||
]
|
||||
return valid_types.includes(file.type) if hasattr(file, 'type') else False
|
||||
|
||||
# Product Service (UNCHANGED)
|
||||
class ProductService:
|
||||
@staticmethod
|
||||
def generate_id(prefix):
|
||||
return f"{prefix}{uuid.uuid4().hex[:4].upper()}"
|
||||
|
||||
@staticmethod
|
||||
def validate_product_data(data):
|
||||
required_fields = ['product_name', 'price']
|
||||
for field in required_fields:
|
||||
if field not in data or not data[field]:
|
||||
raise ValueError(f"Missing or empty required field: {field}")
|
||||
|
||||
try:
|
||||
price = float(data['price'])
|
||||
if price < 0:
|
||||
raise ValueError("Price must be a positive number")
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("Price must be a valid number")
|
||||
|
||||
@staticmethod
|
||||
def save_image(file):
|
||||
if not file:
|
||||
raise ValueError("Product image is required")
|
||||
|
||||
filename = f"{uuid.uuid4().hex}_{file.filename}"
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
file.save(file_path)
|
||||
|
||||
return f"/uploads/{filename}"
|
||||
|
||||
@staticmethod
|
||||
def get_all_products():
|
||||
return Product.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_product(data, file):
|
||||
ProductService.validate_product_data(data)
|
||||
|
||||
image_path = ProductService.save_image(file)
|
||||
|
||||
product_id = ProductService.generate_id("P")
|
||||
created_date = data.get('created_date', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
new_product = Product(
|
||||
product_id=product_id,
|
||||
product_name=data['product_name'],
|
||||
price=float(data['price']),
|
||||
product_image=image_path,
|
||||
created_date=created_date
|
||||
)
|
||||
|
||||
db.session.add(new_product)
|
||||
db.session.commit()
|
||||
return new_product
|
||||
|
||||
@staticmethod
|
||||
def update_product(id, data, file):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
ProductService.validate_product_data(data)
|
||||
|
||||
product.product_name = data.get('product_name', product.product_name)
|
||||
product.price = float(data.get('price', product.price))
|
||||
product.created_date = data.get('created_date', product.created_date)
|
||||
|
||||
if file:
|
||||
if product.product_image:
|
||||
old_image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
|
||||
if os.path.exists(old_image_path):
|
||||
os.remove(old_image_path)
|
||||
product.product_image = ProductService.save_image(file)
|
||||
|
||||
db.session.commit()
|
||||
return product
|
||||
|
||||
@staticmethod
|
||||
def delete_product(id):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
if product.product_image:
|
||||
image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
class TransactionService:
|
||||
@staticmethod
|
||||
def create_transaction(machine_id, product_name, quantity, amount, payment_type, status='Success'):
|
||||
"""Create a new transaction record"""
|
||||
transaction_id = f"TXN-{uuid.uuid4().hex[:8].upper()}"
|
||||
|
||||
new_transaction = Transaction(
|
||||
transaction_id=transaction_id,
|
||||
machine_id=machine_id,
|
||||
product_name=product_name,
|
||||
quantity=quantity,
|
||||
amount=amount,
|
||||
payment_type=payment_type,
|
||||
status=status
|
||||
)
|
||||
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
return new_transaction
|
||||
|
||||
@staticmethod
|
||||
def update_transaction_status(transaction_id, status, amount_receiving_status=None, return_amount=None):
|
||||
"""Update transaction status after dispensing"""
|
||||
transaction = Transaction.query.filter_by(transaction_id=transaction_id).first()
|
||||
if transaction:
|
||||
transaction.status = status
|
||||
if amount_receiving_status:
|
||||
transaction.amount_receiving_status = amount_receiving_status
|
||||
if return_amount:
|
||||
transaction.return_amount = return_amount
|
||||
db.session.commit()
|
||||
return transaction
|
||||
|
||||
@staticmethod
|
||||
def get_all_transactions(filters=None):
|
||||
"""Get all transactions with optional filters"""
|
||||
query = Transaction.query
|
||||
|
||||
if filters:
|
||||
if 'machine_id' in filters:
|
||||
query = query.filter_by(machine_id=filters['machine_id'])
|
||||
if 'status' in filters:
|
||||
query = query.filter_by(status=filters['status'])
|
||||
if 'date_from' in filters:
|
||||
query = query.filter(Transaction.created_at >= filters['date_from'])
|
||||
if 'date_to' in filters:
|
||||
query = query.filter(Transaction.created_at <= filters['date_to'])
|
||||
|
||||
return query.order_by(Transaction.created_at.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def get_transaction_by_id(transaction_id):
|
||||
"""Get single transaction by ID"""
|
||||
return Transaction.query.filter_by(transaction_id=transaction_id).first()
|
||||
|
||||
class RoleService:
|
||||
|
||||
@staticmethod
|
||||
def get_all_roles():
|
||||
"""Get all roles"""
|
||||
return Role.query.all()
|
||||
|
||||
@staticmethod
|
||||
def get_role_by_id(role_id):
|
||||
"""Get role by ID"""
|
||||
return Role.query.get_or_404(role_id)
|
||||
|
||||
@staticmethod
|
||||
def create_role(data):
|
||||
"""Create new role"""
|
||||
name = data.get('name')
|
||||
description = data.get('description', '')
|
||||
permissions = data.get('permissions', [])
|
||||
|
||||
# Check if role already exists
|
||||
existing_role = Role.query.filter_by(name=name).first()
|
||||
if existing_role:
|
||||
raise ValueError(f"Role '{name}' already exists")
|
||||
|
||||
new_role = Role(
|
||||
name=name,
|
||||
description=description,
|
||||
permissions=json.dumps(permissions)
|
||||
)
|
||||
|
||||
db.session.add(new_role)
|
||||
db.session.commit()
|
||||
|
||||
return new_role
|
||||
|
||||
@staticmethod
|
||||
def update_role(role_id, data):
|
||||
"""Update existing role"""
|
||||
role = Role.query.get_or_404(role_id)
|
||||
|
||||
if 'name' in data:
|
||||
# Check if new name conflicts with existing role
|
||||
existing = Role.query.filter(Role.name == data['name'], Role.id != role_id).first()
|
||||
if existing:
|
||||
raise ValueError(f"Role '{data['name']}' already exists")
|
||||
role.name = data['name']
|
||||
|
||||
if 'description' in data:
|
||||
role.description = data['description']
|
||||
|
||||
if 'permissions' in data:
|
||||
role.permissions = json.dumps(data['permissions'])
|
||||
|
||||
role.updated_at = datetime.datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
def delete_role(role_id):
|
||||
"""Delete role"""
|
||||
role = Role.query.get_or_404(role_id)
|
||||
|
||||
# Check if any users have this role
|
||||
users_with_role = User.query.filter_by(roles=role.name).count()
|
||||
if users_with_role > 0:
|
||||
raise ValueError(f"Cannot delete role '{role.name}' - {users_with_role} user(s) still assigned to it")
|
||||
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
|
||||
return True
|
||||
160
Machine-Backend/app/sms_notifications.json
Normal file
@ -0,0 +1,160 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2025-05-05 15:18:35",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "B4DC37B8",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: B4DC37B8"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 18:31:21",
|
||||
"username": "Gokul",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "7CB34107",
|
||||
"message": "Your account credentials: Username: Gokul, Email: fate.altre@gmail.com, Password: 7CB34107"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 20:53:59",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "0A96ABB6",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: 0A96ABB6"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 20:54:21",
|
||||
"username": "Joe Done",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "1385EE14",
|
||||
"message": "Your account credentials: Username: Joe Done, Email: fate.altre@gmail.com, Password: 1385EE14"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 21:26:23",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "A5FAED83",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: A5FAED83"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 12:51:15",
|
||||
"username": "admin",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "D1FCD9E5",
|
||||
"message": "Your account credentials: Username: admin, Email: fate.altre@gmail.com, Password: D1FCD9E5"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 13:19:32",
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"contact": "1234567890",
|
||||
"password": "67CC0B4F",
|
||||
"message": "Your account credentials: Username: testuser, Email: test@example.com, Password: 67CC0B4F"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 14:40:29",
|
||||
"username": "mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "5A353979",
|
||||
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 5A353979"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 14:46:06",
|
||||
"username": "mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "0C42E059",
|
||||
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 0C42E059"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-19 16:37:59",
|
||||
"username": "mukesh",
|
||||
"email": "test@example.com",
|
||||
"contact": "9999999999",
|
||||
"password": "42F71FD9",
|
||||
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: 42F71FD9"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-19 16:38:01",
|
||||
"username": "mukesh",
|
||||
"email": "test@example.com",
|
||||
"contact": "9999999999",
|
||||
"password": "F0DE0F08",
|
||||
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: F0DE0F08"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-22 14:57:01",
|
||||
"username": "John",
|
||||
"email": "john@test.com",
|
||||
"contact": "9876543210",
|
||||
"password": "623EBBCB",
|
||||
"message": "Your account credentials: Username: John, Email: john@test.com, Password: 623EBBCB"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 11:03:10",
|
||||
"username": "Rootxvending",
|
||||
"email": "praveenkumar@rootxvending.com",
|
||||
"contact": "9360857385",
|
||||
"password": "9213F482",
|
||||
"message": "Your account credentials: Username: Rootxvending, Email: praveenkumar@rootxvending.com, Password: 9213F482"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 13:17:02",
|
||||
"user_id": "USR-MUKEMUKE8421",
|
||||
"username": "Mukesh S",
|
||||
"email": "mukesh@rtx.com",
|
||||
"contact": "9876543210",
|
||||
"password": "840F9104",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE8421, Username: Mukesh S, Email: mukesh@rtx.com, Password: 840F9104"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 13:19:40",
|
||||
"user_id": "USR-MUKEMUKE8578",
|
||||
"username": "Mukesh S",
|
||||
"email": "mukesh@rxw.com",
|
||||
"contact": "9876543210",
|
||||
"password": "8382FB34",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE8578, Username: Mukesh S, Email: mukesh@rxw.com, Password: 8382FB34"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 17:40:58",
|
||||
"user_id": "USR-PRAVPRAV4256",
|
||||
"username": "Praveen",
|
||||
"email": "praveen@rtx.com",
|
||||
"contact": "9876543210",
|
||||
"password": "F8CB73CB",
|
||||
"message": "Your account credentials: User ID: USR-PRAVPRAV4256, Username: Praveen, Email: praveen@rtx.com, Password: F8CB73CB"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:17:34",
|
||||
"user_id": "USR-MUKEMUKE3650",
|
||||
"username": "Mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9876543211",
|
||||
"password": "77C1C095",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE3650, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 77C1C095"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:22:22",
|
||||
"user_id": "USR-MUKEMUKE3938",
|
||||
"username": "Mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9876543210",
|
||||
"password": "0358342E",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE3938, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 0358342E"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:25:49",
|
||||
"user_id": "USR-MUKEMUKE4145",
|
||||
"username": "mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9080063331",
|
||||
"password": "E95284C4",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE4145, Username: mukesh, Email: mukesh@rootxwire.com, Password: E95284C4"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
BIN
Machine-Backend/instance/machines.db
Normal file
BIN
Machine-Backend/instance/vending.db
Normal file
5
Machine-Backend/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Flask
|
||||
Flask-CORS
|
||||
Flask-SQLAlchemy
|
||||
python-dotenv
|
||||
pymysql
|
||||
5
Machine-Backend/run.py
Normal file
@ -0,0 +1,5 @@
|
||||
from app import create_app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, port=5000)
|
||||
16
fuse-starter-v20.0.0/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
fuse-starter-v20.0.0/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
1
fuse-starter-v20.0.0/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
1
fuse-starter-v20.0.0/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
20
|
||||
11
fuse-starter-v20.0.0/.prettierrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
|
||||
}
|
||||
72
fuse-starter-v20.0.0/CREDITS
Normal file
@ -0,0 +1,72 @@
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 3rd party credits
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
// Flags
|
||||
https://github.com/Yummygum/flagpack-core
|
||||
|
||||
// Icons
|
||||
Material - https://material.io/tools/icons
|
||||
Feather - https://feathericons.com/
|
||||
Heroicons - https://github.com/refactoringui/heroicons
|
||||
Iconsmind - https://iconsmind.com/
|
||||
|
||||
// Avatars
|
||||
https://uifaces.co
|
||||
|
||||
// 404, 500 & Maintenance
|
||||
https://undraw.co
|
||||
|
||||
// Mail app
|
||||
Photo by Riccardo Chiarini on Unsplash - https://unsplash.com/photos/2VDa8bnLM8c
|
||||
Photo by Johannes Plenio on Unsplash - https://unsplash.com/photos/RwHv7LgeC7s
|
||||
Photo by Jamie Davies on Unsplash - https://unsplash.com/photos/Hao52Fu9-F8
|
||||
Photo by Christian Joudrey on Unsplash - https://unsplash.com/photos/mWRR1xj95hg
|
||||
|
||||
// Profile page
|
||||
Photo by Alex Knight on Unsplash - https://unsplash.com/photos/DpPutJwgyW8
|
||||
|
||||
// Cards
|
||||
Photo by Kym Ellis on Unsplash - https://unsplash.com/photos/RPT3AjdXlZc
|
||||
Photo by Patrick Hendry on Unsplash - https://unsplash.com/photos/Qgxk3PQsMiI
|
||||
Photo by Hailey Kean on Unsplash - https://unsplash.com/photos/QxjsOlFNr_4
|
||||
Photo by Nathan Anderson on Unsplash - https://unsplash.com/photos/mG8ShlWrMDI
|
||||
Photo by Adrian Infernus on Unsplash - https://unsplash.com/photos/5apewqWk978
|
||||
Photo by freestocks.org on Unsplash - https://unsplash.com/photos/c73TZ2sIU38
|
||||
Photo by Tim Marshall on Unsplash - https://unsplash.com/photos/PKSCrmZdvwA
|
||||
Photo by Daniel Koponyas on Unsplash - https://unsplash.com/photos/rbiLY6ZwvXQ
|
||||
Photo by John Westrock on Unsplash - https://unsplash.com/photos/LCesauDseu8
|
||||
Photo by Gabriel Sollmann on Unsplash - https://unsplash.com/photos/kFWj9y-tJB4
|
||||
Photo by Kevin Wolf on Unsplash - https://unsplash.com/photos/BJyjgEdNTPs
|
||||
Photo by Luca Bravo on Unsplash - https://unsplash.com/photos/hFzIoD0F_i8
|
||||
Photo by Ian Baldwin on Unsplash - https://unsplash.com/photos/Dlj-SxxTlQ0
|
||||
Photo by Ben Kolde on Unsplash - https://unsplash.com/photos/KRTFIBOfcFw
|
||||
Photo by Chad Peltola on Unsplash - https://unsplash.com/photos/BTvQ2ET_iKc
|
||||
Photo by rocknwool on Unsplash - https://unsplash.com/photos/r56oO1V5oms
|
||||
Photo by Vita Vilcina on Unsplash - https://unsplash.com/photos/KtOid0FLjqU
|
||||
Photo by Jia Ye on Unsplash - https://unsplash.com/photos/y8ZnQqgohLk
|
||||
Photo by Parker Whitson on Unsplash - https://unsplash.com/photos/OlTYIqTjmVM
|
||||
Photo by Dorian Hurst on Unsplash - https://unsplash.com/photos/a9uWPQlIbYc
|
||||
Photo by Everaldo Coelho on Unsplash - https://unsplash.com/photos/KPaSCpklCZw
|
||||
Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/fh2JefbNlII
|
||||
Photo by Orlova Maria on Unsplash - https://unsplash.com/photos/p8y4dWEMGMU
|
||||
Photo by Jake Blucker on Unsplash - https://unsplash.com/photos/tMzCrBkM99Y
|
||||
Photo by Jerry Zhang on Unsplash - https://unsplash.com/photos/oIBcow6n36s
|
||||
Photo by John Cobb on Unsplash - https://unsplash.com/photos/IE_sifhay7o
|
||||
Photo by Dan Gold on Unsplash - https://unsplash.com/photos/mDlhOIfGxNI
|
||||
Photo by Ana Toma on Unsplash - https://unsplash.com/photos/XsGwe6gYg0c
|
||||
Photo by Andrea on Unsplash - https://unsplash.com/photos/1AWY0N960Sk
|
||||
Photo by Aswin on Unsplash - https://unsplash.com/photos/_roUcFWstas
|
||||
Photo by Justin Kauffman on Unsplash - https://unsplash.com/photos/aWG_dqyhI0A
|
||||
Photo by Barna Bartis on Unsplash - https://unsplash.com/photos/VVoBQqWrvkc
|
||||
Photo by Kyle Hinkson on Unsplash - https://unsplash.com/photos/3439EnvnAGo
|
||||
Photo by Spencer Watson on Unsplash - https://unsplash.com/photos/5TBf16GnHKg
|
||||
Photo by adrian on Unsplash - https://unsplash.com/photos/1wrzvwoK8A4
|
||||
Photo by Christopher Rusev on Unsplash - https://unsplash.com/photos/7gKWgCRixf0
|
||||
Photo by Stephen Leonardi on Unsplash - https://unsplash.com/photos/MDmwQVgDHHM
|
||||
Photo by Dwinanda Nurhanif Mujito on Unsplash - https://unsplash.com/photos/pKT5Mg16w_w
|
||||
Photo by Humphrey Muleba on Unsplash - https://unsplash.com/photos/Zuvf5mxT5fs
|
||||
Photo by adrian on Unsplash - https://unsplash.com/photos/PNRxLFPMyJY
|
||||
Photo by Dahee Son on Unsplash - https://unsplash.com/photos/tV06QVJXVxU
|
||||
Photo by Zachary Kyra-Derksen on Unsplash - https://unsplash.com/photos/vkqS7vLQUtg
|
||||
Photo by Rodrigo Soares on Unsplash - https://unsplash.com/photos/8BFWBUkSqQo
|
||||
13
fuse-starter-v20.0.0/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
# Stage 1: Build Angular app
|
||||
FROM node:18-alpine as build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build --configuration production
|
||||
|
||||
# Stage 2: Serve via Nginx
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
6
fuse-starter-v20.0.0/LICENSE.md
Normal file
@ -0,0 +1,6 @@
|
||||
Envato Standard License
|
||||
|
||||
Copyright (c) Withinpixels <hi@withinpixels.com>
|
||||
|
||||
This project is protected by Envato's Standard License. For more information,
|
||||
check the official license page at [https://themeforest.net/licenses/standard](https://themeforest.net/licenses/standard)
|
||||
27
fuse-starter-v20.0.0/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Fuse - Admin template and Starter project for Angular
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli)
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
122
fuse-starter-v20.0.0/angular.json
Normal file
@ -0,0 +1,122 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"fuse": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/fuse",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"allowedCommonJsDependencies": [
|
||||
"apexcharts",
|
||||
"crypto-js/enc-utf8",
|
||||
"crypto-js/hmac-sha256",
|
||||
"crypto-js/enc-base64",
|
||||
"quill-delta"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "_redirects",
|
||||
"input": "src",
|
||||
"output": "/"
|
||||
}
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["src/@fuse/styles"]
|
||||
},
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/@fuse/styles/tailwind.scss",
|
||||
"src/@fuse/styles/themes.scss",
|
||||
"src/styles/vendors.scss",
|
||||
"src/@fuse/styles/main.scss",
|
||||
"src/styles/styles.scss",
|
||||
"src/styles/tailwind.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "3mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "75kb",
|
||||
"maximumError": "90kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "fuse:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "fuse:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
14730
fuse-starter-v20.0.0/package-lock.json
generated
Normal file
69
fuse-starter-v20.0.0/package.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "fuse-angular",
|
||||
"version": "20.0.0",
|
||||
"description": "Fuse - Angular Admin Template and Starter Project",
|
||||
"author": "https://themeforest.net/user/srcn",
|
||||
"license": "https://themeforest.net/licenses/standard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "18.0.1",
|
||||
"@angular/cdk": "18.0.1",
|
||||
"@angular/common": "18.0.1",
|
||||
"@angular/compiler": "18.0.1",
|
||||
"@angular/core": "18.0.1",
|
||||
"@angular/forms": "18.0.1",
|
||||
"@angular/material": "18.0.1",
|
||||
"@angular/material-luxon-adapter": "18.0.1",
|
||||
"@angular/platform-browser": "18.0.1",
|
||||
"@angular/platform-browser-dynamic": "18.0.1",
|
||||
"@angular/router": "18.0.1",
|
||||
"@ngneat/transloco": "6.0.4",
|
||||
"apexcharts": "3.49.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"highlight.js": "11.9.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
"ng-apexcharts": "1.10.0",
|
||||
"ngx-quill": "26.0.1",
|
||||
"perfect-scrollbar": "1.5.5",
|
||||
"quill": "2.0.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.2",
|
||||
"zone.js": "0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.0.2",
|
||||
"@angular/cli": "18.0.2",
|
||||
"@angular/compiler-cli": "18.0.1",
|
||||
"@tailwindcss/typography": "0.5.13",
|
||||
"@types/chroma-js": "2.4.4",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/highlight.js": "10.1.0",
|
||||
"@types/jasmine": "5.1.4",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/luxon": "3.4.2",
|
||||
"autoprefixer": "10.4.19",
|
||||
"chroma-js": "2.4.2",
|
||||
"jasmine-core": "5.1.2",
|
||||
"karma": "6.4.3",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "2.2.1",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.3.0",
|
||||
"prettier-plugin-organize-imports": "3.2.4",
|
||||
"prettier-plugin-tailwindcss": "0.6.1",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
0
fuse-starter-v20.0.0/public/.gitkeep
Normal file
BIN
fuse-starter-v20.0.0/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
fuse-starter-v20.0.0/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 838 B |