From 86fcd7b49b4ead6a76f835ef5ab4065d76452e32 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Tue, 2 Jun 2026 15:22:08 +0700 Subject: [PATCH] Init --- .sync_state.json | 1 + README.md | 2 - app.py | 583 ++++++++++++++++++++++++++++++++++++++ counter.env | 1 + dashboard_settings.db | Bin 0 -> 16384 bytes karung_masuk.db | Bin 0 -> 20480 bytes karung_masuk.json | 8 + karung_tuang.db | Bin 0 -> 12288 bytes karung_tuang.json | 8 + sync_counter.py | 324 +++++++++++++++++++++ templates/index.html | 578 +++++++++++++++++++++++++++++++++++++ templates/karung_masuk.db | Bin 0 -> 20480 bytes 12 files changed, 1503 insertions(+), 2 deletions(-) create mode 100644 .sync_state.json delete mode 100644 README.md create mode 100644 app.py create mode 100644 counter.env create mode 100644 dashboard_settings.db create mode 100644 karung_masuk.db create mode 100644 karung_masuk.json create mode 100644 karung_tuang.db create mode 100644 karung_tuang.json create mode 100644 sync_counter.py create mode 100644 templates/index.html create mode 100644 templates/karung_masuk.db diff --git a/.sync_state.json b/.sync_state.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.sync_state.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 2cfeae3..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# karunng-web-test - diff --git a/app.py b/app.py new file mode 100644 index 0000000..5c9b46b --- /dev/null +++ b/app.py @@ -0,0 +1,583 @@ +from flask import Flask, abort, redirect, render_template, request, url_for +import sqlite3 +import json +from datetime import datetime +import os +from pathlib import Path + +app = Flask(__name__) +BASE_DIR = Path(__file__).resolve().parent +SETTINGS_DB = BASE_DIR / 'dashboard_settings.db' +DISPLAY_DATE_FORMAT = '%d-%m-%Y' + +def load_env_file(env_path): + if not env_path.exists(): + return + + with env_path.open() as env_file: + for line in env_file: + stripped_line = line.strip() + if not stripped_line or stripped_line.startswith('#') or '=' not in stripped_line: + continue + + key, value = stripped_line.split('=', 1) + os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) + +def get_configured_db_path(env_name, default_file_name): + db_path = Path(os.environ.get(env_name, default_file_name)).expanduser() + if not db_path.is_absolute(): + db_path = BASE_DIR / db_path + + return db_path + +load_env_file(BASE_DIR / '.env') + +KARUNG_TUANG_DB = get_configured_db_path('KARUNG_TUANG_DB', 'karung_tuang.db') +KARUNG_MASUK_DB = get_configured_db_path('KARUNG_MASUK_DB', 'karung_masuk.db') + +if KARUNG_TUANG_DB.resolve() == KARUNG_MASUK_DB.resolve(): + raise RuntimeError('KARUNG_TUANG_DB and KARUNG_MASUK_DB must point to different database files.') + +DB_CONFIGS = { + 'tuang': { + 'path': KARUNG_TUANG_DB, + 'table': 'counter_data', + }, + 'masuk': { + 'path': KARUNG_MASUK_DB, + 'table': 'karung_counts', + }, +} + +DEFAULT_DASHBOARD_SETTINGS = { + 'id': '', + 'cycle_name': 'Siklus Aktif', + 'cycle_start': '', + 'cycle_end': '', + 'saldo_awal': 0, +} + +def get_db_data(db_path, table_name): + """Fetch all data from database""" + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute(f"SELECT id, camera_name, date, counter_value FROM {table_name} ORDER BY date DESC") + rows = cursor.fetchall() + conn.close() + + data = [] + for row in rows: + data.append({ + 'id': row[0], + 'camera_name': row[1], + 'date': row[2], + 'counter_value': row[3] + }) + return data + +def load_dashboard_settings(): + ensure_settings_db() + + with sqlite3.connect(SETTINGS_DB) as conn: + row = conn.execute( + """ + SELECT id, cycle_name, cycle_start, cycle_end, saldo_awal + FROM cycle_settings + ORDER BY is_active DESC, id DESC + LIMIT 1 + """ + ).fetchone() + + if not row: + return DEFAULT_DASHBOARD_SETTINGS.copy() + + return { + 'id': row[0], + 'cycle_name': row[1], + 'cycle_start': row[2], + 'cycle_end': row[3], + 'saldo_awal': row[4], + } + +def save_dashboard_settings(settings): + ensure_settings_db() + + with sqlite3.connect(SETTINGS_DB) as conn: + conn.execute('UPDATE cycle_settings SET is_active = 0') + conn.execute( + """ + INSERT INTO cycle_settings ( + cycle_name, + cycle_start, + cycle_end, + saldo_awal, + is_active, + updated_at + ) + VALUES (?, ?, ?, ?, 1, ?) + """, + ( + settings['cycle_name'], + settings['cycle_start'], + settings['cycle_end'], + settings['saldo_awal'], + datetime.now().isoformat(timespec='seconds') + ) + ) + +def get_cycle_options(): + ensure_settings_db() + + with sqlite3.connect(SETTINGS_DB) as conn: + rows = conn.execute( + """ + SELECT latest_cycle.id, + latest_cycle.cycle_name, + latest_cycle.cycle_start, + latest_cycle.cycle_end, + latest_cycle.saldo_awal + FROM cycle_settings AS latest_cycle + INNER JOIN ( + SELECT cycle_name, MAX(id) AS latest_id + FROM cycle_settings + GROUP BY cycle_name + ) AS grouped_cycle + ON latest_cycle.id = grouped_cycle.latest_id + ORDER BY latest_cycle.cycle_name COLLATE NOCASE + """ + ).fetchall() + + return [ + { + 'id': row[0], + 'cycle_name': row[1], + 'cycle_start': row[2], + 'cycle_end': row[3], + 'saldo_awal': row[4], + } + for row in rows + ] + +def select_cycle_by_name(cycle_name): + ensure_settings_db() + + with sqlite3.connect(SETTINGS_DB) as conn: + row = conn.execute( + """ + SELECT id + FROM cycle_settings + WHERE cycle_name = ? + ORDER BY id DESC + LIMIT 1 + """, + (cycle_name,) + ).fetchone() + + if not row: + abort(404, description='Cycle name was not found.') + + conn.execute('UPDATE cycle_settings SET is_active = 0') + conn.execute( + """ + UPDATE cycle_settings + SET is_active = 1, + updated_at = ? + WHERE id = ? + """, + (datetime.now().isoformat(timespec='seconds'), row[0]) + ) + +def ensure_settings_db(): + with sqlite3.connect(SETTINGS_DB) as conn: + existing_schema = conn.execute( + """ + SELECT sql + FROM sqlite_master + WHERE type = 'table' + AND name = 'cycle_settings' + """ + ).fetchone() + + if existing_schema and 'CHECK (id = 1)' in existing_schema[0]: + conn.execute('ALTER TABLE cycle_settings RENAME TO cycle_settings_old') + create_cycle_settings_table(conn) + conn.execute( + """ + INSERT INTO cycle_settings ( + cycle_name, + cycle_start, + cycle_end, + saldo_awal, + is_active, + updated_at + ) + SELECT cycle_name, + cycle_start, + cycle_end, + saldo_awal, + 0, + updated_at + FROM cycle_settings_old + ORDER BY id + """ + ) + conn.execute('DROP TABLE cycle_settings_old') + else: + create_cycle_settings_table(conn) + ensure_settings_active_column(conn) + + settings_count = conn.execute('SELECT COUNT(*) FROM cycle_settings').fetchone()[0] + if settings_count: + ensure_active_cycle(conn) + return + + conn.execute( + """ + INSERT OR IGNORE INTO cycle_settings ( + cycle_name, + cycle_start, + cycle_end, + saldo_awal, + is_active, + updated_at + ) + VALUES (?, ?, ?, ?, 1, ?) + """, + ( + DEFAULT_DASHBOARD_SETTINGS['cycle_name'], + DEFAULT_DASHBOARD_SETTINGS['cycle_start'], + DEFAULT_DASHBOARD_SETTINGS['cycle_end'], + DEFAULT_DASHBOARD_SETTINGS['saldo_awal'], + datetime.now().isoformat(timespec='seconds') + ) + ) + +def create_cycle_settings_table(conn): + conn.execute( + """ + CREATE TABLE IF NOT EXISTS cycle_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cycle_name TEXT NOT NULL, + cycle_start TEXT NOT NULL DEFAULT '', + cycle_end TEXT NOT NULL DEFAULT '', + saldo_awal INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL + ) + """ + ) + +def ensure_settings_active_column(conn): + columns = { + row[1] + for row in conn.execute('PRAGMA table_info(cycle_settings)').fetchall() + } + if 'is_active' not in columns: + conn.execute( + """ + ALTER TABLE cycle_settings + ADD COLUMN is_active INTEGER NOT NULL DEFAULT 0 + """ + ) + +def ensure_active_cycle(conn): + active_count = conn.execute( + 'SELECT COUNT(*) FROM cycle_settings WHERE is_active = 1' + ).fetchone()[0] + if active_count: + return + + latest_id = conn.execute( + 'SELECT id FROM cycle_settings ORDER BY id DESC LIMIT 1' + ).fetchone()[0] + conn.execute('UPDATE cycle_settings SET is_active = 1 WHERE id = ?', (latest_id,)) + +def parse_cycle_date(date_text, field_name): + date_text = date_text.strip() + if not date_text: + return None + + for date_format in (DISPLAY_DATE_FORMAT, '%Y-%m-%d'): + try: + return datetime.strptime(date_text, date_format).date() + except ValueError: + continue + + abort(400, description=f'{field_name} must use DD-MM-YYYY or YYYY-MM-DD format.') + +def parse_data_date(date_text): + date_text = str(date_text).strip() + if not date_text: + return None + + try: + return datetime.fromisoformat(date_text.replace('Z', '+00:00')).date() + except ValueError: + pass + + for date_format in (DISPLAY_DATE_FORMAT, '%d/%m/%Y'): + try: + return datetime.strptime(date_text, date_format).date() + except ValueError: + continue + + return None + +def format_history_date(date_text): + parsed_date = parse_data_date(date_text) + if not parsed_date: + return date_text + + return parsed_date.strftime('%Y-%m-%d') + +def format_date_picker_value(date_text): + parsed_date = parse_cycle_date(date_text, 'Cycle date') + if not parsed_date: + return '' + + return parsed_date.strftime('%Y-%m-%d') + +def filter_data_by_cycle(data, cycle_start, cycle_end): + if not cycle_start or not cycle_end: + return [] + + filtered_data = [] + for item in data: + item_date = parse_data_date(item['date']) + if not item_date: + continue + if item_date < cycle_start: + continue + if item_date > cycle_end: + continue + + filtered_data.append(item) + + return filtered_data + +def is_masuk_out_item(item): + return str(item['camera_name']).lower().endswith('_out') + +def get_settings_from_form(): + cycle_name = request.form.get('cycle_name', '').strip() + cycle_start_raw = request.form.get('cycle_start', '').strip() + cycle_end_raw = request.form.get('cycle_end', '').strip() + saldo_awal_raw = request.form.get('saldo_awal', '0').strip() + + if not cycle_name: + abort(400, description='Cycle name is required.') + if not cycle_start_raw or not cycle_end_raw: + abort(400, description='Cycle start and cycle end are required to filter historical data.') + + cycle_start = parse_cycle_date(cycle_start_raw, 'Cycle start') + cycle_end = parse_cycle_date(cycle_end_raw, 'Cycle end') + + if cycle_start and cycle_end and cycle_start > cycle_end: + abort(400, description='Cycle start must be before or equal to cycle end.') + + try: + saldo_awal = int(saldo_awal_raw or 0) + except ValueError: + abort(400, description='Saldo awal must be a whole number.') + + if saldo_awal < 0: + abort(400, description='Saldo awal cannot be negative.') + + return { + 'cycle_name': cycle_name, + 'cycle_start': cycle_start.strftime(DISPLAY_DATE_FORMAT) if cycle_start else '', + 'cycle_end': cycle_end.strftime(DISPLAY_DATE_FORMAT) if cycle_end else '', + 'saldo_awal': saldo_awal, + } + +def ensure_db_writable(db_path): + db_path = Path(db_path) + if not db_path.exists(): + abort(404, description=f'Database file not found: {db_path}') + if not os.access(db_path, os.W_OK): + abort(403, description=f'Cannot save jumlah karung because database is not writable: {db_path}') + if not os.access(db_path.parent, os.W_OK): + abort(403, description=f'Cannot save jumlah karung because database directory is not writable: {db_path.parent}') + +def update_db_data(db_path, table_name, row_id, counter_value): + """Update one database row by primary key.""" + ensure_db_writable(db_path) + + try: + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute( + f""" + UPDATE {table_name} + SET counter_value = ? + WHERE id = ? + """, + (counter_value, row_id) + ) + updated_count = cursor.rowcount + except sqlite3.OperationalError as error: + abort(403, description=f'Cannot save jumlah karung to database {db_path}: {error}') + + if updated_count == 0: + abort(404, description='No database row was found for the requested edit.') + +def update_db_rows(db_path, table_name, row_values): + ensure_db_writable(db_path) + + try: + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + for row_id, counter_value in row_values.items(): + cursor.execute( + f""" + UPDATE {table_name} + SET counter_value = ? + WHERE id = ? + """, + (counter_value, row_id) + ) + + if cursor.rowcount == 0: + abort(404, description='No database row was found for one of the requested edits.') + except sqlite3.OperationalError as error: + abort(403, description=f'Cannot save jumlah karung to database {db_path}: {error}') + +def get_counter_value_from_form(counter_value_raw): + if not counter_value_raw: + abort(400, description='Jumlah karung is required to edit database data.') + + try: + counter_value = int(counter_value_raw) + except ValueError: + abort(400, description='Jumlah karung must be a whole number.') + + if counter_value < 0: + abort(400, description='Jumlah karung cannot be negative.') + + return counter_value + +def get_bulk_counter_values_from_form(): + row_values = {} + + for field_name, field_value in request.form.items(): + if not field_name.startswith('counter_value_'): + continue + + row_id_raw = field_name.removeprefix('counter_value_') + try: + row_id = int(row_id_raw) + except ValueError: + abort(400, description='Invalid database row id in edit form.') + + row_values[row_id] = get_counter_value_from_form(field_value.strip()) + + if not row_values: + abort(400, description='No jumlah karung values were submitted.') + + return row_values + +def get_json_data(json_path): + """Fetch current data from JSON file""" + try: + with open(json_path, 'r') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {} + +@app.route('/database///edit', methods=['POST']) +def edit_db_row(db_name, row_id): + db_config = DB_CONFIGS.get(db_name) + if not db_config: + abort(404, description='Unknown database. Use tuang or masuk.') + + counter_value = get_counter_value_from_form(request.form.get('counter_value', '').strip()) + + update_db_data( + db_config['path'], + db_config['table'], + row_id, + counter_value + ) + return redirect(url_for('index')) + +@app.route('/database//edit', methods=['POST']) +def edit_db_rows(db_name): + db_config = DB_CONFIGS.get(db_name) + if not db_config: + abort(404, description='Unknown database. Use tuang or masuk.') + + update_db_rows( + db_config['path'], + db_config['table'], + get_bulk_counter_values_from_form() + ) + return redirect(url_for('index')) + +@app.route('/settings', methods=['POST']) +def update_dashboard_settings(): + settings = get_settings_from_form() + save_dashboard_settings(settings) + return redirect(url_for('index')) + +@app.route('/settings/select', methods=['POST']) +def select_dashboard_cycle(): + cycle_name = request.form.get('cycle_name', '').strip() + if not cycle_name: + abort(400, description='Cycle name is required to select a cycle.') + + select_cycle_by_name(cycle_name) + return redirect(url_for('index')) + +@app.route('/') +def index(): + dashboard_settings = load_dashboard_settings() + cycle_options = get_cycle_options() + cycle_start = parse_cycle_date(dashboard_settings['cycle_start'], 'Cycle start') + cycle_end = parse_cycle_date(dashboard_settings['cycle_end'], 'Cycle end') + cycle_start_picker_value = format_date_picker_value(dashboard_settings['cycle_start']) + cycle_end_picker_value = format_date_picker_value(dashboard_settings['cycle_end']) + + # Get historical data from databases + tuang_db_data = get_db_data(DB_CONFIGS['tuang']['path'], DB_CONFIGS['tuang']['table']) + masuk_db_data = get_db_data(DB_CONFIGS['masuk']['path'], DB_CONFIGS['masuk']['table']) + tuang_db_data = filter_data_by_cycle(tuang_db_data, cycle_start, cycle_end) + masuk_db_data = filter_data_by_cycle(masuk_db_data, cycle_start, cycle_end) + for item in tuang_db_data: + item['display_date'] = format_history_date(item['date']) + + # Get current date data from JSON files + tuang_json_data = get_json_data('/etc/frigate-counter/karung-tuang-2/karung_tuang.json') + masuk_json_data = get_json_data('/etc/frigate-counter/karung-masuk/karung_masuk.json') + + # Calculate totals from JSON (current date) + tuang_json_total = sum(item.get('karung', 0) for item in tuang_json_data.values()) + masuk_json_total = sum(item.get('karung', 0) for item in masuk_json_data.values()) + + # Calculate totals from database (historical) + tuang_db_total = sum(item['counter_value'] for item in tuang_db_data) + masuk_db_total = sum( + item['counter_value'] if not is_masuk_out_item(item) else -item['counter_value'] + for item in masuk_db_data + ) + saldo_akhir = dashboard_settings['saldo_awal'] + masuk_db_total - tuang_db_total + + current_date = datetime.now().strftime(DISPLAY_DATE_FORMAT) + + return render_template('index.html', + dashboard_settings=dashboard_settings, + cycle_options=cycle_options, + cycle_start_picker_value=cycle_start_picker_value, + cycle_end_picker_value=cycle_end_picker_value, + tuang_db_data=tuang_db_data, + masuk_db_data=masuk_db_data, + tuang_json_data=tuang_json_data, + masuk_json_data=masuk_json_data, + tuang_json_total=tuang_json_total, + masuk_json_total=masuk_json_total, + tuang_db_total=tuang_db_total, + masuk_db_total=masuk_db_total, + saldo_akhir=saldo_akhir, + current_date=current_date) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8080) diff --git a/counter.env b/counter.env new file mode 100644 index 0000000..0c6d055 --- /dev/null +++ b/counter.env @@ -0,0 +1 @@ +COUNTER_UUID=9f556d2a-af5c-41b0-bcd0-6cea0dfa0fed diff --git a/dashboard_settings.db b/dashboard_settings.db new file mode 100644 index 0000000000000000000000000000000000000000..e76c59984add0a8f061ebf9feba4801169171526 GIT binary patch literal 16384 zcmeI(O>Wab6bJBelD2NBW7JI-?P9usG-`tUkq_e)rWh18O-t%X%_=g9gWS3)t(_v) zhy@Z7SKu1G01|g$!4Y^#OTZzbX_w{y^>6& z_JniB*cz1-l>`-m%G_0*PNNv!yDrw)qw|lFbdM#LE?8!;bU`Ns1Rwwb2tWV=5P$## zAOL}z3G5~3S2j0=R^+}4yzn%jub6h|oesQyXR?~wYS@-zbH{pGv-w2ZjlO&A<2U#k0uX=z1Rwwb2tWV=5P$##AaK_OB)P}L%qo+U@>ltjoX>pAe9koK1_A;Q zfB*y_009U<00Izz00jOQ0V6H4)%Ep+zPL5~fu2`1t)P?&nxd6ex~hz>*q33wpwOW? zieVOurm7l?SDE5vW_gxOy=;~gLyTuuG<}9Kb+cp`3%AHrADen9D~RlYB#FAX?;iz& zkXuKQ-y6S{>ZpdP>(tPgpJj+2e`OXcv}GP%zio`U$^NH-PxP-<%u>-v#WSaa|IQpO v6@eEOiKZpU-x(bU2tWV=5P$##AOHafKmY;|fB*#UfWWel%n9+oP^5nX6+Zas literal 0 HcmV?d00001 diff --git a/karung_masuk.db b/karung_masuk.db new file mode 100644 index 0000000000000000000000000000000000000000..9ea20025085ccea374129e68b4e4102dce9de69a GIT binary patch literal 20480 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 evening_start: + # Evening window (17:00-23:59): sync current date + sync_date = curr_date + skip_if_synced = False + print(f"\nSyncing {name} (evening window {evening_start:02d}:00-23:59) for date {sync_date}...") + else: + # Morning window (00:00-16:59): sync previous date, skip if already synced + sync_date = prev_date + skip_if_synced = config.get("skip_if_synced_morning", True) + print(f"\nSyncing {name} (morning window 00:00-{evening_start:02d}:00) for date {sync_date}...") + + sync_database( + config, + sync_date, + uuid, + state=sync_state, + skip_if_synced=skip_if_synced, + ) + else: + # Standard behavior for other databases (karung_masuk) + start_hour = config["start_hour"] + end_hour = config.get("end_hour", 24) + + if hour >= start_hour and hour < end_hour: + sync_date = curr_date if config.get("use_current_date", False) else prev_date + + print(f"\nSyncing {name} for date {sync_date}...") + sync_database( + config, + sync_date, + uuid, + state=sync_state, + skip_if_synced=config.get("skip_if_synced", False), + ) + else: + print(f"\nSkipping {name} - starts at {start_hour:02d}:00") + + # Save sync state + save_sync_state(sync_state) + + print("\nSync complete.") + + +if __name__ == "__main__": + main() diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c117afc --- /dev/null +++ b/templates/index.html @@ -0,0 +1,578 @@ + + + + + + Karung Counter Dashboard + + + +
+

