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
    )