File size: 7,657 Bytes
72a805c ee19706 c6eb05b ee19706 3defd1f 72a805c e9c3bc9 678de73 72a805c 3defd1f ee19706 72a805c c6eb05b 31c14a2 9b14457 72a805c 9b14457 72a805c 3defd1f 23ac9de e9c3bc9 5abdff6 c577326 e9c3bc9 72a805c c577326 3b307a2 5abdff6 c577326 72a805c ee19706 c577326 ee19706 c577326 72a805c 31c14a2 3defd1f 23ac9de e9c3bc9 c577326 e9c3bc9 c577326 23ac9de e9c3bc9 678de73 e9c3bc9 678de73 23ac9de e9c3bc9 678de73 3b307a2 e9c3bc9 c577326 3b307a2 72a805c 6b93641 72a805c a815506 2150d84 23ac9de c871548 23ac9de 2150d84 23ac9de 3defd1f 3b307a2 3defd1f 23ac9de 3defd1f 3b307a2 23ac9de 3b307a2 23ac9de 2150d84 ee19706 23ac9de ee19706 c577326 678de73 23ac9de 678de73 a815506 678de73 c577326 678de73 ee19706 23ac9de a815506 c577326 ee19706 3b307a2 e9c3bc9 ee19706 72a805c c577326 3defd1f 9b14457 3defd1f c6eb05b 3defd1f e9c3bc9 c577326 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | 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'<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()
# --- 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/<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
) |