Mhamdans17 commited on
Commit
cd3f69c
·
1 Parent(s): c7c843a

feat: add max branches and cashiers limit, refine admin UI settings, implement tier-based qris fee

Browse files
src/components/Sidebar.jsx CHANGED
@@ -102,7 +102,7 @@ export default function Sidebar() {
102
  <span className="material-symbols-outlined" style={{ fontSize: '20px', fontVariationSettings: active ? "'FILL' 1" : "'FILL' 0", flexShrink: 0 }}>
103
  {menu.icon}
104
  </span>
105
- <span className="sidebar-label" style={{ flex: 1, textAlign: 'left', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{menu.label}</span>
106
  </button>
107
  );
108
  })}
 
102
  <span className="material-symbols-outlined" style={{ fontSize: '20px', fontVariationSettings: active ? "'FILL' 1" : "'FILL' 0", flexShrink: 0 }}>
103
  {menu.icon}
104
  </span>
105
+ <span className="sidebar-label">{menu.label}</span>
106
  </button>
107
  );
108
  })}
src/index.css CHANGED
@@ -242,6 +242,7 @@ h1, h2, h3, .font-headline { font-family: var(--font-headline); }
242
  background-color: #ffedeb;
243
  }
244
  .sticky-col {
 
245
  position: sticky;
246
  left: 0;
247
  z-index: 5;
@@ -249,6 +250,7 @@ h1, h2, h3, .font-headline { font-family: var(--font-headline); }
249
  border-right: 1px solid rgba(78,33,32,0.06);
250
  }
251
  .sticky-header {
 
252
  position: sticky;
253
  left: 0;
254
  z-index: 10;
 
242
  background-color: #ffedeb;
243
  }
244
  .sticky-col {
245
+ position: -webkit-sticky;
246
  position: sticky;
247
  left: 0;
248
  z-index: 5;
 
250
  border-right: 1px solid rgba(78,33,32,0.06);
251
  }
252
  .sticky-header {
253
+ position: -webkit-sticky;
254
  position: sticky;
255
  left: 0;
256
  z-index: 10;
src/pages/AdminTenantSettings.jsx CHANGED
@@ -39,6 +39,14 @@ export default function AdminTenantSettings() {
39
  });
40
  };
41
 
 
 
 
 
 
 
 
 
42
  const handleCancelChanges = () => {
43
  setPendingChanges({});
44
  };
@@ -184,32 +192,78 @@ export default function AdminTenantSettings() {
184
  </label>
185
  </td>
186
  <td style={{ padding: '16px 12px', textAlign: 'center' }}>
187
- <label style={{ display: 'inline-flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
188
- <input
189
- type="checkbox"
190
- style={{ opacity: 0, position: 'absolute', width: 0, height: 0 }}
191
- checked={pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)}
192
- onChange={() => handleToggleSetting(c.id, 'enable_multi_branch', pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true))}
193
- disabled={applying}
194
- />
195
- <div style={{ width: '48px', height: '26px', borderRadius: '13px', background: (pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)) ? '#059669' : '#e5e7eb', position: 'relative', transition: '0.2s' }}>
196
- <div style={{ width: '22px', height: '22px', borderRadius: '50%', background: 'white', position: 'absolute', top: '2px', left: (pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)) ? '24px' : '2px', transition: '0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)' }} />
197
- </div>
198
- </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  </td>
200
  <td style={{ padding: '16px 12px', textAlign: 'center' }}>
201
- <label style={{ display: 'inline-flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
202
- <input
203
- type="checkbox"
204
- style={{ opacity: 0, position: 'absolute', width: 0, height: 0 }}
205
- checked={pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)}
206
- onChange={() => handleToggleSetting(c.id, 'enable_multi_kasir', pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true))}
207
- disabled={applying}
208
- />
209
- <div style={{ width: '48px', height: '26px', borderRadius: '13px', background: (pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)) ? '#059669' : '#e5e7eb', position: 'relative', transition: '0.2s' }}>
210
- <div style={{ width: '22px', height: '22px', borderRadius: '50%', background: 'white', position: 'absolute', top: '2px', left: (pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)) ? '24px' : '2px', transition: '0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)' }} />
211
- </div>
212
- </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  </td>
214
  </tr>
