import os import base64 import io from functools import wraps from datetime import datetime, date, timedelta import qrcode from flask import Flask, render_template, request, Response, url_for, send_from_directory from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import joinedload from flask_admin import Admin, AdminIndexView, expose from flask_admin.contrib.sqla import ModelView from flask_admin.form.upload import ImageUploadField from sqlalchemy import func # --- KONFIGURASI APLIKASI --- app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-local-dev') # --- KONFIGURASI PATH PENYIMPANAN SEMENTARA --- DATA_DIR = '/tmp' UPLOAD_DIR = os.path.join(DATA_DIR, 'uploads') DB_PATH = os.path.join(DATA_DIR, 'database.db') os.makedirs(UPLOAD_DIR, exist_ok=True) # --- KONFIGURASI DATABASE --- app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) # --- MODEL DATABASE --- class Category(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), unique=True, nullable=False) products = db.relationship('Product', back_populates='category', lazy=True) def __repr__(self): return self.name class Product(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), unique=True, nullable=False) description = db.Column(db.Text, nullable=False) price = db.Column(db.String(50), nullable=False) image = db.Column(db.String(100), nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) category = db.relationship('Category', back_populates='products') def __repr__(self): return f'' class Setting(db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(50), unique=True, nullable=False) value = db.Column(db.Text, nullable=True) def __repr__(self): return f'' class Sale(db.Model): id = db.Column(db.Integer, primary_key=True) product_name = db.Column(db.String(100), nullable=False) quantity = db.Column(db.Integer, nullable=False) total_price = db.Column(db.Float, nullable=False) sale_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) def __repr__(self): return f'' with app.app_context(): db.create_all() # --- FUNGSI & CONTEXT PROCESSOR --- def generate_qr_code(data): qr = qrcode.QRCode(version=1, box_size=10, border=4) qr.add_data(data) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffered = io.BytesIO() img.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode("utf-8") @app.context_processor def inject_global_data(): menu_icons = { 'Dashboard': 'fa-solid fa-chart-pie', 'Product': 'fa-solid fa-mug-hot', 'Category': 'fa-solid fa-tags', 'Sale': 'fa-solid fa-cash-register', 'Setting': 'fa-solid fa-sliders' } def get_setting_from_db(key, default=''): setting = Setting.query.filter_by(key=key).first() return setting.value if setting else default return dict(menu_icons=menu_icons, get_setting=get_setting_from_db, generate_qr_code=generate_qr_code) # --- FUNGSI KEAMANAN --- def check_auth(username, password): ADMIN_USER = os.environ.get('ADMIN_USER', 'admin') ADMIN_PASS = os.environ.get('ADMIN_PASS', 'password') return username == ADMIN_USER and password == ADMIN_PASS def authenticate(): return Response('Login Required', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) def protected(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated # --- PENGATURAN ADMIN PANEL --- class ProductAdminView(ModelView): @protected def dispatch_request(self, *args, **kwargs): return super().dispatch_request(*args, **kwargs) create_template = 'admin/custom_create.html' edit_template = 'admin/custom_edit.html' column_list = ('name', 'category', 'price') column_filters = ('category',) form_extra_fields = { 'image': ImageUploadField( 'Gambar Produk', base_path=UPLOAD_DIR, endpoint='get_upload', namegen=lambda o, f: f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{f.filename}", allowed_extensions=['jpg', 'jpeg', 'png', 'gif'], max_size=(3*1024*1024, 'Maksimal 3MB') ) } form_columns = ('category', 'name', 'description', 'price', 'image') form_widget_args = {'description': {'rows': 5}} form_args = {'category': {'label': 'Kategori Produk', 'query_factory': lambda: Category.query.order_by(Category.name).all()}} class SecureModelView(ModelView): @protected def dispatch_request(self, *args, **kwargs): return super().dispatch_request(*args, **kwargs) class DashboardView(AdminIndexView): @protected def dispatch_request(self, *args, **kwargs): return super().dispatch_request(*args, **kwargs) def get_sales_data(self): today = date.today(); seven_days_ago = today - timedelta(days=6) sales = db.session.query(func.date(Sale.sale_date).label('date'), func.sum(Sale.total_price).label('total')).filter(Sale.sale_date >= seven_days_ago).group_by(func.date(Sale.sale_date)).order_by(func.date(Sale.sale_date)).all() sales_dict = {s.date.strftime('%Y-%m-%d'): s.total for s in sales} labels = [(today - timedelta(days=i)).strftime('%a, %d') for i in range(6, -1, -1)] data = [sales_dict.get((today - timedelta(days=i)).strftime('%Y-%m-%d'), 0) for i in range(6, -1, -1)] return labels, data def get_stats(self): stats = {}; total_revenue = db.session.query(func.sum(Sale.total_price)).scalar() stats['total_revenue'] = total_revenue or 0 stats['total_products'] = Product.query.count() stats['today_sales'] = Sale.query.filter(func.date(Sale.sale_date) == date.today()).count() return stats @expose('/') def index(self): chart_labels, chart_data = self.get_sales_data(); stats = self.get_stats() return self.render('admin/dashboard.html', chart_labels=chart_labels, chart_data=chart_data, stats=stats) admin = Admin( app, name='Bit & Bean Dashboard', template_mode='bootstrap4', base_template='admin/custom_master.html', index_view=DashboardView(name='Dashboard', url='/admin') ) admin.add_view(ProductAdminView(Product, db.session, category="Manajemen")) admin.add_view(SecureModelView(Category, db.session, category="Manajemen")) admin.add_view(SecureModelView(Sale, db.session, category='Laporan')) admin.add_view(SecureModelView(Setting, db.session, category='Pengaturan')) # --- ROUTE PENYAJI GAMBAR & HALAMAN UTAMA --- @app.route('/uploads/') def get_upload(filename): """Route untuk menyajikan file yang di-upload dari penyimpanan.""" return send_from_directory(UPLOAD_DIR, filename) @app.route('/') def home(): """Route untuk halaman utama website.""" categories = Category.query.order_by(Category.name).options(joinedload(Category.products)).all() settings_query = Setting.query.all() settings = {s.key: s.value for s in settings_query} return render_template( 'index.html', categories=categories, settings=settings )