forked from ariska/karung-web-admin
remove edit
This commit is contained in:
1 parent
fc617b9cf0
commit
c608580f16
2 files changed
+11
-251
No files matched your search
@@ -560,92 +560,6 @@ def get_cutoff_time_options(*selected_cutoff_times):
|
|||||||
|
|
||||||
return sorted(option_values)
|
return sorted(option_values)
|
||||||
|
|
||||||
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):
|
def get_json_data(json_path):
|
||||||
"""Fetch current data from JSON file"""
|
"""Fetch current data from JSON file"""
|
||||||
try:
|
try:
|
||||||
@@ -982,35 +896,6 @@ def get_k1_combined():
|
|||||||
response.headers['X-Request-ID'] = payload['meta']['requestId']
|
response.headers['X-Request-ID'] = payload['meta']['requestId']
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.route('/database/<db_name>/<int:row_id>/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/<db_name>/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'])
|
@app.route('/settings', methods=['POST'])
|
||||||
def update_dashboard_settings():
|
def update_dashboard_settings():
|
||||||
settings = get_settings_from_form()
|
settings = get_settings_from_form()
|
||||||
|
|||||||
+11
-136
@@ -429,8 +429,7 @@
|
|||||||
|
|
||||||
.theme-select,
|
.theme-select,
|
||||||
.selected-cycle-select,
|
.selected-cycle-select,
|
||||||
.settings-input,
|
.settings-input {
|
||||||
.edit-input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -449,8 +448,7 @@
|
|||||||
|
|
||||||
.theme-select:focus,
|
.theme-select:focus,
|
||||||
.selected-cycle-select:focus,
|
.selected-cycle-select:focus,
|
||||||
.settings-input:focus,
|
.settings-input:focus {
|
||||||
.edit-input:focus {
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 4px var(--accent-soft);
|
box-shadow: 0 0 0 4px var(--accent-soft);
|
||||||
}
|
}
|
||||||
@@ -494,8 +492,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected-cycle-button,
|
.selected-cycle-button,
|
||||||
.settings-button,
|
.settings-button {
|
||||||
.edit-button {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -510,8 +507,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="futuristic"] .selected-cycle-button,
|
html[data-theme="futuristic"] .selected-cycle-button,
|
||||||
html[data-theme="futuristic"] .settings-button,
|
html[data-theme="futuristic"] .settings-button {
|
||||||
html[data-theme="futuristic"] .edit-button {
|
|
||||||
color: #031016;
|
color: #031016;
|
||||||
border: 1px solid rgba(143, 251, 255, 0.65);
|
border: 1px solid rgba(143, 251, 255, 0.65);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -521,8 +517,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected-cycle-button:hover,
|
.selected-cycle-button:hover,
|
||||||
.settings-button:hover,
|
.settings-button:hover {
|
||||||
.edit-button:hover {
|
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 12px 26px var(--accent-soft);
|
box-shadow: 0 12px 26px var(--accent-soft);
|
||||||
}
|
}
|
||||||
@@ -817,37 +812,6 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-button {
|
|
||||||
background: linear-gradient(135deg, var(--balance), #86efac);
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.45;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-status {
|
|
||||||
width: 100%;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-status.has-changes {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-scroll {
|
.table-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -908,24 +872,6 @@
|
|||||||
background: rgba(56, 189, 248, 0.12);
|
background: rgba(56, 189, 248, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-input {
|
|
||||||
min-width: 96px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-input:disabled {
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-input.is-changed {
|
|
||||||
border-color: var(--warning);
|
|
||||||
background: rgba(245, 158, 11, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-form {
|
.settings-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||||
@@ -1006,8 +952,7 @@
|
|||||||
|
|
||||||
html[data-theme="classic"] .theme-select,
|
html[data-theme="classic"] .theme-select,
|
||||||
html[data-theme="classic"] .selected-cycle-select,
|
html[data-theme="classic"] .selected-cycle-select,
|
||||||
html[data-theme="classic"] .settings-input,
|
html[data-theme="classic"] .settings-input {
|
||||||
html[data-theme="classic"] .edit-input {
|
|
||||||
color: #333333;
|
color: #333333;
|
||||||
border: 1px solid #cccccc;
|
border: 1px solid #cccccc;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -1263,8 +1208,7 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="classic"] .settings-button,
|
html[data-theme="classic"] .settings-button {
|
||||||
html[data-theme="classic"] .edit-button {
|
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 11px 16px;
|
padding: 11px 16px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -1272,8 +1216,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="classic"] .settings-button:hover,
|
html[data-theme="classic"] .settings-button:hover {
|
||||||
html[data-theme="classic"] .edit-button:hover {
|
|
||||||
background: #1f2d3a;
|
background: #1f2d3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1334,19 +1277,10 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
justify-content: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-button,
|
|
||||||
.selected-cycle-button,
|
.selected-cycle-button,
|
||||||
.settings-button {
|
.settings-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-status {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -1357,7 +1291,7 @@
|
|||||||
<p class="eyebrow">Karung telemetry</p>
|
<p class="eyebrow">Karung telemetry</p>
|
||||||
<h1>Karung Counter Dashboard</h1>
|
<h1>Karung Counter Dashboard</h1>
|
||||||
<p class="hero-copy">
|
<p class="hero-copy">
|
||||||
Monitor realtime JSON counters, active cycle totals, balance movement, and editable database history from one responsive control surface.
|
Monitor realtime JSON counters, active cycle totals, balance movement, and database history from one responsive control surface.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-card">
|
<div class="theme-card">
|
||||||
@@ -1510,16 +1444,8 @@
|
|||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h3 class="table-title">Karung Tuang</h3>
|
<h3 class="table-title">Karung Tuang</h3>
|
||||||
{% if tuang_db_data %}
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="edit-button" type="button" onclick="enableTableEditor('tuang')">Edit</button>
|
|
||||||
<button id="tuang-save" class="edit-button save-button" form="tuang-edit-form" type="submit" disabled>Simpan</button>
|
|
||||||
<span id="tuang-edit-status" class="edit-status"></span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if tuang_db_data %}
|
{% if tuang_db_data %}
|
||||||
<form id="tuang-edit-form" action="{{ url_for('edit_db_rows', db_name='tuang') }}" method="post">
|
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1536,15 +1462,12 @@
|
|||||||
<span class="camera-cell"><span class="badge badge-tuang">TUANG</span> {{ item.camera_name }}</span>
|
<span class="camera-cell"><span class="badge badge-tuang">TUANG</span> {{ item.camera_name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.display_date }}</td>
|
<td>{{ item.display_date }}</td>
|
||||||
<td>
|
<td>{{ item.counter_value }}</td>
|
||||||
<input class="edit-input count tuang-counter-input" type="number" name="counter_value_{{ item.id }}" value="{{ item.counter_value }}" data-original-value="{{ item.counter_value }}" min="0" step="1" required disabled>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="no-data">Tidak ada data historis karung tuang</p>
|
<p class="no-data">Tidak ada data historis karung tuang</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1553,16 +1476,8 @@
|
|||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h3 class="table-title">Karung Masuk</h3>
|
<h3 class="table-title">Karung Masuk</h3>
|
||||||
{% if masuk_db_data %}
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="edit-button" type="button" onclick="enableTableEditor('masuk')">Edit</button>
|
|
||||||
<button id="masuk-save" class="edit-button save-button" form="masuk-edit-form" type="submit" disabled>Simpan</button>
|
|
||||||
<span id="masuk-edit-status" class="edit-status"></span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if masuk_db_data %}
|
{% if masuk_db_data %}
|
||||||
<form id="masuk-edit-form" action="{{ url_for('edit_db_rows', db_name='masuk') }}" method="post">
|
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1579,15 +1494,12 @@
|
|||||||
<span class="camera-cell"><span class="badge badge-masuk">MASUK</span> {{ item.camera_name }}</span>
|
<span class="camera-cell"><span class="badge badge-masuk">MASUK</span> {{ item.camera_name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.date }}</td>
|
<td>{{ item.date }}</td>
|
||||||
<td>
|
<td>{{ item.counter_value }}</td>
|
||||||
<input class="edit-input count masuk-counter-input" type="number" name="counter_value_{{ item.id }}" value="{{ item.counter_value }}" data-original-value="{{ item.counter_value }}" min="0" step="1" required disabled>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="no-data">Tidak ada data historis karung masuk</p>
|
<p class="no-data">Tidak ada data historis karung masuk</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1688,43 +1600,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTableEditorStatus(tableKey) {
|
|
||||||
const counterInputs = document.querySelectorAll(`.${tableKey}-counter-input`);
|
|
||||||
const statusElement = document.getElementById(`${tableKey}-edit-status`);
|
|
||||||
|
|
||||||
if (!statusElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedCount = Array.from(counterInputs).filter((counterInput) => {
|
|
||||||
const isChanged = counterInput.value !== counterInput.dataset.originalValue;
|
|
||||||
counterInput.classList.toggle('is-changed', isChanged);
|
|
||||||
return isChanged;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
statusElement.classList.toggle('has-changes', changedCount > 0);
|
|
||||||
statusElement.textContent = changedCount > 0
|
|
||||||
? `${changedCount} perubahan belum disimpan`
|
|
||||||
: 'Mode edit aktif';
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableTableEditor(tableKey) {
|
|
||||||
const counterInputs = document.querySelectorAll(`.${tableKey}-counter-input`);
|
|
||||||
const saveButton = document.getElementById(`${tableKey}-save`);
|
|
||||||
|
|
||||||
counterInputs.forEach((counterInput) => {
|
|
||||||
counterInput.disabled = false;
|
|
||||||
counterInput.oninput = () => updateTableEditorStatus(tableKey);
|
|
||||||
});
|
|
||||||
saveButton.disabled = false;
|
|
||||||
updateTableEditorStatus(tableKey);
|
|
||||||
|
|
||||||
if (counterInputs.length > 0) {
|
|
||||||
counterInputs[0].focus();
|
|
||||||
counterInputs[0].select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
|
|||||||
Reference in new issue
Block a user