215
  ))}
 
39
  });
40
  };
41
 
42
+ const handleUpdateValue = (companyId, settingKey, value) => {
43
+ setPendingChanges(prev => {
44
+ const companyChanges = prev[companyId] || {};
45
+ const newChanges = { ...companyChanges, [settingKey]: value };
46
+ return { ...prev, [companyId]: newChanges };
47
+ });
48
+ };
49
+
50
  const handleCancelChanges = () => {
51
  setPendingChanges({});
52
  };
 
192
  </label>
193
  </td>
194
  <td style={{ padding: '16px 12px', textAlign: 'center' }}>
195
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', alignItems: 'center' }}>
196
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
197
+ <input
198
+ type="checkbox"
199
+ style={{ opacity: 0, position: 'absolute', width: 0, height: 0 }}
200
+ checked={pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)}
201
+ onChange={() => handleToggleSetting(c.id, 'enable_multi_branch', pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true))}
202
+ disabled={applying}
203
+ />
204
+ <div style={{ width: '48px', height: '26px', borderRadius: '13px', background: (pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)) ? '#059669' : '#e5e7eb', position: 'relative', transition: '0.2s' }}>
205
+ <div style={{ width: '22px', height: '22px', borderRadius: '50%', background: 'white', position: 'absolute', top: '2px', left: (pendingChanges[c.id]?.enable_multi_branch ?? (c.enable_multi_branch ?? true)) ? '24px' : '2px', transition: '0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)' }} />
206
+ </div>
207
+ </label>
208
+ { (pendingChanges[c.id]?.enable_multi_branch ?? c.enable_multi_branch) && (
209
+ <div className="animate-fade-in" style={{ display: 'flex', alignItems: 'center', gap: '6px', background: 'rgba(78,33,32,0.04)', padding: '4px 10px', borderRadius: '8px', marginTop: '4px' }}>
210
+ <span style={{ fontSize: '9px', fontWeight: 800, color: 'rgba(78,33,32,0.5)', letterSpacing: '0.05em' }}>LIMIT</span>
211
+ <input
212
+ type="text"
213
+ style={{
214
+ width: '40px', padding: '2px', background: 'transparent', border: 'none',
215
+ borderBottom: '1px solid rgba(78,33,32,0.2)', fontSize: '13px', fontWeight: 800,
216
+ color: '#4e2120', textAlign: 'center', outline: 'none'
217
+ }}
218
+ value={(pendingChanges[c.id]?.max_branches ?? (c.max_branches || 0)) === 0 ? '∞' : (pendingChanges[c.id]?.max_branches ?? (c.max_branches || 0))}
219
+ onChange={e => {
220
+ const val = e.target.value.replace(/[^0-9]/g, '');
221
+ handleUpdateValue(c.id, 'max_branches', parseInt(val) || 0);
222
+ }}
223
+ onFocus={e => { if (e.target.value === '∞') e.target.value = ''; }}
224
+ disabled={applying}
225
+ title="Ketik angka, 0 = Tak Terbatas"
226
+ />
227
+ </div>
228
+ )}
229
+ </div>
230
  </td>
231
  <td style={{ padding: '16px 12px', textAlign: 'center' }}>
