StringFellow commited on
Commit
5b9b6e7
·
verified ·
1 Parent(s): f34a6b6

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +171 -121
app.js CHANGED
@@ -1,180 +1,230 @@
1
- const GAS_WEB_APP_URL = "https://script.google.com/macros/s/AKfycbykSpcWHYjyY9QpZFzQfs70-Jj69PfRdnRc4MI0IVUQd9sCDixKMKYNB_2xVbP8y_FKNQ/exec";
 
 
2
 
3
  let state = {
4
- user: null,
5
- config: null,
6
  data: [],
 
7
  currentRowIndex: null
8
  };
9
 
10
- // --- API HELPER ---
11
- // Using raw text POST prevents complex browser CORS Preflight errors with Apps Script
12
  async function apiCall(action, payload = {}) {
 
13
  const response = await fetch(GAS_WEB_APP_URL, {
14
  method: 'POST',
15
- body: JSON.stringify({ action, ...payload })
16
  });
17
  const res = await response.json();
18
  if (res.status === 'error') throw new Error(res.message);
19
  return res.data;
20
  }
21
 
22
- // --- AUTH & INIT ---
23
  async function login() {
24
- const user = document.getElementById('username').value;
25
- const pass = document.getElementById('password').value;
26
  try {
27
- const data = await apiCall('login', { username: user, password: pass });
28
- state.user = { username: user, role: data.role };
 
 
 
29
  document.getElementById('login-view').classList.add('d-none');
30
  document.getElementById('app-view').classList.remove('d-none');
31
- document.getElementById('user-role-badge').innerText = `Logged in as: ${data.role}`;
32
-
33
- if (data.role === 'Admin') document.getElementById('btn-admin').classList.remove('d-none');
34
 
35
- await loadConfigAndData();
 
 
 
36
  } catch (e) {
37
  document.getElementById('login-error').classList.remove('d-none');
38
  }
39
  }
40
 
41
  function logout() {
42
- state.user = null;
43
  document.getElementById('app-view').classList.add('d-none');
44
  document.getElementById('login-view').classList.remove('d-none');
45
- document.getElementById('username').value = '';
46
- document.getElementById('password').value = '';
47
  }
48
 
49
- async function loadConfigAndData() {
50
- state.config = await apiCall('getConfig');
51
- if (state.config) {
52
- state.data = await apiCall('getData');
53
- renderTable();
54
- }
 
 
55
  }
56
 
57
- // --- DATA DASHBOARD ---
58
- function renderTable() {
59
- if (!state.config || !state.config.schema) return;
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  const search = document.getElementById('searchInput').value.toLowerCase();
62
- const filtered = state.data.filter(row => Object.values(row).some(v => String(v).toLowerCase().includes(search)));
63
 
64
- let html = `<thead><tr>`;
65
- state.config.schema.forEach(col => html += `<th>${col.header}</th>`);
66
- html += `<th>Actions</th></tr></thead><tbody>`;
 
 
67
 
 
 
68
  filtered.forEach(row => {
69
- html += `<tr>`;
70
- state.config.schema.forEach(col => html += `<td>${row[col.header] || ''}</td>`);
71
- html += `<td><button class="btn btn-sm btn-info text-white" onclick="openDataModal(${row._rowIndex})">Inspect/Edit</button></td></tr>`;
 
 
 
 
 
 
 
 
 
 
 
 
72
  });
73
- html += `</tbody>`;
74
- document.getElementById('dataTable').innerHTML = html;
75
  }
76
 
