| 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 |
|
|
| |
| app = Flask(__name__) |
| app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-local-dev') |
|
|
| |
| 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) |
|
|
| |
| app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}' |
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False |
| db = SQLAlchemy(app) |
|
|
|
|
| |
| 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'<Product {self.name}>' |
|
|
| 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'<Setting {self.key}>' |
|
|
| 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'<Sale of {self.product_name}>' |
|
|
| with app.app_context(): |
| db.create_all() |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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 |
|
|
|
|
| |
| 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')) |
|
|
|
|
| |
| @app.route('/uploads/<path:filename>') |
| 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 |
| ) |