📊 Karung Counter Dashboard

+ +
+

Selected Cycle

+
+
+ Cycle Name +
+ + +
+
+
+ Range + {{ dashboard_settings.cycle_start or 'awal data' }} - {{ dashboard_settings.cycle_end or 'akhir data' }} +
+
+ Saldo Awal + {{ dashboard_settings.saldo_awal }} +
+
+
+ + +
+
+

Cycle Name

+
{{ dashboard_settings.cycle_name }}
+
+
+

Karung Tuang Hari Ini (JSON)

+
{{ tuang_json_total }}
+
+
+

Karung Masuk Hari Ini (JSON)

+
{{ masuk_json_total }}
+
+
+

Total Karung Tuang (Siklus)

+
{{ tuang_db_total }}
+
+
+

Total Karung Masuk (Siklus)

+
{{ masuk_db_total }}
+
+
+

Saldo Awal

+
{{ dashboard_settings.saldo_awal }}
+
+
+

Saldo Akhir

+
{{ saldo_akhir }}
+
+
+ + +
+

📅 Data Hari Ini ({{ current_date }}) - Dari JSON

+ +

Karung Tuang

+ {% if tuang_json_data %} +
+ {% for camera, data in tuang_json_data.items() %} +
+
{{ camera }}
+
{{ data.karung|default(0) }} karung
+
+ {% endfor %} +
+ {% else %} +