232
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', alignItems: 'center' }}>
233
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
234
+ <input
235
+ type="checkbox"
236
+ style={{ opacity: 0, position: 'absolute', width: 0, height: 0 }}
237
+ checked={pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)}
238
+ onChange={() => handleToggleSetting(c.id, 'enable_multi_kasir', pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true))}
239
+ disabled={applying}
240
+ />
241
+ <div style={{ width: '48px', height: '26px', borderRadius: '13px', background: (pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)) ? '#059669' : '#e5e7eb', position: 'relative', transition: '0.2s' }}>
242
+ <div style={{ width: '22px', height: '22px', borderRadius: '50%', background: 'white', position: 'absolute', top: '2px', left: (pendingChanges[c.id]?.enable_multi_kasir ?? (c.enable_multi_kasir ?? true)) ? '24px' : '2px', transition: '0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)' }} />
243
+ </div>
244
+ </label>
245
+ { (pendingChanges[c.id]?.enable_multi_kasir ?? c.enable_multi_kasir) && (
246
+ <div className="animate-fade-in" style={{ display: 'flex', alignItems: 'center', gap: '6px', background: 'rgba(78,33,32,0.04)', padding: '4px 10px', borderRadius: '8px', marginTop: '4px' }}>
247
+ <span style={{ fontSize: '9px', fontWeight: 800, color: 'rgba(78,33,32,0.5)', letterSpacing: '0.05em' }}>LIMIT</span>
248
+ <input
249
+ type="text"
250
+ style={{
251
+ width: '40px', padding: '2px', background: 'transparent', border: 'none',
252
+ borderBottom: '1px solid rgba(78,33,32,0.2)', fontSize: '13px', fontWeight: 800,
253
+ color: '#4e2120', textAlign: 'center', outline: 'none'
254
+ }}
255
+ value={(pendingChanges[c.id]?.max_cashiers ?? (c.max_cashiers || 0)) === 0 ? '∞' : (pendingChanges[c.id]?.max_cashiers ?? (c.max_cashiers || 0))}
256
+ onChange={e => {
257
+ const val = e.target.value.replace(/[^0-9]/g, '');
258
+ handleUpdateValue(c.id, 'max_cashiers', parseInt(val) || 0);
259
+ }}
260
+ onFocus={e => { if (e.target.value === '∞') e.target.value = ''; }}
261
+ disabled={applying}
262
+ title="Ketik angka, 0 = Tak Terbatas"
263
+ />
264
+ </div>
265
+ )}
266
+ </div>
267
  </td>
268
  </tr>
269
  ))}