77
- function openDataModal(rowIndex = null) {
78
- if (!state.config) return alert("System not configured by admin yet.");
79
- state.currentRowIndex = rowIndex;
80
- const rowData = rowIndex !== null ? state.data.find(r => r._rowIndex === rowIndex) : {};
 
 
 
 
 
 
 
 
81
 
82
- let html = '';
83
- state.config.schema.forEach(field => {
84
- let val = rowData[field.header] || '';
85
- if (field.type === 'text') {
86
- html += `<div class="mb-3"><label>${field.header}</label><input type="text" class="form-control data-field" data-key="${field.header}" value="${val}"></div>`;
87
- } else if (field.type === 'dropdown') {
88
- let opts = field.options.split(',').map(o => `<option value="${o.trim()}" ${val==o.trim()?'selected':''}>${o.trim()}</option>`).join('');
89
- html += `<div class="mb-3"><label>${field.header}</label><select class="form-select data-field" data-key="${field.header}"><option value="">Select...</option>${opts}</select></div>`;
90
- } else if (field.type === 'radio') {
91
- let opts = field.options.split(',').map((o, i) => `
92
- <div class="form-check">
93
- <input class="form-check-input data-field" type="radio" name="${field.header}" data-key="${field.header}" value="${o.trim()}" ${val==o.trim()?'checked':''}>
94
- <label class="form-check-label">${o.trim()}</label>
95
- </div>
96
- `).join('');
97
- html += `<div class="mb-3"><label>${field.header}</label>${opts}</div>`;
98
- }
99
- });
100
-
101
- document.getElementById('dataFormContainer').innerHTML = html;
102
  new coreui.Modal(document.getElementById('dataModal')).show();
103
  }
104
 