Tidak ada data karung tuang untuk hari ini

+ {% endif %} + +

Karung Masuk

+ {% if masuk_json_data %} +
+ {% for camera, data in masuk_json_data.items() %} +
+
{{ camera }}
+
{{ data.karung|default(0) }} karung
+
+ {% endfor %} +
+ {% else %} +

Tidak ada data karung masuk untuk hari ini

+ {% endif %} +
+ + +
+ +

🗄️ Data Historis - Dari Database

+ +
+
+

+ Siklus aktif: + {{ dashboard_settings.cycle_name }}. + Rentang: + {{ dashboard_settings.cycle_start }} + sampai + {{ dashboard_settings.cycle_end }} +

+ +
+
+

Karung Tuang

+ {% if tuang_db_data %} +
+ + +
+ {% endif %} +
+ {% if tuang_db_data %} +
+ + + + + + + + + + {% for item in tuang_db_data %} + + + + + + {% endfor %} + +
Camera NameTanggalJumlah Karung
+ TUANG {{ item.camera_name }} + + {{ item.display_date }} + + +
+
+ {% else %} +

Tidak ada data historis karung tuang

+ {% endif %} +
+ +
+
+

Karung Masuk

+ {% if masuk_db_data %} +
+ + +
+ {% endif %} +
+ {% if masuk_db_data %} +
+ + + + + + + + + + {% for item in masuk_db_data %} + + + + + + {% endfor %} + +
Camera NameTanggalJumlah Karung
+ MASUK {{ item.camera_name }} + + {{ item.date }} + + +
+
+ {% else %} +