src/pages/Companies.jsx CHANGED
@@ -33,6 +33,7 @@ export default function Companies() {
33
  const [feeTarget, setFeeTarget] = useState(null);
34
  const [feeForm, setFeeForm] = useState({
35
  cash_fee_rules: [],
 
36
  qris_fee_percentage: 1.7,
37
  qris_fee_fixed: 500,
38
  qris_fee_min: 850,
@@ -136,8 +137,15 @@ export default function Companies() {
136
  { min_amount: 150001, max_amount: 999999999, fee: 3000 }
137
  ];
138
  }
 
 
 
 
 
 
139
  setFeeForm({
140
  cash_fee_rules: cashRules,
 
141
  qris_fee_percentage: c.qris_fee_percentage ?? 1.7,
142
  qris_fee_fixed: c.qris_fee_fixed ?? 500,
143
  qris_fee_min: c.qris_fee_min ?? 850,
@@ -183,6 +191,13 @@ export default function Companies() {
183
  max_amount: parseInt(r.max_amount),
184
  fee: parseInt(r.fee)
185
  })),
 
 
 
 
 
 
 
186
  qris_fee_percentage: parseFloat(feeForm.qris_fee_percentage),
187
  qris_fee_fixed: parseInt(feeForm.qris_fee_fixed),
188
  qris_fee_min: parseInt(feeForm.qris_fee_min),
@@ -493,28 +508,42 @@ export default function Companies() {
493
  </h3>
494
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
495
  {feeForm.cash_fee_rules.map((rule, idx) => (
496
- <div key={idx} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr auto', gap: '8px', alignItems: 'center' }}>
497
- <input type="number" placeholder="Min" value={rule.min_amount} onChange={e => {
498
- const newRules = [...feeForm.cash_fee_rules];
499
- newRules[idx].min_amount = e.target.value;
500
- setFeeForm({ ...feeForm, cash_fee_rules: newRules });
501
- }} style={INPUT} />
502
- <input type="number" placeholder="Max" value={rule.max_amount} onChange={e => {
503
- const newRules = [...feeForm.cash_fee_rules];
504
- newRules[idx].max_amount = e.target.value;
505
- setFeeForm({ ...feeForm, cash_fee_rules: newRules });
506
- }} style={INPUT} />
507
- <input type="number" placeholder="Fee" value={rule.fee} onChange={e => {
508
- const newRules = [...feeForm.cash_fee_rules];
509
- newRules[idx].fee = e.target.value;
510
- setFeeForm({ ...feeForm, cash_fee_rules: newRules });
511
- }} style={INPUT} />
512
- <button onClick={() => {
513
- const newRules = feeForm.cash_fee_rules.filter((_, i) => i !== idx);
514
- setFeeForm({ ...feeForm, cash_fee_rules: newRules });
515
- }} style={{ background: 'none', border: 'none', color: '#dc2626', cursor: 'pointer', padding: '4px' }}>
516
- <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>delete</span>
517
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  </div>
519
  ))}
520
  <button onClick={() => {
@@ -533,22 +562,90 @@ export default function Companies() {
533
  <span className="material-symbols-outlined" style={{ fontSize: '18px', color: '#ab2e00' }}>qr_code_scanner</span>
534
  Aturan Fee QRIS
535
  </h3>
536
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
537
- <div>
538
- <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Persentase (%)</label>
539
- <input type="number" step="0.1" value={feeForm.qris_fee_percentage} onChange={e => setFeeForm({ ...feeForm, qris_fee_percentage: e.target.value })} style={INPUT} />
540
- </div>
541
- <div>
542
- <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Fee Tetap (+Rp)</label>
543
- <input type="number" value={feeForm.qris_fee_fixed} onChange={e => setFeeForm({ ...feeForm, qris_fee_fixed: e.target.value })} style={INPUT} />
544
- </div>
545
- <div>
546
- <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Potongan Minimal (Rp)</label>
547
- <input type="number" value={feeForm.qris_fee_min} onChange={e => setFeeForm({ ...feeForm, qris_fee_min: e.target.value })} style={INPUT} />
548
- </div>
549
- <div>
550
- <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Minimal Transaksi (Rp)</label>
551
- <input type="number" value={feeForm.qris_min_transaction} onChange={e => setFeeForm({ ...feeForm, qris_min_transaction: e.target.value })} style={INPUT} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  </div>
553
  </div>
554
 
 
33
  const [feeTarget, setFeeTarget] = useState(null);
34
  const [feeForm, setFeeForm] = useState({
35
  cash_fee_rules: [],
36
+ qris_fee_rules: [],
37
  qris_fee_percentage: 1.7,
38
  qris_fee_fixed: 500,
39
  qris_fee_min: 850,
 
137
  { min_amount: 150001, max_amount: 999999999, fee: 3000 }
138
  ];
139
  }
140
+ let qrisRules = [];
141
+ try {
142
+ if (c.qris_fee_rules) qrisRules = JSON.parse(c.qris_fee_rules);
143
+ } catch (e) {
144
+ console.error(e);
145
+ }
146
  setFeeForm({
147
  cash_fee_rules: cashRules,
148
+ qris_fee_rules: qrisRules,
149
  qris_fee_percentage: c.qris_fee_percentage ?? 1.7,
150
  qris_fee_fixed: c.qris_fee_fixed ?? 500,
151
  qris_fee_min: c.qris_fee_min ?? 850,
 
191
  max_amount: parseInt(r.max_amount),
192
  fee: parseInt(r.fee)
193
  })),
194
+ qris_fee_rules: feeForm.qris_fee_rules.map(r => ({
195
+ min_amount: parseInt(r.min_amount),
196
+ max_amount: parseInt(r.max_amount),
197
+ fee_percentage: parseFloat(r.fee_percentage),
198
+ fee_fixed: parseInt(r.fee_fixed),
199
+ fee_min: parseInt(r.fee_min)
200
+ })),
201
  qris_fee_percentage: parseFloat(feeForm.qris_fee_percentage),
202
  qris_fee_fixed: parseInt(feeForm.qris_fee_fixed),
203
  qris_fee_min: parseInt(feeForm.qris_fee_min),
 
508
  </h3>
509
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
510
  {feeForm.cash_fee_rules.map((rule, idx) => (
511
+ <div key={idx} style={{ background: '#f9fafb', padding: '16px', borderRadius: '12px', position: 'relative', border: '1px solid #e5e7eb', marginBottom: '8px' }}>
512
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
513
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#4e2120' }}>TIER {idx + 1}</span>
514
+ <button onClick={() => {
515
+ const newRules = feeForm.cash_fee_rules.filter((_, i) => i !== idx);
516
+ setFeeForm({ ...feeForm, cash_fee_rules: newRules });
517
+ }} style={{ background: 'none', border: 'none', color: '#dc2626', cursor: 'pointer', padding: '4px', display: 'flex' }}>
518
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>delete</span>
519
+ </button>
520
+ </div>
521
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px' }}>
522
+ <div>
523
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Min (Rp)</label>
524
+ <input type="number" placeholder="0" value={rule.min_amount} onChange={e => {
525
+ const newRules = [...feeForm.cash_fee_rules];
526
+ newRules[idx].min_amount = e.target.value;
527
+ setFeeForm({ ...feeForm, cash_fee_rules: newRules });
528
+ }} style={INPUT} />
529
+ </div>
530
+ <div>
531
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Max (Rp)</label>
532
+ <input type="number" placeholder="9999999" value={rule.max_amount} onChange={e => {
533
+ const newRules = [...feeForm.cash_fee_rules];
534
+ newRules[idx].max_amount = e.target.value;
535
+ setFeeForm({ ...feeForm, cash_fee_rules: newRules });
536
+ }} style={INPUT} />
537
+ </div>
538
+ <div>
539
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Fee Tetap (Rp)</label>
540
+ <input type="number" placeholder="0" value={rule.fee} onChange={e => {
541
+ const newRules = [...feeForm.cash_fee_rules];
542
+ newRules[idx].fee = e.target.value;
543
+ setFeeForm({ ...feeForm, cash_fee_rules: newRules });
544
+ }} style={INPUT} />
545
+ </div>
546
+ </div>
547
  </div>
548
  ))}
549
  <button onClick={() => {
 
562
  <span className="material-symbols-outlined" style={{ fontSize: '18px', color: '#ab2e00' }}>qr_code_scanner</span>
563
  Aturan Fee QRIS
564
  </h3>
565
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
566
+ {feeForm.qris_fee_rules.map((rule, idx) => (
567
+ <div key={idx} style={{ background: '#f9fafb', padding: '16px', borderRadius: '12px', position: 'relative', border: '1px solid #e5e7eb', marginBottom: '8px' }}>
568
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
569
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#4e2120' }}>TIER {idx + 1}</span>
570
+ <button onClick={() => {
571
+ const newRules = feeForm.qris_fee_rules.filter((_, i) => i !== idx);
572
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
573
+ }} style={{ background: 'none', border: 'none', color: '#dc2626', cursor: 'pointer', padding: '4px', display: 'flex' }}>
574
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>delete</span>
575
+ </button>
576
+ </div>
577
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
578
+ <div>
579
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Min (Rp)</label>
580
+ <input type="number" placeholder="0" value={rule.min_amount} onChange={e => {
581
+ const newRules = [...feeForm.qris_fee_rules];
582
+ newRules[idx].min_amount = e.target.value;
583
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
584
+ }} style={INPUT} />
585
+ </div>
586
+ <div>
587
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Max (Rp)</label>
588
+ <input type="number" placeholder="9999999" value={rule.max_amount} onChange={e => {
589
+ const newRules = [...feeForm.qris_fee_rules];
590
+ newRules[idx].max_amount = e.target.value;
591
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
592
+ }} style={INPUT} />
593
+ </div>
594
+ </div>
595
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px' }}>
596
+ <div>
597
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Persen (%)</label>
598
+ <input type="number" step="0.1" placeholder="0" value={rule.fee_percentage} onChange={e => {
599
+ const newRules = [...feeForm.qris_fee_rules];
600
+ newRules[idx].fee_percentage = e.target.value;
601
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
602
+ }} style={INPUT} />
603
+ </div>
604
+ <div>
605
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Tetap (+Rp)</label>
606
+ <input type="number" placeholder="0" value={rule.fee_fixed} onChange={e => {
607
+ const newRules = [...feeForm.qris_fee_rules];
608
+ newRules[idx].fee_fixed = e.target.value;
609
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
610
+ }} style={INPUT} />
611
+ </div>
612
+ <div>
613
+ <label style={{ display: 'block', fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px', color: '#834c4a' }}>Min (Rp)</label>
614
+ <input type="number" placeholder="0" value={rule.fee_min} onChange={e => {
615
+ const newRules = [...feeForm.qris_fee_rules];
616
+ newRules[idx].fee_min = e.target.value;
617
+ setFeeForm({ ...feeForm, qris_fee_rules: newRules });
618
+ }} style={INPUT} />
619
+ </div>
620
+ </div>
621
+ </div>
622
+ ))}
623
+ <button onClick={() => {
624
+ setFeeForm({ ...feeForm, qris_fee_rules: [...feeForm.qris_fee_rules, { min_amount: 0, max_amount: 0, fee_percentage: 0, fee_fixed: 0, fee_min: 0 }] });
625
+ }} style={{ background: 'transparent', border: '1px dashed #ab2e00', color: '#ab2e00', padding: '8px', borderRadius: '8px', cursor: 'pointer', fontSize: '12px', fontWeight: 600 }}>
626
+ + Tambah Tier QRIS
627
+ </button>
628
+ </div>
629
+
630
+ <div style={{ marginTop: '16px' }}>
631
+ <h4 style={{ fontSize: '12px', fontWeight: 700, color: '#834c4a', marginBottom: '8px', textTransform: 'uppercase' }}>Aturan Default (Jika tidak ada tier cocok)</h4>
632
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
633
+ <div>
634
+ <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Persentase (%)</label>
635
+ <input type="number" step="0.1" value={feeForm.qris_fee_percentage} onChange={e => setFeeForm({ ...feeForm, qris_fee_percentage: e.target.value })} style={INPUT} />
636
+ </div>
637
+ <div>
638
+ <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Fee Tetap (+Rp)</label>
639
+ <input type="number" value={feeForm.qris_fee_fixed} onChange={e => setFeeForm({ ...feeForm, qris_fee_fixed: e.target.value })} style={INPUT} />
640
+ </div>
641
+ <div>
642
+ <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Potongan Minimal (Rp)</label>
643
+ <input type="number" value={feeForm.qris_fee_min} onChange={e => setFeeForm({ ...feeForm, qris_fee_min: e.target.value })} style={INPUT} />
644
+ </div>
645
+ <div>
646
+ <label style={{ display: 'block', fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', marginBottom: '6px', color: '#834c4a' }}>Minimal Transaksi (Rp)</label>
647
+ <input type="number" value={feeForm.qris_min_transaction} onChange={e => setFeeForm({ ...feeForm, qris_min_transaction: e.target.value })} style={INPUT} />
648
+ </div>
649
  </div>
650
  </div>
651