105
- async function saveData() {
106
- let rowData = {};
107
- document.querySelectorAll('.data-field').forEach(el => {
108
- if (el.type === 'radio') {
109
- if(el.checked) rowData[el.dataset.key] = el.value;
110
- } else {
111
- rowData[el.dataset.key] = el.value;
112
- }
113
- });
114
-
115
- await apiCall('saveRow', { rowData, rowIndex: state.currentRowIndex });
116
- closeModal('dataModal');
117
- await loadConfigAndData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
- // --- ADMIN CONFIGURATION ---
121
- function openAdminModal() {
122
- if (state.config) {
123
- document.getElementById('adminSheetUrl').value = `https://docs.google.com/spreadsheets/d/${state.config.targetSheetId}`;
124
- }
125
- new coreui.Modal(document.getElementById('adminModal')).show();
126
  }
127
 
128
- async function fetchSheets() {
129
- const url = document.getElementById('adminSheetUrl').value;
130
- const sheets = await apiCall('getRemoteSheets', { sheetUrl: url });
131
- const select = document.getElementById('adminSheetSelect');
132
- select.innerHTML = sheets.map(s => `<option value="${s}">${s}</option>`).join('');
133
- document.getElementById('adminSheetSelectGroup').classList.remove('d-none');
134
  }
135
 
136
- async function fetchColumns() {
137
- const url = document.getElementById('adminSheetUrl').value;
138
- const sheetName = document.getElementById('adminSheetSelect').value;
139
- const columns = await apiCall('getRemoteColumns', { sheetUrl: url, sheetName });
140
-
141
- let html = columns.map(col => `
142
- <tr>
143
- <td class="col-header">${col}</td>
144
- <td>
145
- <select class="form-select col-type" onchange="this.parentElement.nextElementSibling.children[0].classList.toggle('d-none', this.value === 'text')">
146
- <option value="text">Textbox</option>
147
- <option value="dropdown">Dropdown</option>
148
- <option value="radio">Radio</option>
149
- </select>
150
- </td>
151
- <td><input type="text" class="form-control col-options d-none" placeholder="e.g. Option1, Option2"></td>
152
- </tr>
153
- `).join('');
154
-
155
- document.getElementById('adminColumnsBody').innerHTML = html;
156
- document.getElementById('adminColumnsTable').classList.remove('d-none');
157
  }
158
 
159
- async function saveAdminConfig() {
160
- const schema = [];
161
- document.querySelectorAll('#adminColumnsBody tr').forEach(tr => {
162
- schema.push({
163
- header: tr.querySelector('.col-header').innerText,
164
- type: tr.querySelector('.col-type').value,
165
- options: tr.querySelector('.col-options').value
166
- });
167
- });
168
 
169
  const payload = {
170
- sheetUrl: document.getElementById('adminSheetUrl').value,
171
- sheetName: document.getElementById('adminSheetSelect').value,
172
- schema: schema
 
 
 
173
  };
174
 
175
- await apiCall('saveConfig', { config: payload });
176
- closeModal('adminModal');
177
- await loadConfigAndData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
 
180
  function closeModal(id) {
 
1
+ // A simple base64 decode to mildly obfuscate the URL from casual source-code reading.
2
+ const ENCODED_URL = "aHR0cHM6Ly9zY3JpcHQuZ29vZ2xlLmNvbS9tYWNyb3Mvcy9BS2Z5Y2J3LW5TRXR5N2ZIMkRoM1BQYmdZNVlUOXdNOUY2eWJMR2p2MEZGMjFlYVF5M3g2MGxmZGl2VGJMMU5MLVRhVXlHTXpJZy9leGVj";
3
+ const GAS_WEB_APP_URL = atob(ENCODED_URL);
4
 
5
  let state = {
6
+ user: null, role: null,
7
+ options: { items: [] },
8
  data: [],
9
+ currentMode: 'ADD', // 'ADD' or 'EDIT'
10
  currentRowIndex: null
11
  };
12
 
13
+ // API Helper
 
14
  async function apiCall(action, payload = {}) {
15
+ const reqPayload = { action, username: state.user, password: state.password, ...payload };
16
  const response = await fetch(GAS_WEB_APP_URL, {
17
  method: 'POST',
18
+ body: JSON.stringify(reqPayload)
19
  });
20
  const res = await response.json();
21
  if (res.status === 'error') throw new Error(res.message);
22
  return res.data;
23
  }
24
 
25
+ // Authentication
26
  async function login() {
27
+ const u = document.getElementById('username').value;
28
+ const p = document.getElementById('password').value;
29
  try {
30
+ const data = await apiCall('login', { username: u, password: p });
31
+ state.user = u;
32
+ state.password = p;
33
+ state.role = data.role;
34
+
35
  document.getElementById('login-view').classList.add('d-none');
36
  document.getElementById('app-view').classList.remove('d-none');
37
+ document.getElementById('user-role-badge').innerText = `Role: ${state.role}`;
 
 
38
 
39
+ if (state.role === 'User' || state.role === 'Admin') {
40
+ document.getElementById('btn-add-new').classList.remove('d-none');
41
+ }
42
+ await loadInitialData();
43
  } catch (e) {
44
  document.getElementById('login-error').classList.remove('d-none');
45
  }
46
  }
47
 
48
  function logout() {
49
+ state.user = null; state.password = null; state.role = null;
50
  document.getElementById('app-view').classList.add('d-none');
51
  document.getElementById('login-view').classList.remove('d-none');
 
 
52
  }
53
 
54
+ // Data Loading
55
+ async function loadInitialData() {
56
+ const result = await apiCall('getData');
57
+ state.options = result.options;
58
+ state.data = result.data;
59
+
60
+ populateDropdowns();
61
+ renderTable();
62
  }
63
 
64
+ function populateDropdowns() {
65
+ const buildOptions = (arr, valKey, labelKey = null) =>
66
+ `<option value="">Select...</option>` + arr.map(i => `<option value="${labelKey ? i[valKey] : i}">${labelKey ? i[labelKey] : i}</option>`).join('');
67
 
68
+ document.getElementById('f_supplier').innerHTML = buildOptions(state.options.suppliers);
69
+ document.getElementById('f_subInv').innerHTML = buildOptions(state.options.subinv);
70
+ document.getElementById('f_item').innerHTML = `<option value="">Select...</option>` +
71
+ state.options.items.map(i => `<option value="${i.display}">${i.display}</option>`).join('');
72
+ }
73
+
74
+ function updateUOM() {
75
+ const selectedItemDisplay = document.getElementById('f_item').value;
76
+ const itemData = state.options.items.find(i => i.display === selectedItemDisplay);
77
+ document.getElementById('f_uom').value = itemData ? itemData.uom : '';
78
+ }
79
+
80
+ // Table Rendering
81
+ function renderTable() {
82
  const search = document.getElementById('searchInput').value.toLowerCase();
83
+ const filtered = state.data.filter(r => Object.values(r).some(v => String(v).toLowerCase().includes(search)));
84
 
85
+ // Build Headers
86
+ let headHTML = `<tr><th>No. STG</th><th>Supplier</th><th>Item</th><th>Status</th>`;
87
+ if (state.role === 'User' || state.role === 'Admin') headHTML += `<th class="text-center">Action</th>`;
88
+ headHTML += `</tr>`;
89
+ document.getElementById('tableHead').innerHTML = headHTML;
90
 
91
+ // Build Body
92
+ let bodyHTML = '';
93
  filtered.forEach(row => {
94
+ let dateStr = row.tglDatang ? new Date(row.tglDatang).toLocaleDateString() : '';
95
+ bodyHTML += `<tr>
96
+ <td>${row.stgManual || '-'}</td>
97
+ <td>${row.supplier}</td>
98
+ <td>${row.item}</td>
99
+ <td><span class="badge ${row.stat === 'OK' ? 'bg-success' : 'bg-secondary'}">${row.stat}</span></td>`;
100
+
101
+ if (state.role === 'User' || state.role === 'Admin') {
102
+ bodyHTML += `<td class="text-center">
103
+ <button class="btn btn-warning btn-sm text-white py-0 px-2" onclick="openEditModal(${row._rowIndex})" title="Edit">
104
+ <i class="bi bi-pencil-square"></i>
105
+ </button>
106
+ </td>`;
107
+ }
108
+ bodyHTML += `</tr>`;
109
  });
110
+ document.getElementById('tableBody').innerHTML = bodyHTML;
 
111
  }
112
 
113
+ // Modals & Forms
114
+ function openAddModal() {
115
+ state.currentMode = 'ADD';
116
+ document.getElementById('recordForm').reset();
117
+ document.getElementById('modalTitle').innerText = "Kedatangan Baru";
118
+
119
+ document.getElementById('section-edit').classList.add('d-none');
120
+ document.getElementById('divider-edit').classList.add('d-none');
121
+
122
+ lockFields(false); // Enable base fields
123
+ document.getElementById('btnActionSave').innerText = "Save";
124
+ document.getElementById('btnActionSave').disabled = true;
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  new coreui.Modal(document.getElementById('dataModal')).show();
127
  }
128
 
129
+ function openEditModal(rowIndex) {
130
+ state.currentMode = 'EDIT';
131
+ state.currentRowIndex = rowIndex;
132
+ const rowData = state.data.find(r => r._rowIndex === rowIndex);
133
+
134
+ document.getElementById('modalTitle').innerText = "Edit Row";
135
+ document.getElementById('section-edit').classList.remove('d-none');
136
+ document.getElementById('divider-edit').classList.remove('d-none');
137
+
138
+ // Populate Base Fields
139
+ document.getElementById('f_supplier').value = rowData.supplier;
140
+ document.getElementById('f_tglDatang').value = formatDateForInput(rowData.tglDatang);
141
+ document.getElementById('f_noSj').value = rowData.noSj;
142
+ document.getElementById('f_item').value = rowData.item;
143
+ document.getElementById('f_qtyAwal').value = rowData.qtyAwal;
144
+ document.getElementById('f_qtyReject').value = rowData.qtyReject;
145
+ document.getElementById('f_qtyNett').value = rowData.qtyNett;
146
+ document.getElementById('f_uom').value = rowData.uom;
147
+ document.getElementById('f_subInv').value = rowData.subInv;
148
+ document.getElementById('f_stgManual').value = rowData.stgManual;
149
+ document.getElementById('f_ekspedisi').value = formatDateForInput(rowData.ekspedisi);
150
+
151
+ // Populate Edit Fields (Parse prefix PO/SKL/_)
152
+ const cleanPo = rowData.noPo ? String(rowData.noPo).replace('PO/SKL/_', '') : '';
153
+ document.getElementById('f_noPo').value = cleanPo;
154
+ document.getElementById('f_tglPo').value = formatDateForInput(rowData.tglPo);
155
+ document.getElementById('f_noReceive').value = rowData.noReceive;
156
+ document.getElementById('f_notes').value = rowData.notes;
157
+
158
+ lockFields(true); // Disable non-editable base fields
159
+ document.getElementById('btnActionSave').innerText = "Update";
160
+ document.getElementById('btnActionSave').disabled = true;
161
+
162
+ new coreui.Modal(document.getElementById('dataModal')).show();
163
  }
164
 
165
+ function formatDateForInput(dateString) {
166
+ if (!dateString) return '';
167
+ const d = new Date(dateString);
168
+ return d.toISOString().split('T')[0];
 
 
169
  }
170
 
171
+ function lockFields(isEdit) {
172
+ // In Edit mode, Supplier, TglDatang, NoSj, Item are locked.
173
+ // qtyAwal, qtyReject, qtyNett, subInv, stgManual, ekspedisi remain editable.
174
+ const toLock = ['f_supplier', 'f_tglDatang', 'f_noSj', 'f_item'];
175
+ toLock.forEach(id => document.getElementById(id).disabled = isEdit);
 
176
  }
177
 
178
+ function checkFormValidity() {
179
+ let isValid = true;
180
+ if (state.currentMode === 'ADD') {
181
+ document.querySelectorAll('.req-add').forEach(el => {
182
+ if (!el.value.trim()) isValid = false;
183
+ });
184
+ } else {
185
+ // In edit mode, strict edit fields must be filled
186
+ document.querySelectorAll('.req-edit-strict').forEach(el => {
187
+ if (!el.value.trim()) isValid = false;
188
+ });
189
+ // And base editable fields must still be filled
190
+ document.querySelectorAll('.req-edit').forEach(el => {
191
+ if (!el.value.trim()) isValid = false;
192
+ });
193
+ }
194
+ document.getElementById('btnActionSave').disabled = !isValid;
 
 
 
 
195
  }
196
 
197
+ async function submitData() {
198
+ const btn = document.getElementById('btnActionSave');
199
+ btn.disabled = true;
200
+ btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Processing...`;
 
 
 
 
 
201
 
202
  const payload = {
203
+ qtyAwal: document.getElementById('f_qtyAwal').value,
204
+ qtyReject: document.getElementById('f_qtyReject').value,
205
+ qtyNett: document.getElementById('f_qtyNett').value,
206
+ subInv: document.getElementById('f_subInv').value,
207
+ stgManual: document.getElementById('f_stgManual').value.toUpperCase(),
208
+ ekspedisi: document.getElementById('f_ekspedisi').value
209
  };
210
 
211
+ if (state.currentMode === 'ADD') {
212
+ payload.supplier = document.getElementById('f_supplier').value;
213
+ payload.tglDatang = document.getElementById('f_tglDatang').value;
214
+ payload.noSj = document.getElementById('f_noSj').value;
215
+ payload.item = document.getElementById('f_item').value;
216
+ payload.uom = document.getElementById('f_uom').value;
217
+ await apiCall('saveRow', { rowData: payload });
218
+ } else {
219
+ payload.noPo = document.getElementById('f_noPo').value;
220
+ payload.tglPo = document.getElementById('f_tglPo').value;
221
+ payload.noReceive = document.getElementById('f_noReceive').value;
222
+ payload.notes = document.getElementById('f_notes').value;
223
+ await apiCall('updateRow', { rowData: payload, rowIndex: state.currentRowIndex });
224
+ }
225
+
226
+ closeModal('dataModal');
227
+ await loadInitialData();
228
  }
229
 
230
  function closeModal(id) {