Tidak ada data historis karung masuk

+ {% endif %} +
+
+
+ + +
+

⚙️ Pengaturan Siklus

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

Data historis hanya menampilkan data dalam rentang siklus.

+
+
+ + + diff --git a/templates/karung_masuk.db b/templates/karung_masuk.db new file mode 100644 index 0000000000000000000000000000000000000000..9ea20025085ccea374129e68b4e4102dce9de69a GIT binary patch literal 20480 zcmeI2Piz!b9LHz&Uw3E!6+|nFF!+bIw2Sj+|GimeWhny|+HG03LeXTi!lDgqfp&|D zcrZpxxafrtG@cA58ZR7-2f1kUqz4oKoDDJYq8>bGOg!j&boTdVVYdBU#W!K;TbR#& zfA2frdHdd*9yvU<&}pfs&Mvi^9o3WuC0UmCsH!AM6Z93PukIn!MyUIS{$%ghJ0_%E z7cM91S%K7xQtDIsmpFt2!U5rca6mX991so&2ZRH{0pWmf;GTA1HV_#a8-Er{w5s6Y{{&@UZ;4bNe$- zH?s*gF`MjS0Qb!c^j$`4ZI$5nd2b`KZTbrD&)aKPg&b(TgtIteU z=_gG))%y5d)CpSEQnSH|Q|r#Lx>}u~ueqtIyH7pYr0eZ{tb_f}x!sG_QsYAN%t{OD z>3@D_q1{^UG~4HHe{g+r+EMp8`zv!(b#-E{R-+|1*gqbrSEgrsY9D0_d^!C$%U*c( z$2}cG(K+FOa6mX991so&2ZRH{0pWmfKsX>A5Dxqg9SBv}vw1oFn?(PJLpUHD5Do|j zgag6>;ec>JI3OGl4hRQ?1HyrO!GR5Oa9Hm71&HHp{a=x8Na-)rm($bf!PNJuYpLf_ z+mk;hKT4iUW)puUZYEw$I0+^GP5esyXnc!uOS!HrD4E#rv5#Xf#Y)kCqF+Sch#ril z2EHA*I&geoTjZz62a$F}5C0MVBz!Sk3QM8SLYG4OLrUJwT;R$!O4$#!ccYc_pq%H*C6uxsYFlXK94P0w zGC?W(p>~le$NEI1=Q?LWISa~Gfho&3!(l*0BY&nb8 zHWI)aT-!ux8&MzDHqaMa2V4hfqN6UhA6nMXz1D_7TI14BGU~3fC|hHgORW`o)gE>yiY<&lk39p~D5}RWoM@hrXat4A zc%bp2#2Q9boSlJ+`CveHpkz%zJ@@hxC|P41SkJxqIEvNbj?nncWIeV%o6VTomQ`KW zc* z*5Jw2BPcdo16`i0(iT*QP3c{Ld9q3mqhLe22Cyeu5232otH64qwHd`~?!8al;?W`x zGIs5WTNWm6tOcno4PpAL7o{uRnspv6@&HQLojg%p9xQS{3U!Aoit=2QHla$+N!Oud z9xQSnO4psdP+guYvJr*4;}u1Ds>lWuYCyp}Q)Cc@x}z0U<&h$36smzLPgE(z(Deol zjY#ixVb3?Hm1JmaC@#84g3rGh^yQt1N4RQ>74N*A5j0 zH0eWtc_OVKM%WTdA4UrsNOFcaqO0)x@pD`-xMDvG}j?8}ak;obsn~Q+Y+%LlJQZ2ZRH{0pWmf zKsX>A5Dxqo9k7pMw-Qa*ZSu^Qt@-umo;fH< z6c_-?9JQapDjTr<>Y1xsbC^yW