Update besar apk finance

This commit is contained in:
Wian Drs
2026-09-11 16:03:00 +07:00
parent d42483b350
commit 4ef15c22ae
897 changed files with 98056 additions and 15979 deletions
@@ -0,0 +1,47 @@
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-1">Periode Accounting</h5>
<small class="text-muted">Soft close dan close hanya dapat dilakukan bila tidak ada jurnal pending.</small>
</div>
<button class="btn btn-warning" id="btnCreateYear"><i class="bi bi-calendar-plus me-1"></i> Tahun Buku</button>
</div>
<div class="table-responsive">
<table id="tablePeriods" class="table modern-table align-middle w-100">
<thead><tr><th>Tahun Buku</th><th>No</th><th>Periode</th><th>Mulai</th><th>Selesai</th><th>Status</th><th>Aksi</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
$(function(){
const table=$('#tablePeriods').DataTable({ajax:{url:"<?= base_url('accountingperiods/get_data') ?>",dataSrc:'data'},order:[[3,'desc']],responsive:true});
$('#btnCreateYear').click(async function(){
const input=await Swal.fire({title:'Buat Tahun Buku',input:'number',inputValue:new Date().getFullYear()+1,inputAttributes:{min:2000,max:2200},showCancelButton:true,inputValidator:v=>!v?'Tahun wajib diisi':undefined});
if(!input.isConfirmed)return;
$.post("<?= base_url('accountingperiods/create_year') ?>",{year:input.value},function(res){
if(res.status){table.ajax.reload();Swal.fire('Sukses',res.message,'success');}else Swal.fire('Error',res.message,'error');
},'json');
});
$(document).on('click','.btn-period-status',async function(){
const id=$(this).data('id'),status=$(this).data('status');
let reason='';
if(status==='open'){
const input=await Swal.fire({title:'Buka kembali periode',input:'textarea',inputLabel:'Alasan wajib diisi',showCancelButton:true,inputValidator:v=>!v?'Alasan wajib diisi':undefined});
if(!input.isConfirmed)return;
reason=input.value;
}else{
const confirm=await Swal.fire({title:status==='closed'?'Tutup periode?':'Soft close periode?',text:'Pastikan rekonsiliasi dan seluruh jurnal pending sudah diselesaikan.',icon:'warning',showCancelButton:true});
if(!confirm.isConfirmed)return;
}
$.post("<?= base_url('accountingperiods/change_status/') ?>"+id,{status:status,reason:reason},function(res){
if(res.status){table.ajax.reload(null,false);Swal.fire('Sukses',res.message,'success');}else Swal.fire('Error',res.message,'error');
},'json').fail(xhr=>Swal.fire('Error',xhr.responseJSON?.message||'Gagal mengubah periode','error'));
});
});
</script>
+48 -6
View File
@@ -18,6 +18,9 @@
<th>Tipe</th>
<th>Posisi</th>
<th>Kategori</th>
<th>Jenis</th>
<th>Visibilitas</th>
<th>Status</th>
<th width="15%">Aksi</th>
</tr>
</thead>
@@ -43,7 +46,8 @@
<input type="hidden" id="inputId">
<label class="fw-semibold">Kode Akun</label>
<input type="text" class="form-control" id="kode_akun">
<input type="text" class="form-control" id="kode_akun" inputmode="numeric" pattern="[0-9]{4}" maxlength="4" placeholder="Contoh: 1010">
<small class="text-muted">Gunakan tepat 4 angka.</small>
<label class="fw-semibold mt-2">Nama Akun</label>
<input type="text" class="form-control" id="nama_akun">
@@ -70,6 +74,23 @@
<?php endforeach; ?>
</select>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="is_header">
<label class="form-check-label" for="is_header">Akun header/grup</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="allow_posting" checked>
<label class="form-check-label" for="allow_posting">Boleh menerima posting jurnal</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="is_active" checked>
<label class="form-check-label" for="is_active">Aktif</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="is_hidden">
<label class="form-check-label" for="is_hidden">Sembunyikan akun</label>
</div>
</div>
<div class="modal-footer">
@@ -83,6 +104,11 @@
<script>
$(function(){
$('#is_header').on('change',function(){
if(this.checked) $('#allow_posting').prop('checked',false).prop('disabled',true);
else $('#allow_posting').prop('disabled',false);
});
let table = $('#tableAccounts').DataTable({
processing: true,
serverSide: true,
@@ -101,7 +127,10 @@ $(function(){
{ data: 4 },
{ data: 5 },
{ data: 6 },
{ data: 7, orderable:false }
{ data: 7 },
{ data: 8 },
{ data: 9 },
{ data: 10, orderable:false }
],
language: {
processing: `<div class="text-center"><div class="spinner-border text-warning"></div></div>`
@@ -116,6 +145,10 @@ $(function(){
$('#priority').val('');
$('#tipe').val('asset');
$('#parent_id').val('');
$('#is_header').prop('checked',false);
$('#allow_posting').prop('checked',true).prop('disabled',false);
$('#is_active').prop('checked',true);
$('#is_hidden').prop('checked',false);
$('#modalAccount .modal-title').text('Tambah Akun');
}
@@ -138,10 +171,14 @@ $(function(){
priority: $('#priority').val().trim(),
tipe: $('#tipe').val(),
parent_id: $('#parent_id').val()
,is_header: $('#is_header').is(':checked') ? 1 : 0
,allow_posting: $('#allow_posting').is(':checked') ? 1 : 0
,is_active: $('#is_active').is(':checked') ? 1 : 0
,is_hidden: $('#is_hidden').is(':checked') ? 1 : 0
};
if(!data.kode_akun || !data.nama_akun){
Swal.fire('Warning','Kode & Nama wajib diisi','warning');
if(!/^[0-9]{4}$/.test(data.kode_akun) || !data.nama_akun){
Swal.fire('Warning','Kode akun wajib 4 angka dan nama akun wajib diisi.','warning');
return;
}
@@ -182,6 +219,11 @@ $(function(){
$('#priority').val(res.priority);
$('#tipe').val(res.tipe);
$('#parent_id').val(res.parent_id);
$('#is_header').prop('checked',Number(res.is_header)===1);
$('#allow_posting').prop('checked',Number(res.allow_posting)===1);
$('#is_active').prop('checked',Number(res.is_active)===1);
$('#is_hidden').prop('checked',Number(res.is_hidden)===1);
$('#is_header').trigger('change');
$('#modalAccount .modal-title').text('Edit Akun');
$('#btnSimpan').data('action','edit');
@@ -205,7 +247,7 @@ $(function(){
}).then((result)=>{
if(result.isConfirmed){
$.get("<?= base_url('accounts/delete/'); ?>" + id, function(res){
$.post("<?= base_url('accounts/delete/'); ?>" + id, {}, function(res){
if(res.status){
table.ajax.reload(null,false);
@@ -220,4 +262,4 @@ $(function(){
});
});
</script>
</script>
+6
View File
@@ -0,0 +1,6 @@
<div class="container py-3">
<div class="d-flex justify-content-between align-items-center mb-3"><div><h4 class="mb-0">Kotak Persetujuan</h4><small class="text-muted">Transaksi yang menunggu tindakan Anda</small></div><span class="badge bg-primary"><?= count($requests) ?></span></div>
<div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>No.</th><th>Jenis</th><th>Pemohon</th><th>Nilai</th><th>Diajukan</th><th>Aksi</th></tr></thead><tbody>
<?php foreach($requests as $row): ?><tr><td><?= html_escape($row->request_no) ?></td><td><?= html_escape($row->workflow_name) ?><small class="d-block text-muted"><?=html_escape($row->entity_type)?> #<?=(int)$row->entity_id?></small></td><td><?= html_escape($row->requester_name ?: '-') ?></td><td class="text-end"><?= number_format($row->amount,0,',','.') ?></td><td><?= html_escape($row->requested_at) ?></td><td><a class="btn btn-sm btn-primary" href="<?=html_escape($row->destination_url)?>"><i class="bi bi-box-arrow-up-right"></i> <?=html_escape($row->destination_label)?></a></td></tr><?php endforeach; ?>
<?php if(!$requests): ?><tr><td colspan="6" class="text-center text-muted py-4">Tidak ada transaksi yang menunggu persetujuan.</td></tr><?php endif; ?></tbody></table></div>
</div>
+4 -1
View File
@@ -726,6 +726,7 @@ $('#s_qty').on('keyup change',function(){
});
// ================= SAVE FROM STOCK =================
let assetStockRequestKey=(window.crypto&&crypto.randomUUID)?crypto.randomUUID():(Date.now().toString(36)+'-'+Math.random().toString(36).slice(2));
$('#btnSaveStock').click(function(){
let btn = $(this);
let trackingType = ($('#s_item option:selected').data('type') || '').toString().trim().toUpperCase();
@@ -755,6 +756,7 @@ $('#btnSaveStock').click(function(){
account_asset: $('#s_account_asset').val(),
s_keterangan: $('#s_keterangan').val(),
barcode_id: $('#barcode_select').val(),
idempotency_key: assetStockRequestKey,
qty: qty
};
@@ -777,6 +779,7 @@ $('#btnSaveStock').click(function(){
$.post("<?= base_url('asset/save_from_stock'); ?>",data,function(res){
if(res.status){
assetStockRequestKey=(window.crypto&&crypto.randomUUID)?crypto.randomUUID():(Date.now().toString(36)+'-'+Math.random().toString(36).slice(2));
$('#modalAssetStock').modal('hide');
table.ajax.reload(null,false);
Swal.fire({
@@ -796,4 +799,4 @@ $('#btnSaveStock').click(function(){
});
</script>
</script>
+4 -8
View File
@@ -12,9 +12,6 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Make sure the path is correct in a real scenario -->
<link href="<?= base_url('assets/css/style-custom.css'); ?>" rel="stylesheet">
<style>
:root {
--orange-main: #ff8c00;
@@ -98,12 +95,13 @@
<?php endif; ?>
<form action="<?= base_url('auth/process_login'); ?>" method="post">
<input type="hidden" name="<?= $this->security->get_csrf_token_name(); ?>" value="<?= $this->security->get_csrf_hash(); ?>">
<div class="mb-3">
<label for="username" class="form-label fw-semibold">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-person"></i></span>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Masukkan username Anda" required>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Masukkan username Anda" autocomplete="username" maxlength="100" required autofocus>
</div>
</div>
@@ -111,7 +109,7 @@
<label for="password" class="form-label fw-semibold">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Masukkan password Anda" required>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Masukkan password Anda" autocomplete="current-password" required>
</div>
</div>
@@ -132,8 +130,6 @@
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<?php if($this->session->flashdata('error')): ?>
<script>
Swal.fire({
@@ -143,4 +139,4 @@ Swal.fire({
confirmButtonText: 'Coba Lagi'
});
</script>
<?php endif; ?>
<?php endif; ?>
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="id"><head><meta charset="utf-8"><title><?=html_escape($title)?></title><style>
@page{margin:24px 28px}body{font-family:DejaVu Sans,Arial,sans-serif;color:#172033;font-size:9px}.header{display:table;width:100%;padding-bottom:12px;border-bottom:2px solid #176bff}.header>div{display:table-cell;vertical-align:bottom}.header .right{text-align:right}.company{font-size:15px;font-weight:700}.title{margin:15px 0 3px;font-size:18px}.subtitle{color:#637087}.generated{color:#7b8798;font-size:8px}table{width:100%;border-collapse:collapse;margin-top:14px}th{padding:8px 7px;background:#edf3ff;color:#33415c;text-align:left;text-transform:uppercase;font-size:7.5px;letter-spacing:.03em;border:1px solid #dbe3ef}td{padding:7px;border:1px solid #e0e5ed;vertical-align:top}tbody tr:nth-child(even){background:#f9fafc}.number{text-align:right;white-space:nowrap}.empty{text-align:center;padding:28px;color:#778397}.footer{margin-top:12px;color:#7b8798;font-size:8px}
</style></head><body>
<div class="header"><div><div class="company"><?=html_escape($company?($company->name??$company->company_name??'Perusahaan'):'Perusahaan')?></div><div>Dokumen Budgeting</div></div><div class="right generated">Dibuat <?=date('d-m-Y H:i')?></div></div>
<h1 class="title"><?=html_escape($title)?></h1><div class="subtitle"><?=html_escape($subtitle)?></div>
<table><thead><tr><?php foreach($columns as$label):?><th><?=html_escape($label)?></th><?php endforeach?></tr></thead><tbody>
<?php foreach($rows as$row):?><tr><?php foreach($columns as$key=>$label):?><td class="<?=in_array($key,$money_columns,true)?'number':''?>"><?php if(in_array($key,$money_columns,true)):?>Rp <?=number_format((float)($row[$key]??0),2,',','.')?><?php else:?><?=html_escape($row[$key]??'-')?><?php endif?></td><?php endforeach?></tr><?php endforeach?>
<?php if(!$rows):?><tr><td colspan="<?=count($columns)?>" class="empty">Tidak ada data sesuai filter yang dipilih.</td></tr><?php endif?>
</tbody></table><div class="footer">Dokumen ini dihasilkan dari aplikasi accounting dan mengikuti filter yang dipilih pengguna.</div>
</body></html>
+75
View File
@@ -0,0 +1,75 @@
<?php
$monthNames=array(1=>'Januari',2=>'Februari',3=>'Maret',4=>'April',5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus',9=>'September',10=>'Oktober',11=>'November',12=>'Desember');
$accountTypes=array('asset'=>'Aset','liability'=>'Liabilitas','equity'=>'Ekuitas','revenue'=>'Pendapatan','expense'=>'Beban');
$periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$period);return isset($parts[1])?($monthNames[(int)$parts[1]].' '.$parts[0]):$period;};
?>
<div class="container-fluid py-3 budget-page">
<header class="budget-hero">
<div><small>PERENCANAAN KEUANGAN</small><h4>Budgeting</h4><p>Kelola nominal tetap per akun dan pantau akumulasinya untuk periode yang dipilih.</p></div>
<?php if($can_create):?><button type="button" class="btn btn-primary add-budget"><i class="bi bi-plus-lg me-1"></i> Tambah Budget</button><?php endif?>
</header>
<?php if($this->session->flashdata('success')):?><div class="alert alert-success"><?=html_escape($this->session->flashdata('success'))?></div><?php endif?>
<?php if($this->session->flashdata('error')):?><div class="alert alert-danger"><?=html_escape($this->session->flashdata('error'))?></div><?php endif?>
<nav class="budget-section-nav" aria-label="Bagian Budgeting">
<a class="<?=$active_tab==='list'?'active':''?>" href="<?=base_url('budgets?tab=list')?>"><i class="bi bi-card-checklist"></i><span>Daftar Budget</span><b><?=count($entries)?></b></a>
<a class="<?=$active_tab==='accounts'?'active':''?>" href="<?=base_url('budgets?tab=accounts')?>"><i class="bi bi-pie-chart"></i><span>Budget Per Akun</span><b><?=$budget_account_count?></b></a>
</nav>
<?php if($reminders):?><section class="budget-reminder-alert"><div class="reminder-icon"><i class="bi bi-bell"></i></div><div class="flex-grow-1"><strong><?=count($reminders)?> pengingat pembayaran perlu diperhatikan</strong><div class="reminder-items"><?php foreach(array_slice($reminders,0,4)as$reminder):?><span class="<?=$reminder->reminder_state==='overdue'?'overdue':''?>"><?=html_escape($reminder->title)?> · tanggal <?=$reminder->reminder_day?></span><?php endforeach?><?php if(count($reminders)>4):?><span>+<?=count($reminders)-4?> lainnya</span><?php endif?></div></div></section><?php endif?>
<?php if($active_tab==='list'):?>
<section class="budget-card">
<div class="budget-card-head"><div><h5>Daftar Budget</h5><p>Setiap kebutuhan dibuat sebagai jadwal tersendiri. Beberapa jadwal boleh menggunakan akun yang sama dan semuanya dijumlahkan.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Daftar Budget" data-url="<?=base_url('budgets/export_list/pdf?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_list/excel?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div>
<form class="budget-filter" method="get"><input type="hidden" name="tab" value="list"><div><label class="form-label">Filter Akun</label><select class="form-select" name="list_account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$list_account_id==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-outline-primary"><i class="bi bi-funnel me-1"></i>Tampilkan</button><?php if($list_account_id):?><a href="<?=base_url('budgets?tab=list')?>" class="btn btn-light">Reset</a><?php endif?></form>
<div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Budget / Akun</th><th>Pola</th><th>Mulai / Bulan</th><th class="text-end">Nominal Budget</th><th>Pengingat</th><th>Keterangan</th><th>Diperbarui</th><th>Aksi</th></tr></thead><tbody>
<?php foreach($entries as$entry):?><tr>
<td><strong><?=html_escape($entry->title)?></strong><small class="d-block text-muted"><?=html_escape($entry->kode_akun.' - '.$entry->nama_akun.' · '.($accountTypes[$entry->tipe]??ucfirst($entry->tipe)))?></small></td>
<td><?php if($entry->entry_type==='recurring'):?><span class="badge bg-primary-subtle text-primary">Berulang</span><small class="d-block text-muted mt-1">Ditambahkan setiap bulan</small><?php else:?><span class="badge bg-warning-subtle text-warning-emphasis">Satu Bulan</span><small class="d-block text-muted mt-1">Hanya bulan yang dipilih</small><?php endif?></td>
<td><?=html_escape($periodLabel($entry->period))?></td><td class="text-end fw-semibold">Rp <?=number_format($entry->amount,2,',','.')?></td>
<td><?php if(!(int)$entry->reminder_enabled):?><span class="text-muted">Tidak ada</span><?php else:?><strong><?=$entry->entry_type==='recurring'?'Setiap tanggal ':'Tanggal '?><?=$entry->reminder_day?></strong><?php $reminderLabels=array('completed'=>array('success','Selesai bulan ini'),'overdue'=>array('danger','Terlambat'),'due'=>array('danger','Jatuh tempo hari ini'),'upcoming'=>array('warning text-dark','Segera jatuh tempo'),'scheduled'=>array('secondary','Terjadwal'),'future'=>array('info text-dark','Belum berlaku'),'inactive_period'=>array('secondary','Bukan bulan berjalan'));$rl=$reminderLabels[$entry->reminder_state]??array('secondary','Terjadwal');?><small class="d-block mt-1"><span class="badge bg-<?=$rl[0]?>"><?=$rl[1]?></span></small><?php endif?></td>
<td><?=html_escape($entry->notes?:'-')?></td><td><small><?=html_escape($entry->updated_at?:$entry->created_at)?></small><small class="d-block text-muted"><?=html_escape($entry->updater_name?:$entry->creator_name?:'-')?></small></td>
<td class="text-nowrap"><?php if($can_update&&in_array($entry->reminder_state,array('scheduled','upcoming','due','overdue'),true)):?><button type="button" class="btn btn-sm btn-outline-success complete-reminder" data-id="<?=$entry->id?>" data-label="<?=html_escape($entry->title)?>"><i class="bi bi-check2"></i> Selesai Bulan Ini</button><?php endif?> <?php if($can_update):?><button type="button" class="btn btn-sm btn-outline-primary edit-budget" data-id="<?=$entry->id?>" data-title="<?=html_escape($entry->title)?>" data-account="<?=$entry->account_id?>" data-period="<?=html_escape($entry->period)?>" data-type="<?=html_escape($entry->entry_type)?>" data-amount="<?=html_escape($entry->amount)?>" data-reminder="<?=(int)$entry->reminder_enabled?>" data-day="<?=html_escape($entry->reminder_day)?>" data-notes="<?=html_escape($entry->notes)?>"><i class="bi bi-pencil"></i> Edit</button><?php endif?> <?php if($can_delete):?><button type="button" class="btn btn-sm btn-outline-danger delete-budget" data-id="<?=$entry->id?>" data-label="<?=html_escape($entry->title)?>"><i class="bi bi-x-circle"></i> Nonaktifkan</button><?php endif?></td>
</tr><?php endforeach?>
<?php if(!$entries):?><tr><td colspan="8" class="empty-state"><i class="bi bi-wallet2"></i><strong>Belum ada budget</strong><span>Tambahkan budget tetap untuk akun yang diperlukan.</span></td></tr><?php endif?>
</tbody></table></div>
</section>
<?php else:?>
<section class="budget-card">
<div class="budget-card-head"><div><h5>Budget Per Akun</h5><p>Hanya akun yang mempunyai budget yang ditampilkan. Budget berulang dihitung otomatis untuk setiap bulan dalam rentang.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Budget Per Akun" data-url="<?=base_url('budgets/export_accounts/pdf?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_accounts/excel?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div>
<form class="budget-filter budget-period-filter" method="get"><input type="hidden" name="tab" value="accounts"><div><label class="form-label">Dari Bulan</label><input type="month" class="form-control" name="from" value="<?=html_escape($filters['from'])?>" required></div><div><label class="form-label">Sampai Bulan</label><input type="month" class="form-control" name="to" value="<?=html_escape($filters['to'])?>" required></div><div><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun yang memiliki budget</option><?php foreach($budget_accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-primary"><i class="bi bi-search me-1"></i>Tampilkan</button></form>
<div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Akun</th><th class="text-end">Akumulasi Budget</th><th class="text-end">Realisasi</th><th class="text-end">Komitmen PO</th><th class="text-end">Total Pemakaian</th><th class="text-end">Sisa</th><th class="text-end">%</th><th>Kondisi</th></tr></thead><tbody>
<?php foreach($report_rows as$row):?><tr><td><strong><?=html_escape($row['kode_akun'].' - '.$row['nama_akun'])?></strong><small class="d-block text-muted"><?=html_escape($accountTypes[$row['tipe']]??ucfirst($row['tipe']))?></small></td><?php foreach(array('budget','actual','commitment','usage','remaining')as$key):?><td class="text-end <?=$key==='budget'?'fw-semibold':''?>">Rp <?=number_format($row[$key],2,',','.')?></td><?php endforeach?><td class="text-end"><?=number_format($row['percentage'],2,',','.')?>%</td><td><span class="badge bg-<?=$row['indicator']==='over'?'danger':($row['indicator']==='warning'?'warning text-dark':'success')?>"><?=$row['indicator']==='over'?'Melebihi':($row['indicator']==='warning'?'Mendekati Batas':'Aman')?></span></td></tr><?php endforeach?>
<?php if(!$report_rows):?><tr><td colspan="8" class="empty-state"><i class="bi bi-bar-chart"></i><strong>Belum ada budget pada periode ini</strong><span>Buat budget berulang atau budget khusus bulan terlebih dahulu.</span></td></tr><?php endif?>
</tbody></table></div>
</section>
<?php endif?>
</div>
<div class="modal fade" id="budgetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"><div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"><form class="modal-content" id="budgetForm" action="<?=base_url('budgets/save_entry')?>" method="post"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id" id="budgetId"><div class="modal-header"><div><small class="text-muted">BUDGET PER AKUN</small><h5 class="modal-title">Tambah Budget</h5></div><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body">
<label class="form-label">Nama Budget <span class="text-danger">*</span></label><input type="text" class="form-control mb-3" name="title" id="budgetTitle" maxlength="150" placeholder="Contoh: Pembayaran Listrik Kantor 1" required>
<label class="form-label">Akun <span class="text-danger">*</span></label><select class="form-select mb-3" name="account_id" id="budgetAccount" required><option value="">Pilih akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun.' · '.($accountTypes[$a->tipe]??ucfirst($a->tipe)))?></option><?php endforeach?></select>
<label class="form-label">Pola Budget <span class="text-danger">*</span></label><div class="budget-type-options mb-3"><label><input type="radio" name="entry_type" value="recurring" checked><span><b>Budget Berulang</b><small>Nominal ditambahkan setiap bulan mulai dari bulan yang dipilih.</small></span></label><label><input type="radio" name="entry_type" value="override"><span><b>Budget Satu Bulan</b><small>Nominal hanya ditambahkan pada satu bulan yang dipilih.</small></span></label></div>
<label class="form-label" id="periodLabel">Berlaku Mulai Bulan <span class="text-danger">*</span></label><input type="month" class="form-control mb-3" name="period" id="budgetPeriod" value="<?=date('Y-m')?>" required>
<label class="form-label">Nominal Budget per Bulan <span class="text-danger">*</span></label><div class="input-group mb-3"><span class="input-group-text">Rp</span><input type="text" class="form-control format-rupiah" inputmode="decimal" name="amount" id="budgetAmount" placeholder="0,00" required></div>
<div class="budget-reminder-option mb-3"><div class="form-check form-switch"><input class="form-check-input" type="checkbox" name="reminder_enabled" value="1" id="budgetReminder"><label class="form-check-label fw-semibold" for="budgetReminder">Aktifkan pengingat pembayaran bulanan</label></div><small class="text-muted">Pengingat akan muncul tujuh hari sebelum tanggal pembayaran.</small><div class="mt-3 d-none" id="reminderDayWrap"><label class="form-label">Tanggal Pembayaran <span class="text-danger">*</span></label><input type="number" class="form-control" name="reminder_day" id="budgetReminderDay" min="1" max="31" inputmode="numeric" placeholder="1–31"><small class="text-muted">Jika bulan lebih pendek, tanggal otomatis memakai hari terakhir bulan tersebut.</small></div></div>
<label class="form-label">Keterangan</label><textarea class="form-control" name="notes" id="budgetNotes" rows="3" placeholder="Contoh: Biaya sewa kantor tetap"></textarea>
</div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="saveBudget"><i class="bi bi-check2-circle me-1"></i>Simpan Budget</button></div></form></div></div>
<div class="modal fade" id="budgetPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content budget-pdf-content"><div class="modal-header"><h5 class="modal-title">Pratinjau PDF Budget</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF Budget"></iframe></div></div></div></div>
<style>
.budget-page{--budget-blue:#176bff}.budget-hero{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}.budget-hero small{font-size:.7rem;letter-spacing:.15em;color:#758199}.budget-hero h4{margin:2px 0 3px}.budget-hero p,.budget-card-head p{margin:0;color:#69758a}.budget-section-nav{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px;padding:8px;margin-bottom:16px;background:#fff;border:1px solid #e4e9f1;border-radius:14px}.budget-section-nav a{display:flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 14px;border-radius:9px;color:#526079;text-decoration:none;font-weight:600}.budget-section-nav a:hover{background:#f3f7ff;color:var(--budget-blue)}.budget-section-nav a.active{background:var(--budget-blue);color:#fff;box-shadow:0 5px 14px rgba(23,107,255,.2)}.budget-section-nav b{min-width:22px;padding:2px 6px;border-radius:20px;background:rgba(255,255,255,.22);font-size:.72rem;text-align:center}.budget-section-nav a:not(.active) b{background:#eef2f8}.budget-card{overflow:hidden;background:#fff;border:1px solid #e4e9f1;border-radius:14px;box-shadow:0 6px 22px rgba(22,34,55,.05)}.budget-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:17px 18px;border-bottom:1px solid #e8ecf2}.budget-card-head h5{margin:0 0 3px}.budget-filter{display:flex;align-items:end;gap:10px;padding:14px 18px;background:#fafbfd;border-bottom:1px solid #e8ecf2}.budget-filter>div{min-width:min(360px,100%)}.budget-period-filter{display:grid;grid-template-columns:180px 180px minmax(240px,1fr) auto}.budget-card .table th{padding:12px 14px;background:#f6f8fb;color:#59657a;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.budget-card .table td{padding:13px 14px}.empty-state{text-align:center!important;padding:54px 20px!important;color:#768196}.empty-state i,.empty-state strong,.empty-state span{display:block}.empty-state i{margin-bottom:8px;font-size:1.8rem}.empty-state strong{color:#344054}.budget-type-options{display:grid;gap:8px}.budget-type-options label{display:flex;gap:10px;padding:12px;border:1px solid #dfe5ee;border-radius:10px;cursor:pointer}.budget-type-options label:has(input:checked){border-color:var(--budget-blue);background:#f2f7ff}.budget-type-options input{margin-top:3px}.budget-type-options span,.budget-type-options small{display:block}.budget-type-options small{margin-top:2px;color:#6f7b8f;font-weight:400}@media(max-width:767.98px){.budget-hero{align-items:flex-start}.budget-section-nav{display:flex;overflow:auto}.budget-section-nav a{flex:1;white-space:nowrap}.budget-filter,.budget-period-filter{display:grid;grid-template-columns:1fr}.budget-filter>div{min-width:0}.budget-card-head{align-items:flex-start}.budget-card .table td,.budget-card .table th{white-space:nowrap}.modal-dialog{margin:.5rem}}
.budget-reminder-alert{display:flex;align-items:flex-start;gap:12px;margin-bottom:16px;padding:14px 16px;border:1px solid #ffd89a;border-radius:13px;background:#fff8e8;color:#6c4a09}.reminder-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:10px;background:#ffedc4;color:#e48300;font-size:1.1rem}.reminder-items{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}.reminder-items span{padding:4px 9px;border-radius:20px;background:#fff;border:1px solid #f2d79c;font-size:.78rem}.reminder-items span.overdue{border-color:#f2b8b8;background:#fff0f0;color:#b42318}.budget-reminder-option{padding:13px 14px;border:1px solid #dfe5ee;border-radius:10px;background:#fafbfd}
.budget-pdf-content{height:min(90vh,900px)}.budget-pdf-content .modal-body{min-height:0}.budget-pdf-content iframe{width:100%;height:100%;border:0}.budget-card-head .btn-group{flex:0 0 auto}@media(max-width:575.98px){.budget-card-head{flex-direction:column}.budget-card-head .btn-group{width:100%}.budget-card-head .btn-group>*{flex:1}}
</style>
<script>
document.addEventListener('DOMContentLoaded',function(){
const modalEl=document.getElementById('budgetModal'),modal=modalEl?bootstrap.Modal.getOrCreateInstance(modalEl):null,form=document.getElementById('budgetForm'),money=n=>Number(n||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
function reset(){form.reset();document.getElementById('budgetId').value='';document.getElementById('budgetPeriod').value='<?=date('Y-m')?>';document.querySelector('#budgetModal .modal-title').textContent='Tambah Budget';updatePeriodLabel();updateReminder()}
function updatePeriodLabel(){const recurring=form.querySelector('[name="entry_type"]:checked')?.value==='recurring';document.getElementById('periodLabel').innerHTML=(recurring?'Berlaku Mulai Bulan':'Bulan Budget')+' <span class="text-danger">*</span>'}
function updateReminder(){const enabled=document.getElementById('budgetReminder').checked,wrap=document.getElementById('reminderDayWrap'),day=document.getElementById('budgetReminderDay');wrap.classList.toggle('d-none',!enabled);day.required=enabled;if(!enabled)day.value=''}
document.querySelectorAll('[name="entry_type"]').forEach(x=>x.addEventListener('change',updatePeriodLabel));
document.getElementById('budgetReminder')?.addEventListener('change',updateReminder);
document.querySelector('.add-budget')?.addEventListener('click',()=>{reset();modal.show()});
document.querySelectorAll('.edit-budget').forEach(button=>button.addEventListener('click',()=>{reset();document.getElementById('budgetId').value=button.dataset.id;document.getElementById('budgetTitle').value=button.dataset.title||'';document.getElementById('budgetAccount').value=button.dataset.account;document.getElementById('budgetPeriod').value=button.dataset.period;document.getElementById('budgetAmount').value=money(button.dataset.amount);document.getElementById('budgetNotes').value=button.dataset.notes||'';document.getElementById('budgetReminder').checked=button.dataset.reminder==='1';document.getElementById('budgetReminderDay').value=button.dataset.day||'';const type=form.querySelector('[name="entry_type"][value="'+button.dataset.type+'"]');if(type)type.checked=true;document.querySelector('#budgetModal .modal-title').textContent='Edit Budget';updatePeriodLabel();updateReminder();modal.show()}));
form?.addEventListener('submit',async function(event){event.preventDefault();const button=document.getElementById('saveBudget');button.disabled=true;try{const response=await fetch(form.action,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:new FormData(form)}),result=await response.json().catch(()=>({status:false,message:'Respons server tidak valid.'}));if(!response.ok||!result.status)throw Error(result.message||'Budget gagal disimpan.');modal.hide();if(window.AppAlert)AppAlert.toast(result.message,'success');setTimeout(()=>location.href='<?=base_url('budgets?tab=list')?>',500)}catch(error){if(window.AppAlert)AppAlert.toast(error.message,'error');else Swal.fire({icon:'error',title:'Gagal',text:error.message})}finally{button.disabled=false}});
document.querySelectorAll('.delete-budget').forEach(button=>button.addEventListener('click',async()=>{const ask=window.AppAlert?await AppAlert.confirm({title:'Nonaktifkan budget?',text:button.dataset.label+' tidak lagi dipakai dalam perhitungan.',confirmButtonText:'Ya, Nonaktifkan'}):await Swal.fire({title:'Nonaktifkan budget?',text:button.dataset.label,icon:'warning',showCancelButton:true});if(!ask.isConfirmed)return;try{const body=new FormData();body.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');const response=await fetch('<?=base_url('budgets/delete_entry/')?>'+button.dataset.id,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:body}),result=await response.json().catch(()=>({status:false,message:'Respons server tidak valid.'}));if(!response.ok||!result.status)throw Error(result.message||'Budget gagal dinonaktifkan.');if(window.AppAlert)AppAlert.toast(result.message,'success');setTimeout(()=>location.reload(),500)}catch(error){if(window.AppAlert)AppAlert.toast(error.message,'error');else Swal.fire({icon:'error',title:'Gagal',text:error.message})}}));
document.querySelectorAll('.complete-reminder').forEach(button=>button.addEventListener('click',async()=>{const ask=window.AppAlert?await AppAlert.confirm({title:'Tandai pembayaran selesai?',text:button.dataset.label+' tidak akan muncul lagi sebagai pengingat pada bulan ini.',confirmButtonText:'Ya, Sudah Selesai'}):await Swal.fire({title:'Tandai pembayaran selesai?',text:button.dataset.label,icon:'question',showCancelButton:true});if(!ask.isConfirmed)return;try{const body=new FormData();body.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');const response=await fetch('<?=base_url('budgets/complete_reminder/')?>'+button.dataset.id,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:body}),result=await response.json().catch(()=>({status:false,message:'Respons server tidak valid.'}));if(!response.ok||!result.status)throw Error(result.message||'Pengingat gagal diperbarui.');if(window.AppAlert)AppAlert.toast(result.message,'success');setTimeout(()=>location.reload(),500)}catch(error){if(window.AppAlert)AppAlert.toast(error.message,'error');else Swal.fire({icon:'error',title:'Gagal',text:error.message})}}));
document.querySelectorAll('.budget-pdf').forEach(button=>button.addEventListener('click',()=>{const pdfModal=document.getElementById('budgetPdfModal');pdfModal.querySelector('.modal-title').textContent=button.dataset.title||'Pratinjau PDF Budget';pdfModal.querySelector('iframe').src=button.dataset.url;bootstrap.Modal.getOrCreateInstance(pdfModal).show()}));document.getElementById('budgetPdfModal')?.addEventListener('hidden.bs.modal',function(){this.querySelector('iframe').src='about:blank'});
});
</script>
+28
View File
@@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body{font-family:DejaVu Sans,sans-serif;font-size:9px;color:#202635}
h2{margin:0 0 4px}.meta{color:#667085;margin-bottom:14px}
.summary{width:100%;border-collapse:collapse}.summary th,.summary td{border:1px solid #d7dce5;padding:5px}
.summary th{background:#f1f4f8;text-align:left}.num{text-align:right}.footer{margin-top:20px;color:#667085}
</style>
</head>
<body>
<h2><?=html_escape($company->name??'Perusahaan')?> &mdash; Dokumen Budget</h2>
<div class="meta"><?=html_escape($budget->budget_no)?> &middot; <?=html_escape($budget->name)?> &middot; Tahun Fiskal <?=$budget->fiscal_year?> &middot; Versi <?=$budget->version_no?> &middot; Status <?=strtoupper(html_escape($budget->status))?></div>
<?php
$months=array('Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des');
$rows=array();
foreach($lines as$l){
$key=(int)$l->account_id;
if(!isset($rows[$key]))$rows[$key]=array('name'=>$l->kode_akun.' - '.$l->nama_akun,'amounts'=>array_fill(1,12,0));
$rows[$key]['amounts'][(int)substr($l->period,5,2)]+=(float)$l->amount;
}
?>
<table class="summary"><thead><tr><th>Akun</th><?php foreach($months as$m):?><th class="num"><?=$m?></th><?php endforeach?><th class="num">Total</th></tr></thead><tbody>
<?php $grand=0;foreach($rows as$r):$total=array_sum($r['amounts']);$grand+=$total;?><tr><td><?=html_escape($r['name'])?></td><?php foreach($r['amounts']as$a):?><td class="num"><?=number_format($a,2,',','.')?></td><?php endforeach?><td class="num"><strong><?=number_format($total,2,',','.')?></strong></td></tr><?php endforeach?>
<tr><th colspan="13">TOTAL BUDGET TAHUNAN</th><th class="num"><?=number_format($grand,2,',','.')?></th></tr></tbody></table>
<div class="footer">Dicetak <?=date('d-m-Y H:i')?>. Budget aktif tidak dapat diubah langsung; perubahan wajib melalui dokumen revisi.</div>
</body></html>
+5
View File
@@ -0,0 +1,5 @@
<div class="container-fluid py-3"><h4>Kas, Bank & Rekonsiliasi</h4><?php foreach(array('success','error')as$f)if($this->session->flashdata($f)):?><div class="alert alert-<?=$f==='success'?'success':'danger'?>"><?=html_escape($this->session->flashdata($f))?></div><?php endif?><div class="row mb-3"><?php foreach($positions as$p):?><div class="col-6 col-lg-3"><div class="card mb-2"><div class="card-body"><small><?=html_escape($p->name)?></small><h5>Rp <?=number_format($p->balance,0,',','.')?></h5></div></div></div><?php endforeach?></div>
<div class="row g-3"><div class="col-lg-6"><div class="card h-100"><div class="card-header">Kas masuk/keluar, petty cash, advance, reimbursement</div><div class="card-body"><form method="post" action="<?=base_url('cashbank/transaction')?>"><div class="row g-2"><div class="col-6"><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>"></div><div class="col-6"><select class="form-select" name="cash_account_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select></div><div class="col-6"><select class="form-select" name="type"><option value="receipt">Kas masuk</option><option value="payment">Kas keluar</option><option value="petty_cash">Petty cash</option><option value="cash_advance">Cash advance</option><option value="advance_settlement">Settlement advance</option><option value="reimbursement">Reimbursement</option><option value="bank_charge">Bank charge</option><option value="interest">Bunga bank</option></select></div><div class="col-6"><select class="form-select" name="direction"><option value="in">Masuk</option><option value="out">Keluar</option></select></div><div class="col-6"><input class="form-control" type="number" step=".01" name="amount" placeholder="Jumlah" required></div><div class="col-6"><select class="form-select" name="counter_account_id"><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-6"><input class="form-control" name="reference_no" placeholder="Referensi"></div><div class="col-6"><input class="form-control" name="idempotency_key" value="WEB-<?=date('YmdHis')?>-<?=bin2hex(random_bytes(3))?>"></div><div class="col-12"><textarea class="form-control" name="description" required placeholder="Keterangan transaksi yang detail"></textarea></div><div><button class="btn btn-success">Simpan/Post</button></div></div></form></div></div></div>
<div class="col-lg-6"><div class="card h-100"><div class="card-header">Transfer antar-rekening</div><div class="card-body"><form method="post" action="<?=base_url('cashbank/transfer')?>"><div class="row g-2"><div class="col-6"><select class="form-select" name="from_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>">Dari: <?=html_escape($c->name)?></option><?php endforeach?></select></div><div class="col-6"><select class="form-select" name="to_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>">Ke: <?=html_escape($c->name)?></option><?php endforeach?></select></div><div class="col-4"><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>"></div><div class="col-4"><input class="form-control" type="number" name="amount" placeholder="Jumlah"></div><div class="col-4"><input class="form-control" type="number" name="charge" value="0" placeholder="Biaya"></div><div class="col-12"><select class="form-select" name="charge_account_id"><?php foreach($accounts as$a):?><option value="<?=$a->id?>">Biaya: <?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-6"><input class="form-control" name="reference_no" placeholder="Referensi bank"></div><div class="col-6"><input class="form-control" name="idempotency_key" value="TRF-<?=date('YmdHis')?>-<?=bin2hex(random_bytes(3))?>"></div><div><button class="btn btn-primary">Post Transfer</button></div></div></form></div></div></div></div>
<div class="row g-3 mt-1"><div class="col-lg-6"><div class="card"><div class="card-header">Import & matching rekening koran (CSV)</div><div class="card-body"><form method="post" enctype="multipart/form-data" action="<?=base_url('cashbank/import')?>"><select class="form-select mb-2" name="cash_account_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select><input class="form-control mb-2" type="file" name="statement" accept=".csv" required><small>Kolom: tanggal, deskripsi, referensi, debit, kredit, saldo</small><br><button class="btn btn-outline-primary mt-2">Import & Auto-match</button></form></div></div></div><div class="col-lg-6"><div class="card"><div class="card-header">Rekonsiliasi bank/kas</div><div class="card-body"><form method="post" action="<?=base_url('cashbank/reconcile')?>"><select class="form-select mb-2" name="cash_account_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select><div class="row g-2"><div class="col-6"><input class="form-control" type="date" name="period_end" value="<?=date('Y-m-d')?>"></div><div class="col-6"><input class="form-control" type="number" name="statement_balance" placeholder="Saldo rekening koran"></div><div class="col-6"><input class="form-control" type="number" name="outstanding_receipts" value="0" placeholder="Outstanding receipt"></div><div class="col-6"><input class="form-control" type="number" name="outstanding_payments" value="0" placeholder="Outstanding payment"></div></div><input class="form-control my-2" name="notes" placeholder="Tindak lanjut selisih"><button class="btn btn-warning">Hitung & simpan</button></form></div></div></div></div>
<div class="card mt-3"><div class="card-header">Cash Ledger & Approval</div><div class="table-responsive"><table class="table table-sm mb-0"><tr><th>Tanggal</th><th>No.</th><th>Rekening</th><th>Jenis</th><th>Masuk</th><th>Keluar</th><th>Status</th><th></th></tr><?php foreach($transactions as$t):?><tr><td><?=$t->transaction_date?></td><td><?=html_escape($t->transaction_no)?></td><td><?=html_escape($t->account_name)?></td><td><?=$t->transaction_type?></td><td class="text-end"><?=$t->direction==='in'?number_format($t->amount,0,',','.'):'-'?></td><td class="text-end"><?=$t->direction==='out'?number_format($t->amount,0,',','.'):'-'?></td><td><?=$t->status?></td><td><?php if($t->status==='submitted'):?><form method="post" action="<?=base_url('cashbank/approve/'.$t->id)?>"><button class="btn btn-sm btn-success">Approve & Post</button></form><?php endif?></td></tr><?php endforeach?></table></div></div></div>
+1
View File
@@ -0,0 +1 @@
<div class="container-fluid py-3"><h4>Multi-company & Mata Uang</h4><div class="row g-3"><div class="col-lg-6"><div class="card"><div class="card-header">Perusahaan</div><div class="card-body"><form method="post" action="<?=base_url('companies/save')?>"><div class="row g-2"><div class="col-4"><input class="form-control" name="code" placeholder="Kode"></div><div class="col-8"><input class="form-control" name="name" placeholder="Nama"></div><div class="col-8"><input class="form-control" name="legal_name" placeholder="Nama legal"></div><div class="col-4"><select class="form-select" name="base_currency"><?php foreach($currencies as$c):?><option><?=$c->code?></option><?php endforeach?></select></div><div class="col-8"><input class="form-control" name="tax_number" placeholder="NPWP"></div><div class="col-4"><button class="btn btn-primary">Tambah</button></div></div></form><hr><?php foreach($companies as$c):?><a class="btn btn-outline-dark m-1" href="<?=base_url('companies/switch_company/'.$c->id)?>"><?=html_escape($c->company_code.' · '.$c->name)?></a><?php endforeach?></div></div></div><div class="col-lg-6"><div class="card"><div class="card-header">Kurs Harian</div><div class="card-body"><form method="post" action="<?=base_url('companies/rate')?>"><div class="row g-2"><div class="col-4"><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>"></div><div class="col-2"><select class="form-select" name="from_currency"><?php foreach($currencies as$c):?><option><?=$c->code?></option><?php endforeach?></select></div><div class="col-2"><select class="form-select" name="to_currency"><?php foreach($currencies as$c):?><option><?=$c->code?></option><?php endforeach?></select></div><div class="col-4"><input class="form-control" type="number" step=".00000001" name="rate" placeholder="Kurs"></div><div class="col-8"><input class="form-control" name="source" placeholder="Sumber kurs"></div><div class="col-4"><button class="btn btn-success">Simpan Kurs</button></div></div></form></div></div></div></div><div class="card mt-3"><div class="table-responsive"><table class="table table-sm"><tr><th>Tanggal</th><th>Perusahaan</th><th>Mata uang</th><th>Kurs</th><th>Sumber</th></tr><?php foreach($rates as$r):?><tr><td><?=$r->rate_date?></td><td><?=html_escape($r->company_name)?></td><td><?=$r->from_currency?>/<?=$r->to_currency?></td><td><?=number_format($r->rate,8,',','.')?></td><td><?=html_escape($r->source?:'-')?></td></tr><?php endforeach?></table></div></div></div>
+8 -5
View File
@@ -50,7 +50,8 @@
<input type="text" id="telp" class="form-control">
<label class="mt-2">Email</label>
<input type="email" id="email" class="form-control">
<input type="email" id="email" class="form-control">
<div class="row g-2 mt-1"><div class="col-md-6"><label class="form-label">Credit Limit</label><input type="number" id="credit_limit" class="form-control" min="0" step="0.01"></div><div class="col-md-6"><label class="form-label">Termin (hari)</label><input type="number" id="payment_term_days" class="form-control" min="0" value="30"></div></div><label class="form-label mt-2">NPWP/Tax Number</label><input type="text" id="tax_number" class="form-control"><div class="form-check mt-2"><input type="checkbox" id="is_credit_blocked" class="form-check-input"><label class="form-check-label" for="is_credit_blocked">Blokir transaksi kredit</label></div>
</div>
@@ -84,7 +85,8 @@ $(function(){
$('#nama').val('');
$('#alamat').val('');
$('#telp').val('');
$('#email').val('');
$('#email').val('');
$('#credit_limit').val(0); $('#payment_term_days').val(30); $('#tax_number').val(''); $('#is_credit_blocked').prop('checked',false);
}
// ADD
@@ -103,7 +105,8 @@ $(function(){
$('#nama').val(res.nama);
$('#alamat').val(res.alamat);
$('#telp').val(res.telp);
$('#email').val(res.email);
$('#email').val(res.email);
$('#credit_limit').val(res.credit_limit); $('#payment_term_days').val(res.payment_term_days); $('#tax_number').val(res.tax_number); $('#is_credit_blocked').prop('checked',res.is_credit_blocked==1);
action = 'edit';
$('#modalCustomer').modal('show');
@@ -118,7 +121,7 @@ $(function(){
nama: $('#nama').val(),
alamat: $('#alamat').val(),
telp: $('#telp').val(),
email: $('#email').val()
email: $('#email').val(), credit_limit: $('#credit_limit').val(), payment_term_days: $('#payment_term_days').val(), tax_number: $('#tax_number').val(), is_credit_blocked: $('#is_credit_blocked').is(':checked') ? 1 : 0
};
if(!data.nama){
@@ -162,4 +165,4 @@ $(function(){
});
});
</script>
</script>
@@ -0,0 +1,6 @@
<!doctype html><html><head><meta charset="utf-8"><style><?=file_get_contents(APPPATH.'views/fixed_assets/pdf_style.css')?></style></head><body>
<header><div><h1><?=html_escape($company->name??'Perusahaan')?></h1><p>KARTU DAN REGISTER ASET TETAP</p></div><img src="<?=$qr?>" class="qr"></header>
<section class="title"><h2><?=html_escape($asset->kode_asset.' - '.$asset->nama_asset)?></h2><p>Dicetak <?=date('d/m/Y H:i')?> · Status <?=html_escape(strtoupper($asset->lifecycle_status))?></p></section>
<table class="kv"><tr><th>Dokumen Perolehan</th><td><?=html_escape($asset->document_no)?></td><th>Sumber</th><td><?=html_escape($asset->source_type.' / '.($asset->source_reference_no?:'-'))?></td></tr><tr><th>Kategori</th><td><?=html_escape($asset->category_name?:'-')?></td><th>Lokasi</th><td><?=html_escape($asset->location_name?:'-')?></td></tr><tr><th>Penanggung Jawab</th><td><?=html_escape($asset->responsible_name?:'-')?></td><th>Barcode / Serial</th><td><?=html_escape(($asset->barcode?:'-').' / '.($asset->serial_number?:'-'))?></td></tr><tr><th>Tanggal Perolehan</th><td><?=html_escape($asset->tanggal_perolehan)?></td><th>Kapitalisasi</th><td><?=html_escape($asset->capitalization_date?:'-')?></td></tr><tr><th>Nilai Perolehan</th><td>Rp <?=number_format($asset->nilai_perolehan,2,',','.')?></td><th>Nilai Residu</th><td>Rp <?=number_format($asset->nilai_residu,2,',','.')?></td></tr><tr><th>Akumulasi Penyusutan</th><td>Rp <?=number_format($asset->akumulasi_penyusutan,2,',','.')?></td><th>Nilai Buku</th><td><b>Rp <?=number_format($asset->nilai_buku,2,',','.')?></b></td></tr><tr><th>Masa Manfaat</th><td><?=$asset->masa_manfaat?> bulan</td><th>Metode</th><td><?=html_escape($asset->depreciation_method)?></td></tr></table>
<h3>Riwayat Mutasi dan Jurnal</h3><table><thead><tr><th>Tanggal</th><th>Dokumen</th><th>Jenis</th><th>Nilai</th><th>Nilai Buku</th><th>Jurnal / Keterangan</th></tr></thead><tbody><?php foreach($asset->events as$e):?><tr><td><?=$e->event_date?></td><td><?=html_escape($e->event_no)?></td><td><?=html_escape($e->event_type)?></td><td class="num"><?=number_format($e->amount,2,',','.')?></td><td class="num"><?=number_format($e->book_value_after,2,',','.')?></td><td><?=html_escape(($e->journal_no?:'-').' · '.($e->notes?:'-'))?></td></tr><?php endforeach?><?php if(!$asset->events):?><tr><td colspan="6">Belum ada mutasi.</td></tr><?php endif?></tbody></table>
<footer>Dokumen ini dihasilkan dari subledger Aset Tetap. Verifikasi identitas melalui QR.</footer></body></html>
+27
View File
@@ -0,0 +1,27 @@
<?php $p=$asset_permissions;$today=date('Y-m-d'); function assetAccountOptions($accounts,$placeholder=true){$html=$placeholder?'<option value="">Pilih akun</option>':'';foreach($accounts as$a)$html.='<option value="'.(int)$a->id.'">'.html_escape($a->kode_akun.' - '.$a->nama_akun).'</option>';return$html;} ?>
<link rel="stylesheet" href="<?=base_url('assets/css/fixed-assets-professional.css').'?v='.filemtime(FCPATH.'assets/css/fixed-assets-professional.css')?>">
<section class="asset-page">
<div class="asset-hero"><div><span class="eyebrow">SUBLEDGER ASET TETAP</span><h2>Siklus Aset Tetap</h2><p>Perolehan, persetujuan, kapitalisasi, penyusutan, mutasi, opname, dan rekonsiliasi dalam satu alur.</p></div><?php if($p['create']):?><button class="btn btn-primary" id="btnNewAsset"><i class="bi bi-plus-lg"></i> Perolehan Aset</button><?php endif?></div>
<div class="asset-metrics"><article><span>Total register</span><strong><?=number_format((int)$summary->total_assets,0,',','.')?></strong><small><?=number_format((int)$summary->active_assets,0,',','.')?> aset aktif</small></article><article><span>Nilai perolehan</span><strong>Rp <?=number_format((float)$summary->acquisition_cost,2,',','.')?></strong><small>Subledger terkapitalisasi</small></article><article><span>Nilai buku</span><strong>Rp <?=number_format((float)$summary->book_value,2,',','.')?></strong><small>Akumulasi Rp <?=number_format((float)$summary->accumulated_depreciation,2,',','.')?></small></article><article><span>Perlu tindakan</span><strong><?=number_format((int)$summary->pending_workflow+(int)$summary->open_opnames,0,',','.')?></strong><small><?=$summary->pending_workflow?> approval · <?=$summary->open_opnames?> opname</small></article></div>
<nav class="asset-tabs" role="tablist"><button class="active" data-bs-toggle="tab" data-bs-target="#assetRegister"><i class="bi bi-journal-text"></i><span>Daftar Aset</span></button><button data-bs-toggle="tab" data-bs-target="#assetOpname"><i class="bi bi-qr-code-scan"></i><span>Asset Opname</span></button><button data-bs-toggle="tab" data-bs-target="#assetMaster"><i class="bi bi-sliders"></i><span>Master Data</span></button><button data-bs-toggle="tab" data-bs-target="#assetReports"><i class="bi bi-bar-chart-line"></i><span>Laporan & Rekonsiliasi</span></button></nav>
<div class="tab-content">
<div class="tab-pane fade show active" id="assetRegister"><div class="card asset-card"><div class="card-body"><div class="filter-grid mb-3"><input id="assetSearch" class="form-control" placeholder="Cari kode, nama, lokasi, barcode atau PIC"><select id="filterCategory" class="form-select"><option value="">Semua kategori</option><?php foreach($categories as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select><select id="filterWorkflow" class="form-select"><option value="">Semua workflow</option><option value="draft">Draft</option><option value="submitted">Menunggu approval</option><option value="approved">Siap kapitalisasi</option><option value="capitalized">Terkapitalisasi</option><option value="rejected">Ditolak</option></select><select id="filterLifecycle" class="form-select"><option value="">Semua kondisi</option><option value="active">Aktif</option><option value="fully_depreciated">Tersusut penuh</option><option value="disposed">Dilepas</option><option value="lost">Hilang</option><option value="damaged">Rusak</option></select></div><div class="table-responsive"><table id="assetTable" class="table align-middle w-100"><thead><tr><th>Aset</th><th>Kategori/Lokasi</th><th>Sumber</th><th>Status</th><th class="text-end">Perolehan</th><th class="text-end">Nilai Buku</th><th>Aksi</th></tr></thead></table></div></div></div></div>
<div class="tab-pane fade" id="assetOpname"><div class="section-heading"><div><h3>Asset Opname</h3><p>Hitung fisik per lokasi dengan bukti, approval, dan audit trail.</p></div><?php if($p['create']):?><button class="btn btn-primary" id="btnNewOpname"><i class="bi bi-plus-lg"></i> Buat Opname</button><?php endif?></div><div class="card asset-card"><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Dokumen</th><th>Tanggal/Lokasi</th><th>Status</th><th>Pembuat</th><th>Aksi</th></tr></thead><tbody><?php foreach($opnames as$o):?><tr><td><strong><?=html_escape($o->opname_no)?></strong></td><td><?=html_escape($o->opname_date)?><br><small><?=html_escape($o->location_name?:'-')?></small></td><td><span class="status-pill status-<?=html_escape($o->status)?>"><?=html_escape(strtoupper($o->status))?></span></td><td><?=html_escape($o->creator_name?:'-')?></td><td><button class="btn btn-sm btn-outline-primary btn-opname" data-id="<?=$o->id?>">Detail</button> <?php if($p['export']):?><button class="btn btn-sm btn-outline-danger btn-pdf" data-url="<?=base_url('fixedassets/opname_pdf/'.$o->id)?>">PDF</button><?php endif?></td></tr><?php endforeach?><tr class="<?=count($opnames)?'d-none':''?>"><td colspan="5" class="empty-state">Belum ada dokumen opname.</td></tr></tbody></table></div></div></div>
<div class="tab-pane fade" id="assetMaster"><div class="section-heading"><div><h3>Master Aset</h3><p>Kategori menentukan default masa manfaat dan akun. Lokasi dipakai oleh register, transfer, dan opname.</p></div></div><div class="row g-3"><div class="col-xl-7"><div class="card asset-card"><div class="card-header"><strong>Kategori Aset</strong><?php if($p['update']):?><button class="btn btn-sm btn-primary" data-master="category">Tambah</button><?php endif?></div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Kode/Nama</th><th>Masa Manfaat</th><th>Metode</th><th>Status</th><th></th></tr></thead><tbody><?php foreach($categories as$c):?><tr><td><strong><?=html_escape($c->code)?></strong><br><small><?=html_escape($c->name)?></small></td><td><?=$c->default_useful_life_months?> bulan</td><td><?=html_escape($c->depreciation_method)?></td><td><?=$c->is_active?'Aktif':'Nonaktif'?></td><td><?php if($p['update']):?><button class="btn btn-sm btn-outline-primary edit-category" data-row='<?=html_escape(json_encode($c),true)?>'>Edit</button><?php endif?></td></tr><?php endforeach?></tbody></table></div></div></div><div class="col-xl-5"><div class="card asset-card"><div class="card-header"><strong>Lokasi Aset</strong><?php if($p['update']):?><button class="btn btn-sm btn-primary" data-master="location">Tambah</button><?php endif?></div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Kode/Nama</th><th>Status</th><th></th></tr></thead><tbody><?php foreach($locations as$l):?><tr><td><strong><?=html_escape($l->code?:'-')?></strong><br><small><?=html_escape($l->nama)?></small></td><td><?=$l->is_active?'Aktif':'Nonaktif'?></td><td><?php if($p['update']):?><button class="btn btn-sm btn-outline-primary edit-location" data-row='<?=html_escape(json_encode($l),true)?>'>Edit</button><?php endif?></td></tr><?php endforeach?></tbody></table></div></div></div></div></div>
<div class="tab-pane fade" id="assetReports"><div class="section-heading"><div><h3>Laporan & Rekonsiliasi</h3><p>Laporan berasal dari subledger aset dan dapat ditelusuri ke jurnal.</p></div></div><div class="card asset-card"><div class="card-body"><div class="report-controls"><select id="reportType" class="form-select"><option value="register">Daftar Aset</option><option value="depreciation">Jadwal Penyusutan</option><option value="maintenance">Riwayat Maintenance</option><option value="events">Mutasi & Audit Aset</option><option value="reconciliation">Rekonsiliasi dengan Buku Besar</option></select><input type="date" id="reportFrom" class="form-control" value="<?=date('Y-01-01')?>"><input type="date" id="reportTo" class="form-control" value="<?=$today?>"><button class="btn btn-primary" id="btnRunReport">Tampilkan</button><?php if($p['export']):?><button class="btn btn-outline-danger" id="btnReportPdf">PDF</button><button class="btn btn-outline-success" id="btnReportExcel">Excel</button><?php endif?></div><div id="reportResult" class="report-result"><div class="empty-state">Pilih jenis laporan lalu klik Tampilkan.</div></div></div></div></div>
</div>
</section>
<div class="modal fade" id="assetFormModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><form id="assetForm" data-ajax="true"><div class="modal-header"><div><small class="text-muted">DOKUMEN PEROLEHAN</small><h5 class="modal-title">Perolehan Aset Tetap</h5></div><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button></div><div class="modal-body"><input type="hidden" name="id"><input type="hidden" name="idempotency_key"><input type="hidden" name="item_id"><input type="hidden" name="barcode_id"><input type="hidden" name="warehouse_id"><input type="hidden" name="bin_id">
<div class="source-choice"><label><input type="radio" name="source_type" value="manual" checked><span><i class="bi bi-pencil-square"></i><b>Input Langsung</b><small>Pembelian/manual yang belum berasal dari stok.</small></span></label><label><input type="radio" name="source_type" value="warehouse"><span><i class="bi bi-box-seam"></i><b>Dari Gudang</b><small>Ambil barang aktif beserta barcode dan nilai ledger.</small></span></label></div>
<div class="form-section warehouse-source d-none"><h6>Barang Gudang</h6><select id="inventoryLookup" class="form-select"></select><div id="inventoryInfo" class="source-info mt-2">Cari kode barang, nama, barcode, atau serial number.</div></div>
<div class="form-section"><h6>Identitas & Penempatan</h6><div class="row g-3"><div class="col-md-6"><label class="form-label">Nama aset</label><input class="form-control" name="nama_asset" required></div><div class="col-md-3"><label class="form-label">Tanggal perolehan</label><input type="date" class="form-control" name="tanggal_perolehan" value="<?=$today?>" required></div><div class="col-md-3"><label class="form-label">Tanggal kapitalisasi</label><input type="date" class="form-control" name="capitalization_date" value="<?=$today?>"></div><div class="col-md-4"><label class="form-label">Kategori</label><select class="form-select" name="category_id" required><option value="">Pilih kategori</option><?php foreach($categories as$c)if($c->is_active):?><option value="<?=$c->id?>" data-life="<?=$c->default_useful_life_months?>" data-method="<?=$c->depreciation_method?>" data-asset="<?=$c->asset_account_id?>" data-accum="<?=$c->accumulated_depreciation_account_id?>" data-expense="<?=$c->depreciation_expense_account_id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endif?></select></div><div class="col-md-4"><label class="form-label">Lokasi</label><select class="form-select" name="lokasi_asset_id" required><option value="">Pilih lokasi</option><?php foreach($locations as$l)if($l->is_active):?><option value="<?=$l->id?>"><?=html_escape($l->code.' - '.$l->nama)?></option><?php endif?></select></div><div class="col-md-4"><label class="form-label">Penanggung jawab</label><select id="employeeLookup" class="form-select" name="responsible_employee_id"></select></div></div></div>
<div class="form-section"><h6>Nilai & Penyusutan</h6><div class="row g-3"><div class="col-md-3 warehouse-source d-none"><label class="form-label">Qty dikapitalisasi</label><input class="form-control money-input" name="qty" value="1" inputmode="decimal"></div><div class="col-md-3"><label class="form-label" id="acquisitionValueLabel">Nilai perolehan</label><input class="form-control money-input" name="nilai_perolehan" inputmode="decimal"></div><div class="col-md-3"><label class="form-label">Nilai residu</label><input class="form-control money-input" name="nilai_residu" value="0" inputmode="decimal"></div><div class="col-md-3"><label class="form-label">Masa manfaat (bulan)</label><input type="number" min="1" class="form-control" name="masa_manfaat" required></div><div class="col-md-3"><label class="form-label">Metode</label><select class="form-select" name="depreciation_method"><option value="straight_line">Garis lurus</option><option value="declining_balance">Saldo menurun</option></select></div></div></div>
<div class="form-section"><h6>Akun & Sumber Jurnal</h6><div class="row g-3"><div class="col-md-4"><label class="form-label">Akun aset</label><select class="form-select" name="asset_account_id" required><?=assetAccountOptions($accounts)?></select></div><div class="col-md-4"><label class="form-label">Akumulasi penyusutan</label><select class="form-select" name="accumulated_depreciation_account_id" required><?=assetAccountOptions($accounts)?></select></div><div class="col-md-4"><label class="form-label">Beban penyusutan</label><select class="form-select" name="depreciation_expense_account_id" required><?=assetAccountOptions($accounts)?></select></div><div class="col-md-4 manual-source"><label class="form-label">Perlakuan jurnal</label><select class="form-select" name="source_journal_mode"><option value="post_journal">Buat jurnal kapitalisasi</option><option value="linked_journal">Hubungkan jurnal yang sudah posted</option><option value="no_journal">Saldo awal/tanpa jurnal baru</option></select></div><div class="col-md-4 manual-source credit-account"><label class="form-label">Akun lawan</label><select class="form-select" name="account_kredit_id"><?=assetAccountOptions($accounts)?></select></div><div class="col-md-4 manual-source linked-journal d-none"><label class="form-label">Jurnal posted</label><select id="journalLookup" class="form-select" name="source_journal_id"></select></div><div class="col-md-4"><label class="form-label">Referensi sumber</label><input class="form-control" name="source_reference_no" placeholder="Invoice/BAST/dokumen lain"></div></div></div>
<div class="form-section"><h6>Dokumen Pendukung <span class="text-danger">*</span></h6><p class="form-hint">Minimal satu gambar atau PDF wajib sebelum diajukan.</p><div id="assetFiles" class="file-grid"><label class="file-add"><input type="file" id="assetFileInput" accept="image/*,.pdf" multiple hidden><i class="bi bi-plus-lg"></i><span>Tambah</span></label></div></div><div class="form-section"><label class="form-label">Keterangan lengkap</label><textarea class="form-control" name="keterangan" rows="3" placeholder="Jelaskan asal, kondisi, nomor dokumen, dan alasan kapitalisasi"></textarea></div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" type="submit">Simpan Draft</button></div></form></div></div></div>
<div class="modal fade" id="assetDetailModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">ASSET REGISTER</small><h5 class="modal-title">Detail Aset</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="assetDetailBody"></div><div class="modal-footer" id="assetDetailActions"></div></div></div></div>
<div class="modal fade" id="pdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Dokumen PDF</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF"></iframe></div></div></div></div>
<div class="modal fade" id="masterModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content"><form id="masterForm"><div class="modal-header"><h5 class="modal-title">Master Aset</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="masterFields"></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan</button></div></form></div></div></div>
<div class="modal fade" id="opnameModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Asset Opname</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="opnameBody"></div><div class="modal-footer" id="opnameActions"></div></div></div></div>
<script>window.FIXED_ASSET_PAGE=<?=json_encode(array('base'=>base_url(),'permissions'=>$p,'locations'=>$locations,'accounts'=>$accounts),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/fixed-assets.js').'?v='.(file_exists(FCPATH.'assets/js/fixed-assets.js')?filemtime(FCPATH.'assets/js/fixed-assets.js'):time())?>"></script>
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><style><?=file_get_contents(APPPATH.'views/fixed_assets/pdf_style.css')?></style></head><body><header><div><h1><?=html_escape($company->name??'Perusahaan')?></h1><p>BERITA ACARA ASSET OPNAME</p></div></header><section class="title"><h2><?=html_escape($opname->opname_no)?></h2><p>Tanggal <?=html_escape($opname->opname_date)?> · Lokasi <?=html_escape($opname->location_name)?> · Status <?=html_escape(strtoupper($opname->status))?></p></section><table><thead><tr><th>Kode</th><th>Nama Aset</th><th>Lokasi Catatan</th><th>Lokasi Fisik</th><th>Ditemukan</th><th>Kondisi</th><th>Catatan</th></tr></thead><tbody><?php foreach($opname->lines as$l):?><tr><td><?=html_escape($l->kode_asset)?></td><td><?=html_escape($l->nama_asset)?></td><td><?=html_escape($l->expected_location?:'-')?></td><td><?=html_escape($l->actual_location?:'-')?></td><td><?=$l->is_found?'Ya':'Tidak'?></td><td><?=html_escape($l->actual_status)?></td><td><?=html_escape($l->notes?:'-')?></td></tr><?php endforeach?></tbody></table><div class="signatures"><div>Dibuat oleh<br><br><br><b><?=html_escape($opname->creator_name?:'-')?></b></div><div>Disetujui oleh<br><br><br><b><?=html_escape($opname->approver_name?:'-')?></b></div><div>Diposting oleh<br><br><br><b><?=html_escape($opname->poster_name?:'-')?></b></div></div><footer>Selisih kondisi tidak otomatis menjurnal. Pelepasan, hilang, atau rusak diproses melalui mutasi aset resmi.</footer></body></html>
@@ -0,0 +1 @@
@page{margin:24px}body{font-family:DejaVu Sans,Arial,sans-serif;color:#1d2738;font-size:9px}header{display:flex;border-bottom:2px solid #1769e0;padding-bottom:10px;margin-bottom:14px}header h1{margin:0;font-size:17px}header p{margin:3px 0;color:#607087}.qr{width:75px;margin-left:auto}.title{margin-bottom:13px}.title h2{font-size:15px;margin:0 0 3px}.title p{margin:0;color:#607087}h3{font-size:11px;margin:16px 0 6px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #d6dde8;padding:6px;vertical-align:top}thead th{background:#edf3fa;text-transform:uppercase;font-size:7px;letter-spacing:.04em}.kv th{width:16%;background:#f5f7fa;text-align:left}.num{text-align:right}.signatures{display:flex;justify-content:space-between;margin-top:28px;text-align:center}.signatures div{width:30%}footer{position:fixed;bottom:-13px;border-top:1px solid #ccd5e1;width:100%;padding-top:5px;color:#6a7687;font-size:7px}
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:Arial;margin:20px}.label{width:320px;border:1px solid #111;padding:12px;text-align:center}.label img{width:150px}.label h2{font-size:18px;margin:7px}.label p{margin:4px;font-size:12px}@media print{button{display:none}}</style></head><body><div class="label"><img src="<?=$qr?>"><h2><?=html_escape($asset->kode_asset)?></h2><p><?=html_escape($asset->nama_asset)?></p><p><?=html_escape($asset->location_name?:'-')?> · <?=html_escape(strtoupper($asset->lifecycle_status))?></p></div><br><button onclick="print()">Cetak Label QR</button></body></html>
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><style><?=file_get_contents(APPPATH.'views/fixed_assets/pdf_style.css')?></style></head><body><header><div><h1><?=html_escape($company->name??'Perusahaan')?></h1><p>LAPORAN ASET TETAP</p></div></header><section class="title"><h2><?=html_escape($definition['title'])?></h2><p>Periode <?=html_escape($filters['date_from'])?> s.d. <?=html_escape($filters['date_to'])?> · Dicetak <?=date('d/m/Y H:i')?></p></section><table><thead><tr><?php foreach($definition['columns']as$label):?><th><?=html_escape($label)?></th><?php endforeach?></tr></thead><tbody><?php foreach($rows as$row):?><tr><?php foreach($definition['columns']as$key=>$label):?><td class="<?=preg_match('/value|nilai|cost|amount|selisih|buku|perolehan|akumulasi/i',$key)?'num':''?>"><?php $v=$row->$key??null; echo html_escape(is_numeric($v)&&preg_match('/value|nilai|cost|amount|selisih|buku|perolehan|akumulasi/i',$key)?number_format($v,2,',','.'):$v);?></td><?php endforeach?></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="<?=count($definition['columns'])?>">Tidak ada data.</td></tr><?php endif?></tbody></table><footer>Laporan dihasilkan dari subledger Aset Tetap dan dibatasi pada perusahaan aktif.</footer></body></html>
+3
View File
@@ -0,0 +1,3 @@
<div class="container-fluid py-3"><h4>HR & Payroll Control</h4><p class="text-muted">Lock absensi → Draft → Review HR → Approval → Final → Jurnal → Pembayaran.</p><?php foreach(array('success','error')as$f)if($this->session->flashdata($f)):?><div class="alert alert-<?=$f==='success'?'success':'danger'?>"><?=html_escape($this->session->flashdata($f))?></div><?php endif?><div class="row g-3"><div class="col-lg-4"><div class="card h-100"><div class="card-header">1. Lock Absensi Bulanan</div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/lock_attendance')?>"><input class="form-control mb-2" type="month" name="period" value="<?=date('Y-m')?>" required><input class="form-control mb-2" name="notes" required placeholder="Catatan validasi absensi"><button class="btn btn-warning">Validasi & Lock</button></form></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-header">2. Hitung Draft Payroll</div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/calculate')?>"><input class="form-control mb-2" type="month" name="month" value="<?=date('Y-m')?>" required><p class="small">Menggunakan snapshot absensi terkunci dan formula komponen aktif.</p><button class="btn btn-primary">Generate/Hitung Ulang Draft</button></form></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-header">Formula Configurable</div><div class="card-body"><small>Variabel: basic_salary, work_days, present_days, unpaid_days, overtime_hours, overtime_rate, daily_rate.</small><form class="mt-2" method="post" action="<?=base_url('hrpayroll/component')?>"><div class="d-flex gap-1"><input class="form-control" name="code" placeholder="Kode" required><input class="form-control" name="name" placeholder="Nama" required></div><div class="d-flex gap-1 my-1"><select class="form-select" name="component_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option><option value="employer_cost">Beban perusahaan</option></select><select class="form-select" name="category"><option>allowance</option><option>overtime</option><option>unpaid_leave</option><option>loan</option><option>bpjs</option><option>tax</option><option>thr</option><option>bonus</option><option>other</option></select></div><input class="form-control mb-1" name="formula_expression" placeholder="Contoh: overtime_hours * overtime_rate"><input class="form-control mb-1" type="number" name="fixed_amount" placeholder="Atau nominal tetap"><label class="me-2"><input type="checkbox" name="taxable" value="1"> Taxable</label><label><input type="checkbox" name="prorated" value="1"> Prorata</label><br><button class="btn btn-sm btn-outline-primary mt-2">Simpan Komponen</button></form></div></div></div></div>
<div class="card mt-3"><div class="card-header">Payroll Register</div><div class="table-responsive"><table class="table mb-0"><tr><th>Periode</th><th>Karyawan</th><th>Bruto</th><th>Potongan</th><th>Neto</th><th>Status</th><th>Workflow</th></tr><?php foreach($periods as$p):?><tr><td><?=html_escape($p->period_name)?></td><td><?=$p->employee_count?></td><td class="text-end"><?=number_format($p->gross,0,',','.')?></td><td class="text-end"><?=number_format($p->deductions,0,',','.')?></td><td class="text-end"><?=number_format($p->net,0,',','.')?></td><td><span class="badge bg-secondary"><?=$p->workflow_status?></span></td><td><div class="d-flex gap-1"><?php if($p->workflow_status==='draft'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/review')?>"><button class="btn btn-sm btn-info">Review HR</button></form><?php elseif($p->workflow_status==='reviewed'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/approve')?>"><button class="btn btn-sm btn-warning">Approve</button></form><?php elseif($p->workflow_status==='approved'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/finalize')?>"><button class="btn btn-sm btn-danger">Final & Lock</button></form><?php elseif($p->workflow_status==='final'):?><form class="d-flex" method="post" action="<?=base_url('hrpayroll/post/'.$p->id)?>"><input class="form-control form-control-sm" type="date" name="date" value="<?=$p->end_date?>"><button class="btn btn-sm btn-success">Post Jurnal</button></form><?php elseif($p->workflow_status==='posted'):?><form class="d-flex" method="post" action="<?=base_url('hrpayroll/pay/'.$p->id)?>"><input class="form-control form-control-sm" type="date" name="date" value="<?=date('Y-m-d')?>"><select class="form-select form-select-sm" name="cash_account_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select><button class="btn btn-sm btn-primary">Bayar</button></form><?php endif?></div></td></tr><?php endforeach?></table></div></div>
<div class="card mt-3"><div class="card-header">Komponen Aktif</div><div class="table-responsive"><table class="table table-sm"><tr><th>Kode</th><th>Nama</th><th>Jenis</th><th>Kategori</th><th>Formula/Nominal</th><th>Pajak</th></tr><?php foreach($components as$c):?><tr><td><?=$c->code?></td><td><?=html_escape($c->name)?></td><td><?=$c->component_type?></td><td><?=$c->category?></td><td><?=html_escape($c->formula_expression?:number_format($c->fixed_amount,0,',','.'))?></td><td><?=$c->taxable?'Ya':'Tidak'?></td></tr><?php endforeach?></table></div></div></div>
@@ -0,0 +1,37 @@
<div class="container-fluid py-3">
<div class="mb-3"><h4 class="mb-1">Master Departemen & Posisi</h4><p class="text-muted mb-0">Satu sumber data untuk karyawan, Purchase Request, payroll, jadwal, dan modul lainnya.</p></div>
<div class="row g-3">
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><b>Departemen</b></div><div class="card-body">
<form id="departmentForm" class="row g-2 mb-3"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama departemen" required></div><div class="col-md-5"><input class="form-control" name="description" placeholder="Keterangan"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button class="btn btn-primary">Simpan</button></div></form>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Kode</th><th>Departemen</th><th>Status</th><th></th></tr></thead><tbody id="departmentRows"></tbody></table></div>
</div></div></div>
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><b>Posisi / Jabatan</b></div><div class="card-body">
<form id="positionForm" class="row g-2 mb-3"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama posisi" required></div><div class="col-md-5"><select class="form-select" name="department_id" id="positionDepartment"><option value="">Semua departemen</option></select></div><div class="col-12"><input class="form-control" name="description" placeholder="Keterangan/tanggung jawab"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button class="btn btn-primary">Simpan</button></div></form>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Kode</th><th>Posisi</th><th>Departemen</th><th>Status</th><th></th></tr></thead><tbody id="positionRows"></tbody></table></div>
</div></div></div>
</div>
</div>
<script>
$(function(){
const base='<?=base_url('organizationmaster/')?>';
const departmentForm=document.getElementById('departmentForm'),positionForm=document.getElementById('positionForm');
const departmentRows=document.getElementById('departmentRows'),positionRows=document.getElementById('positionRows'),positionDepartment=document.getElementById('positionDepartment');
const esc=v=>$('<div>').text(v??'').html();
let state={departments:[],positions:[]};
async function load(){
try{
const response=await fetch(base+'data',{headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();
if(!response.ok||!json.status)throw new Error(json.message||'Gagal mengambil master organisasi.');
state={departments:Array.isArray(json.departments)?json.departments:[],positions:Array.isArray(json.positions)?json.positions:[]};
departmentRows.innerHTML=state.departments.map(x=>`<tr><td>${esc(x.code)}</td><td>${esc(x.department_name)}<small class="d-block text-muted">${esc(x.description||'')}</small></td><td>${x.is_active==1?'Aktif':'Nonaktif'}</td><td class="text-end"><button class="btn btn-sm btn-outline-primary edit-dep" data-id="${x.id}">Edit</button> <button class="btn btn-sm btn-outline-danger del-dep" data-id="${x.id}">Hapus</button></td></tr>`).join('')||'<tr><td colspan="4" class="text-center text-muted">Belum ada departemen.</td></tr>';
positionRows.innerHTML=state.positions.map(x=>`<tr><td>${esc(x.code)}</td><td>${esc(x.position_name)}</td><td>${esc(x.department_name||'Semua')}</td><td>${x.is_active==1?'Aktif':'Nonaktif'}</td><td class="text-end"><button class="btn btn-sm btn-outline-primary edit-pos" data-id="${x.id}">Edit</button> <button class="btn btn-sm btn-outline-danger del-pos" data-id="${x.id}">Hapus</button></td></tr>`).join('')||'<tr><td colspan="5" class="text-center text-muted">Belum ada posisi.</td></tr>';
positionDepartment.innerHTML='<option value="">Semua departemen</option>'+state.departments.filter(x=>x.is_active==1).map(x=>`<option value="${x.id}">${esc(x.code+' - '+x.department_name)}</option>`).join('');
}catch(error){departmentRows.innerHTML=positionRows.innerHTML=`<tr><td colspan="5" class="text-danger">${esc(error.message)}</td></tr>`;}
}
async function save(form,path){const button=form.querySelector('button[type="submit"]');button.disabled=true;try{const response=await fetch(base+path,{method:'POST',body:new FormData(form),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();if(!response.ok||!json.status)throw new Error(json.message||'Gagal menyimpan');form.reset();form.elements.id.value='';form.elements.is_active.checked=true;await load();}catch(error){alert(error.message)}finally{button.disabled=false}}
departmentForm.addEventListener('submit',event=>{event.preventDefault();save(departmentForm,'save_department')});
positionForm.addEventListener('submit',event=>{event.preventDefault();save(positionForm,'save_position')});
$(document).on('click','.btn-reset',function(){const form=this.closest('form');form.reset();form.elements.id.value='';form.elements.is_active.checked=true}).on('click','.edit-dep',function(){const x=state.departments.find(v=>v.id==this.dataset.id),f=departmentForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.department_name;f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.edit-pos',function(){const x=state.positions.find(v=>v.id==this.dataset.id),f=positionForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.position_name;f.elements.department_id.value=x.department_id||'';f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.del-dep,.del-pos',async function(){if(!confirm('Hapus data ini? Data yang sudah digunakan akan dinonaktifkan.'))return;const path=this.classList.contains('del-dep')?'delete_department/':'delete_position/',response=await fetch(base+path+this.dataset.id,{method:'POST',body:new FormData(departmentForm),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();alert(json.message);load()});
load();
});
</script>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Slip Gaji</title><style>body{font:14px Arial;max-width:760px;margin:30px auto}table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #ddd}.r{text-align:right}@media print{button{display:none}}</style></head><body><button onclick="print()">Cetak</button><h2>SLIP GAJI</h2><p><b><?=html_escape($payroll->full_name)?></b> — <?=html_escape($payroll->employee_code)?><br>Periode <?=html_escape($payroll->period_name)?></p><table><tr><th>Komponen</th><th>Jenis</th><th class="r">Nilai</th></tr><?php foreach($lines as$l):?><tr><td><?=html_escape($l->component_name)?></td><td><?=$l->component_type?></td><td class="r"><?=number_format($l->amount,0,',','.')?></td></tr><?php endforeach?><tr><th colspan="2">Gaji Neto</th><th class="r"><?=number_format($payroll->total_salary,0,',','.')?></th></tr></table><small>Dokumen rahasia. Status payroll: <?=$payroll->workflow_status?></small></body></html>
@@ -0,0 +1,15 @@
<link rel="stylesheet" href="<?=base_url('assets/css/inventory-professional.css')?>?v=<?=filemtime(FCPATH.'assets/css/inventory-professional.css')?>">
<section class="inventory-hero mb-3">
<div><span class="inventory-kicker">PERSEDIAAN TERINTEGRASI</span><h2 class="mb-1"><?=html_escape($inventory_title??'Persediaan')?></h2><p class="mb-0"><?=html_escape($inventory_subtitle??'Kontrol barang, gudang, dan nilai persediaan dalam satu alur.')?></p></div>
<a class="btn btn-light inventory-scan-link" href="<?=base_url('inventoryprofessional')?>#scanner"><i class="bi bi-qr-code-scan"></i> Scan Barcode</a>
</section>
<nav class="inventory-module-nav mb-4" aria-label="Menu Persediaan">
<a class="<?=($active_menu??'')==='inventory_overview'?'active':''?>" href="<?=base_url('inventoryprofessional')?>"><i class="bi bi-grid-1x2"></i><span>Ringkasan</span></a>
<a class="<?=($active_menu??'')==='draft_items'?'active':''?>" href="<?=base_url('items/draft_items')?>"><i class="bi bi-hourglass-split"></i><span>Barang Pending</span></a>
<a class="<?=($active_menu??'')==='items'?'active':''?>" href="<?=base_url('items')?>"><i class="bi bi-box-seam"></i><span>Daftar Barang</span></a>
<a class="<?=($active_menu??'')==='inventory_operations'?'active':''?>" href="<?=base_url('inventoryprofessional/operations')?>"><i class="bi bi-arrow-left-right"></i><span>Operasional Stok</span></a>
<a class="<?=($active_menu??'')==='inventory_locations'?'active':''?>" href="<?=base_url('inventoryprofessional/locations')?>"><i class="bi bi-geo-alt"></i><span>Gudang & Rak</span></a>
<a class="<?=($active_menu??'')==='kode_barang'?'active':''?>" href="<?=base_url('kodebarang')?>"><i class="bi bi-tags"></i><span>Jenis & Kode Barang</span></a>
<?php if(is_master_admin_user()||check_permission('technician_equipment','can_view')||check_permission('items','can_view')):?><a class="<?=($active_menu??'')==='peralatan_teknisi'?'active':''?>" href="<?=base_url('teknisi')?>"><i class="bi bi-tools"></i><span>Peralatan Teknisi</span></a><?php endif?>
<a class="<?=($active_menu??'')==='inventory_reports'?'active':''?>" href="<?=base_url('inventoryprofessional/reports')?>"><i class="bi bi-clipboard-data"></i><span>Laporan</span></a>
</nav>
@@ -0,0 +1,22 @@
<?php $inventory_title='Ringkasan Persediaan';$inventory_subtitle='Pekerjaan gudang, nilai stok, dan pengecualian yang perlu ditangani.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<div class="row g-3 inventory-metrics">
<?php foreach(array(
array('Barang Aktif',$summary->active_items,'bi-box-seam','primary'),array('Perlu Diaktifkan',$summary->pending_items,'bi-hourglass-split','warning'),
array('Nilai Persediaan','Rp '.number_format($summary->inventory_value,2,',','.'),'bi-cash-stack','success'),array('Perlu Reorder',$summary->reorder_items,'bi-exclamation-triangle','danger'),
array('Dokumen Terbuka',$summary->open_documents,'bi-file-earmark-text','info'),array('Reservasi Aktif',$summary->open_reservations,'bi-bookmark-check','secondary')
)as$m):?><div class="col-6 col-xl-2"><div class="metric-card"><span class="metric-icon text-<?=$m[3]?>"><i class="bi <?=$m[2]?>"></i></span><strong><?=$m[1]?></strong><small><?=$m[0]?></small></div></div><?php endforeach?>
</div>
<div class="row g-3 mt-1">
<div class="col-xl-7"><section class="inventory-card h-100"><div class="inventory-card-head"><div><h5>Pekerjaan Gudang</h5><p>Barang yang sudah diterima tetapi belum siap digunakan atau dijual.</p></div><a href="<?=base_url('items/draft_items')?>" class="btn btn-sm btn-outline-primary">Buka Barang Pending</a></div>
<?php if(!$action_items):?><div class="inventory-empty"><i class="bi bi-check-circle"></i><strong>Tidak ada barang pending</strong><span>Semua penerimaan sudah ditangani.</span></div><?php else:?><div class="inventory-list"><?php foreach($action_items as$i):?><a href="<?=base_url('items/draft_items?focus='.$i->id)?>"><span class="item-avatar"><i class="bi bi-box"></i></span><span class="flex-grow-1"><strong><?=html_escape($i->kode_detail.' - '.$i->nama_barang)?></strong><small><?=number_format($i->ready_barcodes,0,',','.')?> siap dari <?=number_format($i->pending_barcodes,0,',','.')?> barcode pending</small></span><i class="bi bi-chevron-right"></i></a><?php endforeach?></div><?php endif?>
</section></div>
<div class="col-xl-5"><section class="inventory-card h-100" id="scanner"><div class="inventory-card-head"><div><h5>Scan Barcode / Serial</h5><p>Lihat posisi, status, saldo, dan riwayat unit.</p></div></div><div class="p-3"><div class="input-group input-group-lg"><span class="input-group-text"><i class="bi bi-upc-scan"></i></span><input id="scanCode" class="form-control" placeholder="Scan atau ketik kode"><button id="scanButton" class="btn btn-primary">Periksa</button></div><div id="scanResult" class="scan-result mt-3"><div class="inventory-empty py-4"><i class="bi bi-qr-code-scan"></i><span>Hasil scan tampil di sini.</span></div></div></div></section></div>
</div>
<section class="inventory-card mt-3"><div class="inventory-card-head"><div><h5>Dokumen Mutasi Terbaru</h5><p>Jejak dokumen stok dari seluruh gudang.</p></div><a href="<?=base_url('inventoryprofessional/operations')?>" class="btn btn-sm btn-outline-primary">Lihat Semua</a></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Dokumen</th><th>Jenis</th><th>Tanggal</th><th>Lokasi</th><th>Status</th><th>Pembuat</th></tr></thead><tbody><?php foreach($documents as$d):?><tr><td><a href="<?=base_url('inventoryprofessional/operations?focus='.$d->id)?>" class="fw-semibold"><?=html_escape($d->document_no)?></a></td><td><?=html_escape(ucwords(str_replace('_',' ',$d->document_type)))?></td><td><?=date('d/m/Y',strtotime($d->document_date))?></td><td><?=html_escape(($d->from_warehouse?:'-').' → '.($d->to_warehouse?:'-'))?></td><td><span class="status-chip status-<?=$d->status?>"><?=html_escape(strtoupper($d->status))?></span></td><td><?=html_escape($d->creator_name?:'-')?></td></tr><?php endforeach?></tbody></table></div></section>
<script>
$(function(){
function esc(v){return $('<div>').text(v==null?'-':v).html()}
$('#scanButton').on('click',function(){var code=$('#scanCode').val().trim();if(!code)return AppAlert.toast('Barcode atau serial wajib diisi.','warning');var b=$(this).prop('disabled',true);$('#scanResult').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memeriksa barcode…</div>');$.ajax({url:'<?=base_url('inventoryprofessional/scan')?>',data:{code:code},dataType:'json'}).done(function(r){if(!r||!r.status||!r.item){var invalidMessage=r&&r.message?r.message:'Respons scan tidak lengkap.';AppAlert.toast(invalidMessage,'error');$('#scanResult').html('<div class="alert alert-danger mb-0">'+esc(invalidMessage)+'</div>');return;}var x=r.item,available=Math.max(0,Number(x.qty_sisa||0)-Number(x.reserved_qty||0)-Number(x.return_reserved_qty||0));var history=(Array.isArray(x.history)?x.history:[]).map(function(h){return '<li><strong>'+esc(h.movement_type)+'</strong><span>'+esc(h.notes)+' · '+esc(h.created_at)+'</span></li>'}).join('');$('#scanResult').html('<div class="scan-product"><span class="status-chip status-'+esc(x.status)+'">'+esc(x.status)+'</span><h5>'+esc((x.kode_detail||'')+' - '+(x.nama_barang||''))+'</h5><div class="scan-grid"><span>Barcode<strong>'+esc(x.barcode)+'</strong></span><span>Serial<strong>'+esc(x.serial_number)+'</strong></span><span>Gudang / Rak<strong>'+esc(x.warehouse_name)+' / '+esc(x.bin_code)+'</strong></span><span>Qty Tersedia<strong>'+available.toLocaleString('id-ID')+' '+esc(x.unit)+'</strong></span><span>Batch<strong>'+esc(x.batch_no)+'</strong></span></div><ul class="scan-history">'+(history||'<li>Belum ada riwayat perpindahan.</li>')+'</ul></div>')}).fail(function(xhr){var message=xhr.responseJSON&&xhr.responseJSON.message?xhr.responseJSON.message:'Barcode atau serial tidak ditemukan.';AppAlert.toast(message,'error');$('#scanResult').html('<div class="alert alert-danger mb-0">'+esc(message)+'</div>')}).always(function(){b.prop('disabled',false)})});
$('#scanCode').on('keydown',function(e){if(e.key==='Enter')$('#scanButton').click()});
});
</script>
@@ -0,0 +1,85 @@
<?php
$formatQty = static function ($value) {
$number = (float) $value;
$decimals = abs($number - round($number)) < 0.0001 ? 0 : 4;
return number_format($number, $decimals, ',', '.');
};
$companyLabel = trim(($document->company_code ? $document->company_code.' - ' : '').($document->company_name ?: 'Perusahaan'));
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body{font-family:DejaVu Sans,sans-serif;font-size:10px;color:#263044}
h1{font-size:18px;margin:2px 0 0}.company{font-size:11px;font-weight:700}.subtitle{color:#657086}
p{margin:3px 0}.head{border-bottom:2px solid #ee7800;padding-bottom:12px;margin-bottom:14px}
.meta{width:100%;margin-bottom:14px}.meta td{width:33%;vertical-align:top;padding:4px}
.label{font-size:8px;color:#6e7888;text-transform:uppercase}.value{font-weight:bold;margin-top:2px}
table.lines{width:100%;border-collapse:collapse}.lines th{background:#edf2f8;text-align:left;font-size:8px}
.lines th,.lines td{border:1px solid #dfe5ed;padding:6px;vertical-align:top}.right{text-align:right}
.attachments{margin-top:12px;padding:8px;border:1px solid #dfe5ed}.attachments ul{margin:5px 0 0;padding-left:18px}
.sign{margin-top:35px;width:100%;text-align:center}.sign td{width:25%;height:70px;vertical-align:top}
.foot{position:fixed;bottom:0;font-size:8px;color:#788292}
</style>
</head>
<body>
<div class="head">
<div class="company"><?=html_escape($companyLabel)?></div>
<h1>Dokumen Mutasi Persediaan</h1>
<p class="subtitle"><?=html_escape($document->document_no)?> &middot; <?=html_escape(strtoupper(str_replace('_', ' ', $document->document_type)))?></p>
</div>
<table class="meta">
<tr>
<td><div class="label">Tanggal</div><div class="value"><?=date('d/m/Y', strtotime($document->document_date))?></div></td>
<td><div class="label">Gudang Asal</div><div class="value"><?=html_escape($document->from_warehouse ?: '-')?></div></td>
<td><div class="label">Gudang Tujuan</div><div class="value"><?=html_escape($document->to_warehouse ?: '-')?></div></td>
</tr>
<tr>
<td><div class="label">Status</div><div class="value"><?=html_escape(strtoupper($document->status))?></div></td>
<td><div class="label">Dokumen Sumber</div><div class="value"><?=html_escape(($document->source_type ?: '-').' #'.($document->source_id ?: '-'))?></div></td>
<td><div class="label">Alasan</div><div class="value"><?=html_escape($document->reason ?: '-')?></div></td>
</tr>
</table>
<table class="lines">
<thead>
<tr><th>No</th><th>Barang</th><th>Barcode / Serial</th><th>Batch</th><th class="right">Qty</th><th class="right">Biaya Satuan</th><th>Catatan</th></tr>
</thead>
<tbody>
<?php foreach ($document->lines as $line): ?>
<tr>
<td><?=$line->line_no?></td>
<td><b><?=html_escape($line->kode_detail)?></b><br><?=html_escape($line->nama_barang)?></td>
<td><?=html_escape($line->barcode ?: '-')?><?php if ($line->serial_number): ?><br>SN <?=html_escape($line->serial_number)?><?php endif ?></td>
<td><?=html_escape($line->batch_no ?: '-')?></td>
<td class="right"><?=$formatQty($document->document_type === 'opname' ? $line->counted_qty : $line->qty)?> <?=html_escape($line->unit ?: '')?></td>
<td class="right">Rp <?=number_format((float) $line->unit_cost, 2, ',', '.')?></td>
<td><?=html_escape($line->reason ?: '-')?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<div class="attachments">
<div class="label">Lampiran Pendukung</div>
<?php if ($document->attachments): ?>
<ul><?php foreach ($document->attachments as $attachment): ?><li><?=html_escape($attachment->original_name)?></li><?php endforeach ?></ul>
<?php else: ?>
<p>Tidak ada lampiran.</p>
<?php endif ?>
</div>
<table class="sign">
<tr>
<td>Dibuat oleh<br><br><br><b><?=html_escape($document->creator_name ?: '-')?></b></td>
<td>Diajukan oleh<br><br><br><b><?=html_escape($document->submitter_name ?: '-')?></b></td>
<td>Disetujui oleh<br><br><br><b><?=html_escape($document->approver_name ?: '-')?></b></td>
<td>Diposting oleh<br><br><br><b><?=html_escape($document->poster_name ?: '-')?></b></td>
</tr>
</table>
<div class="foot">Dicetak <?=date('d/m/Y H:i')?> &middot; Dokumen elektronik dapat diverifikasi melalui riwayat status aplikasi.</div>
</body>
</html>
@@ -0,0 +1,23 @@
<?php $inventory_title='Gudang & Lokasi Rak';$inventory_subtitle='Kelola gudang, rak/bin, serta batch tanpa menghapus histori transaksi.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<section class="inventory-card">
<div class="inventory-card-head"><div><h5>Master Lokasi Persediaan</h5><p>Data yang sudah digunakan dinonaktifkan, bukan dihapus.</p></div></div>
<ul class="nav nav-tabs px-3 pt-3" role="tablist"><li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#warehousePane">Gudang</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#binPane">Rak / Bin</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#batchPane">Batch / Lot</button></li></ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="warehousePane"><div class="inventory-card-head"><div><h5>Daftar Gudang</h5><p>Pusat penyimpanan fisik barang.</p></div><?php if($inventory_permissions['update']):?><button class="btn btn-sm btn-primary add-master" data-type="warehouse"><i class="bi bi-plus"></i> Gudang</button><?php endif?></div><div class="table-responsive"><table class="table inventory-table mb-0"><thead><tr><th>Kode</th><th>Nama</th><th>Alamat</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="warehouseRows"></tbody></table></div></div>
<div class="tab-pane fade" id="binPane"><div class="inventory-card-head"><div><h5>Lokasi Rak / Bin</h5><p>Lokasi spesifik di dalam gudang.</p></div><?php if($inventory_permissions['update']):?><button class="btn btn-sm btn-primary add-master" data-type="bin"><i class="bi bi-plus"></i> Rak</button><?php endif?></div><div class="table-responsive"><table class="table inventory-table mb-0"><thead><tr><th>Gudang</th><th>Kode Rak</th><th>Nama</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="binRows"></tbody></table></div></div>
<div class="tab-pane fade" id="batchPane"><div class="inventory-card-head"><div><h5>Batch / Lot</h5><p>Pelacakan tanggal produksi dan kedaluwarsa.</p></div><?php if($inventory_permissions['update']):?><button class="btn btn-sm btn-primary add-master" data-type="batch"><i class="bi bi-plus"></i> Batch</button><?php endif?></div><div class="table-responsive"><table class="table inventory-table mb-0"><thead><tr><th>Barang</th><th>Batch</th><th>Produksi</th><th>Kedaluwarsa</th><th>Supplier</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="batchRows"></tbody></table></div></div>
</div>
</section>
<div class="modal fade inventory-modal" id="masterModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">MASTER PERSEDIAAN</small><h5 class="modal-title">Data Master</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="masterForm"><div class="modal-body" id="masterFields"></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan</button></div></form></div></div></div>
<script>
$(function(){
const canUpdate=<?=json_encode($inventory_permissions['update'])?>,csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null?'-':v).html();let data={warehouses:[],bins:[],batches:[]},type='warehouse',editing=null;
function badge(v){return '<span class="status-chip '+(Number(v)?'status-approved':'status-rejected')+'">'+(Number(v)?'Aktif':'Nonaktif')+'</span>'}function button(t,x){return canUpdate?'<button class="btn btn-sm btn-outline-primary edit-master" data-type="'+t+'" data-id="'+x.id+'"><i class="bi bi-pencil"></i> Ubah</button>':'-'}
function load(){$.get('<?=base_url('inventoryprofessional/master_data')?>').done(r=>{data=r;$('#warehouseRows').html(r.warehouses.map(x=>'<tr><td><strong>'+esc(x.code)+'</strong></td><td>'+esc(x.nama)+'</td><td>'+esc(x.address)+'</td><td>'+badge(x.is_active)+'</td><td>'+button('warehouse',x)+'</td></tr>').join('')||empty(5));$('#binRows').html(r.bins.map(x=>'<tr><td>'+esc(x.warehouse_name)+'</td><td><strong>'+esc(x.code)+'</strong></td><td>'+esc(x.name)+'</td><td>'+badge(x.is_active)+'</td><td>'+button('bin',x)+'</td></tr>').join('')||empty(5));$('#batchRows').html(r.batches.map(x=>'<tr><td>'+esc(x.kode_detail+' - '+x.nama_barang)+'</td><td><strong>'+esc(x.batch_no)+'</strong></td><td>'+esc(x.manufactured_date)+'</td><td>'+esc(x.expiry_date)+'</td><td>'+esc(x.supplier_name)+'</td><td>'+badge(x.is_active)+'</td><td>'+button('batch',x)+'</td></tr>').join('')||empty(7))}).fail(fail)}function empty(n){return'<tr><td colspan="'+n+'" class="text-center text-muted py-4">Belum ada data.</td></tr>'}function fail(x){AppAlert.toast(x.responseJSON&&x.responseJSON.message||'Data tidak dapat diproses.','error')}
function warehouseOptions(selected){return'<option value="">-- Pilih Gudang --</option>'+data.warehouses.filter(x=>Number(x.is_active)||String(x.id)===String(selected)).map(x=>'<option value="'+x.id+'" '+(String(x.id)===String(selected)?'selected':'')+'>'+esc(x.code+' - '+x.nama)+'</option>').join('')}
function fields(t,x){x=x||{};if(t==='warehouse')return'<input type="hidden" id="masterId" value="'+(x.id||'')+'"><div class="mb-3"><label class="form-label">Kode Gudang</label><input id="warehouseCode" class="form-control" value="'+esc(x.code||'')+'" required></div><div class="mb-3"><label class="form-label">Nama Gudang</label><input id="warehouseName" class="form-control" value="'+esc(x.nama||'')+'" required></div><div class="mb-3"><label class="form-label">Alamat</label><textarea id="warehouseAddress" class="form-control">'+esc(x.address||'')+'</textarea></div>'+active(x);if(t==='bin')return'<input type="hidden" id="masterId" value="'+(x.id||'')+'"><div class="mb-3"><label class="form-label">Gudang</label><select id="binWarehouse" class="form-select" required>'+warehouseOptions(x.warehouse_id)+'</select></div><div class="mb-3"><label class="form-label">Kode Rak / Bin</label><input id="binCode" class="form-control" value="'+esc(x.code||'')+'" required></div><div class="mb-3"><label class="form-label">Nama Lokasi</label><input id="binName" class="form-control" value="'+esc(x.name||'')+'"></div>'+active(x);return'<input type="hidden" id="masterId" value="'+(x.id||'')+'"><div class="mb-3"><label class="form-label">Barang</label><select id="batchItem" class="form-select"></select></div><div class="mb-3"><label class="form-label">Nomor Batch / Lot</label><input id="batchNo" class="form-control" value="'+esc(x.batch_no||'')+'" required></div><div class="row g-3"><div class="col-6"><label class="form-label">Tanggal Produksi</label><input id="batchMfg" type="date" class="form-control" value="'+esc(x.manufactured_date||'')+'"></div><div class="col-6"><label class="form-label">Kedaluwarsa</label><input id="batchExpiry" type="date" class="form-control" value="'+esc(x.expiry_date||'')+'"></div></div>'+active(x)}function active(x){return'<div class="form-check form-switch mt-3"><input id="masterActive" class="form-check-input" type="checkbox" '+(x.id&&!Number(x.is_active)?'':'checked')+'><label class="form-check-label">Aktif dan dapat dipilih pada transaksi baru</label></div>'}
function open(t,x){type=t;editing=x||null;$('.modal-title').text((x?'Ubah ':'Tambah ')+(t==='warehouse'?'Gudang':t==='bin'?'Rak / Bin':'Batch / Lot'));$('#masterFields').html(fields(t,x));if(t==='batch'){$('#batchItem').select2({width:'100%',dropdownParent:$('#masterModal'),placeholder:'Cari barang',ajax:{url:'<?=base_url('inventoryprofessional/lookup_items')?>',dataType:'json',delay:250,data:p=>({q:p.term||''}),processResults:r=>({results:r.results})}});if(x&&x.item_id){let o=new Option(x.kode_detail+' - '+x.nama_barang,x.item_id,true,true);$('#batchItem').append(o).trigger('change')}}bootstrap.Modal.getOrCreateInstance('#masterModal').show()}
$('.add-master').on('click',function(){open($(this).data('type'))});$(document).on('click','.edit-master',function(){let t=$(this).data('type'),list=t==='warehouse'?data.warehouses:t==='bin'?data.bins:data.batches;open(t,list.find(x=>String(x.id)===String($(this).data('id'))))});
$('#masterForm').on('submit',function(e){e.preventDefault();let payload={id:$('#masterId').val(),is_active:$('#masterActive').is(':checked')?1:0},url='';if(type==='warehouse'){url='save_warehouse';Object.assign(payload,{code:$('#warehouseCode').val(),name:$('#warehouseName').val(),address:$('#warehouseAddress').val()})}else if(type==='bin'){url='save_bin';Object.assign(payload,{warehouse_id:$('#binWarehouse').val(),code:$('#binCode').val(),name:$('#binName').val()})}else{url='save_batch';Object.assign(payload,{item_id:$('#batchItem').val(),batch_no:$('#batchNo').val(),manufactured_date:$('#batchMfg').val(),expiry_date:$('#batchExpiry').val()})}let b=$(this).find('button[type=submit]').prop('disabled',true);$.post('<?=base_url('inventoryprofessional')?>/'+url,Object.assign(payload,csrf())).done(r=>{bootstrap.Modal.getInstance(document.getElementById('masterModal')).hide();AppAlert.toast(r.message,'success');load()}).fail(fail).always(()=>b.prop('disabled',false))});load();
});
</script>
@@ -0,0 +1,67 @@
<?php $inventory_title='Operasional Stok';$inventory_subtitle='Buat, setujui, posting, dan telusuri setiap mutasi gudang.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<nav class="inventory-operation-tabs mb-3" aria-label="Alur operasional stok">
<button data-inventory-tab="manual"><i class="bi bi-box-arrow-in-down"></i><span>Masuk/Keluar Manual</span></button>
<button data-inventory-tab="transfer"><i class="bi bi-arrow-left-right"></i><span>Transfer Gudang</span></button>
<button data-inventory-tab="reservation"><i class="bi bi-bookmark-check"></i><span>Reservasi</span></button>
<button data-inventory-tab="adjustment"><i class="bi bi-sliders"></i><span>Penyesuaian</span></button>
<button data-inventory-tab="opname"><i class="bi bi-clipboard-check"></i><span>Stock Opname</span></button>
<button data-inventory-tab="damage"><i class="bi bi-exclamation-diamond"></i><span>Rusak/Hilang</span></button>
<button class="active" data-inventory-tab=""><i class="bi bi-clock-history"></i><span>Riwayat Dokumen</span></button>
</nav>
<section class="inventory-card" id="documentSection">
<div class="inventory-card-head"><div><h5>Dokumen Operasional</h5><p>Penerimaan dan retur pembelian tetap dibuat dari Purchase Workflow.</p></div><?php if($inventory_permissions['create']):?><button class="btn btn-primary" id="newDocument"><i class="bi bi-plus-lg"></i> Buat Dokumen</button><?php endif?></div>
<div class="p-3 border-bottom inventory-toolbar">
<div class="filter-field"><label class="form-label">Jenis Mutasi</label><select id="filterType" class="form-select form-select-sm"><option value="">Semua</option><option value="manual">Masuk/Keluar Manual</option><option value="opening">Saldo Awal</option><option value="in">Barang Masuk Manual</option><option value="out">Barang Keluar Manual</option><option value="transfer">Transfer Gudang</option><option value="adjustment">Penyesuaian</option><option value="opname">Stock Opname</option><option value="damage">Rusak/Hilang</option><option value="damaged">Barang Rusak</option><option value="lost">Barang Hilang</option></select></div>
<div class="filter-field"><label class="form-label">Status</label><select id="filterStatus" class="form-select form-select-sm"><option value="">Semua</option><option value="draft">Draft</option><option value="submitted">Diajukan</option><option value="approved">Disetujui</option><option value="posted">Diposting</option><option value="rejected">Ditolak</option><option value="reversed">Dibalik</option></select></div>
<button class="btn btn-sm btn-outline-secondary" id="refreshDocuments"><i class="bi bi-arrow-clockwise"></i> Muat Ulang</button>
</div>
<div class="table-responsive"><table id="documentTable" class="table inventory-table align-middle w-100 mb-0"><thead><tr><th>Dokumen</th><th>Tanggal</th><th>Jenis</th><th>Gudang</th><th>Baris</th><th>Status</th><th>Pembuat</th><th>Aksi</th></tr></thead></table></div>
</section>
<section class="inventory-card mt-3 d-none" id="reservationSection">
<div class="inventory-card-head"><div><h5>Reservasi / Alokasi Stok</h5><p>Stok dialokasikan ke dokumen sumber dan dapat dilepas atau digunakan bertahap.</p></div><?php if($inventory_permissions['create']):?><button class="btn btn-outline-primary" id="newReservation"><i class="bi bi-bookmark-plus"></i> Buat Reservasi</button><?php endif?></div>
<div class="table-responsive"><table class="table inventory-table align-middle mb-0" id="reservationTable"><thead><tr><th>Reservasi</th><th>Barang</th><th>Gudang</th><th>Sumber</th><th>Qty</th><th>Berlaku Sampai</th><th>Status</th><th>Aksi</th></tr></thead><tbody></tbody></table></div>
</section>
<div class="modal fade inventory-modal" id="documentModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">DOKUMEN PERSEDIAAN</small><h5 class="modal-title">Buat Dokumen Mutasi</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="documentForm"><div class="modal-body">
<input type="hidden" id="idempotencyKey"><div class="row g-3"><div class="col-md-3"><label class="form-label">Jenis Mutasi</label><select id="documentType" class="form-select" required><option value="">-- Pilih --</option><option value="opening">Saldo Awal</option><option value="in">Barang Masuk Manual</option><option value="out">Barang Keluar Manual</option><option value="transfer">Transfer Antar-gudang</option><option value="adjustment">Penyesuaian Stok</option><option value="opname">Stock Opname</option><option value="damaged">Barang Rusak</option><option value="lost">Barang Hilang</option></select></div><div class="col-md-3"><label class="form-label">Tanggal</label><input id="documentDate" type="date" class="form-control" value="<?=date('Y-m-d')?>" required></div><div class="col-md-3 warehouse-from"><label class="form-label">Gudang Asal / Lokasi Opname</label><select id="fromWarehouse" class="form-select"><option value="">-- Pilih --</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div><div class="col-md-3 warehouse-to"><label class="form-label">Gudang Tujuan</label><select id="toWarehouse" class="form-select"><option value="">-- Pilih --</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div></div>
<div class="row g-3 mt-0"><div class="col-md-3"><label class="form-label">Jenis Dokumen Sumber</label><input id="sourceType" class="form-control" placeholder="Contoh: memo_gudang"></div><div class="col-md-3"><label class="form-label">Nomor/ID Sumber</label><input id="sourceId" type="number" min="1" class="form-control" placeholder="Opsional"></div><div class="col-md-6"><label class="form-label">Alasan / Referensi</label><input id="documentReason" class="form-control" maxlength="500" required placeholder="Jelaskan tujuan dan dasar mutasi"></div></div>
<div class="d-flex justify-content-between align-items-center mt-4 mb-2"><div><h6 class="mb-0">Detail Barang</h6><small class="text-muted">Satu dokumen dapat memuat beberapa barang.</small></div><button type="button" class="btn btn-sm btn-outline-primary" id="addLine"><i class="bi bi-plus"></i> Tambah Barang</button></div>
<div class="table-responsive"><table class="table inventory-table inventory-line-table align-middle"><thead><tr><th>Barang</th><th>Barcode/SN</th><th>Batch</th><th class="direction-column">Arah</th><th>Qty / Hitung Fisik</th><th>Biaya Satuan</th><th>Catatan</th><th></th></tr></thead><tbody id="documentLines"></tbody></table></div>
<div><label class="form-label">Lampiran Pendukung <span class="text-muted">(gambar/PDF, opsional)</span></label><input id="documentAttachments" type="file" class="d-none" accept="image/*,application/pdf" multiple><div id="attachmentNames" class="inventory-attachments"><button type="button" class="attachment-add" id="chooseAttachments"><i class="bi bi-plus-lg"></i><span>Tambah</span></button></div></div>
</div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="saveDocument"><i class="bi bi-save"></i> Simpan Draft</button></div></form></div></div></div>
<div class="modal fade inventory-modal" id="detailModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">AUDIT DOKUMEN</small><h5 class="modal-title" id="detailTitle">Detail Dokumen</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="detailBody"></div><div class="modal-footer" id="detailActions"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<div class="modal fade inventory-modal" id="pdfModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Dokumen Persediaan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0" style="height:min(82vh,900px)"><iframe title="Dokumen persediaan" style="width:100%;height:100%;border:0"></iframe></div></div></div></div>
<div class="modal fade inventory-modal" id="reservationModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-lg"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">ALOKASI STOK</small><h5 class="modal-title">Buat Reservasi</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="reservationForm"><div class="modal-body"><div class="row g-3"><div class="col-md-6"><label class="form-label">Barang</label><select id="reservationItem" class="form-select"></select></div><div class="col-md-6"><label class="form-label">Gudang</label><select id="reservationWarehouse" class="form-select"><option value="">-- Pilih --</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div><div class="col-md-6"><label class="form-label">Barcode/SN <span class="text-muted">(opsional)</span></label><select id="reservationBarcode" class="form-select"></select></div><div class="col-md-3"><label class="form-label">Qty</label><input id="reservationQty" inputmode="decimal" class="form-control" required></div><div class="col-md-3"><label class="form-label">Berlaku Sampai</label><input id="reservationExpiry" type="datetime-local" class="form-control"></div><div class="col-md-4"><label class="form-label">Jenis Sumber</label><input id="reservationSourceType" class="form-control" value="sales_order" required></div><div class="col-md-4"><label class="form-label">ID Dokumen Sumber</label><input id="reservationSourceId" type="number" min="1" class="form-control" required></div><div class="col-md-4"><label class="form-label">Catatan</label><input id="reservationNotes" class="form-control"></div></div><div id="reservationAvailability" class="inventory-balance-box mt-3"><div><span>Stok akan diperiksa saat disimpan.</span></div></div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan Reservasi</button></div></form></div></div></div>
<script>
$(function(){
const permissions=<?=json_encode($inventory_permissions)?>,csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null?'-':v).html(),fmt=v=>Number(v||0).toLocaleString('id-ID',{maximumFractionDigits:4}),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
const labels={opening:'Saldo Awal',in:'Masuk Manual',out:'Keluar Manual',transfer:'Transfer Gudang',adjustment:'Penyesuaian',opname:'Stock Opname',damaged:'Barang Rusak',lost:'Barang Hilang',draft:'Draft',submitted:'Diajukan',approved:'Disetujui',posted:'Diposting',rejected:'Ditolak',reversed:'Dibalik'};
function api(url,data){return $.ajax({url:url,type:'POST',data:Object.assign({},data||{},csrf()),dataType:'json'})}
function fail(xhr){AppAlert.toast(xhr.responseJSON&&xhr.responseJSON.message||'Proses tidak berhasil.','error')}
const table=$('#documentTable').DataTable({processing:true,serverSide:true,responsive:true,searchDelay:350,pageLength:10,order:[],ajax:{url:'<?=base_url('inventoryprofessional/document_data')?>',data:d=>{d.type=$('#filterType').val();d.status=$('#filterStatus').val()}},columns:[{data:'document_no',render:(v,t,r)=>'<button class="btn btn-link p-0 fw-semibold btn-detail" data-id="'+r.id+'">'+esc(v)+'</button>'},{data:'document_date',render:v=>v?v.split('-').reverse().join('/'):'-'},{data:'document_type',render:v=>labels[v]||v},{data:null,render:r=>esc((r.from_warehouse||'-')+' → '+(r.to_warehouse||'-'))},{data:'line_count',className:'text-center'},{data:'status',render:v=>'<span class="status-chip status-'+v+'">'+esc(labels[v]||v)+'</span>'},{data:'creator_name',defaultContent:'-'},{data:null,orderable:false,searchable:false,render:r=>actions(r)}],language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',processing:'Memuat data...',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});
const focusDocument=new URLSearchParams(window.location.search).get('focus');if(focusDocument)table.one('draw.dt',function(){setTimeout(function(){$('<button type="button" class="d-none btn-detail" data-id="'+String(focusDocument).replace(/[^0-9]/g,'')+'"></button>').appendTo(document.body).trigger('click').remove()},100)});
function actions(r){let a='<div class="d-flex flex-wrap gap-1"><button class="btn btn-sm btn-outline-primary btn-detail" data-id="'+r.id+'"><i class="bi bi-eye"></i></button>';if(permissions.export)a+='<button class="btn btn-sm btn-outline-danger btn-pdf" data-id="'+r.id+'"><i class="bi bi-file-earmark-pdf"></i></button>';if(r.status==='draft'&&permissions.create)a+='<button class="btn btn-sm btn-primary btn-state" data-action="submit" data-id="'+r.id+'">Ajukan</button>';if(r.status==='submitted'&&permissions.approve)a+='<button class="btn btn-sm btn-success btn-state" data-action="approve" data-id="'+r.id+'">Setujui</button><button class="btn btn-sm btn-outline-danger btn-reject" data-id="'+r.id+'">Tolak</button>';if(r.status==='approved'&&permissions.post)a+='<button class="btn btn-sm btn-success btn-state" data-action="post" data-id="'+r.id+'">Posting</button>';if(r.status==='posted'&&permissions.post)a+='<button class="btn btn-sm btn-outline-danger btn-reverse" data-id="'+r.id+'">Reversal</button>';return a+'</div>'}
$('#filterType,#filterStatus').on('change',()=>table.ajax.reload());$('#refreshDocuments').on('click',()=>table.ajax.reload(null,false));
$('[data-inventory-tab]').on('click',function(){let tab=$(this).data('inventory-tab');$('[data-inventory-tab]').removeClass('active');$(this).addClass('active');if(tab==='reservation'){$('#documentSection').addClass('d-none');$('#reservationSection').removeClass('d-none');loadReservations();return}$('#reservationSection').addClass('d-none');$('#documentSection').removeClass('d-none');$('#filterType').val(tab);table.ajax.reload()});
function addLine(){let id=Date.now()+Math.random().toString(16).slice(2),tr=$('<tr class="document-line"><td><select class="form-select line-item"></select></td><td><select class="form-select line-barcode"><option value="">Tanpa barcode</option></select></td><td><select class="form-select line-batch"><option value="">Tanpa batch</option></select></td><td class="direction-column"><select class="form-select line-direction"><option value="in">Tambah</option><option value="out">Kurangi</option></select></td><td><input class="form-control line-qty" inputmode="decimal" placeholder="0" required></td><td><input class="form-control line-cost format-rupiah" inputmode="decimal" placeholder="0"></td><td><input class="form-control line-reason" placeholder="Catatan baris"></td><td><button type="button" class="btn btn-sm btn-outline-danger remove-line"><i class="bi bi-x"></i></button></td></tr>');tr.attr('data-key',id);$('#documentLines').append(tr);initLine(tr);toggleType()}
function initLine(tr){tr.find('.line-item').select2({width:'100%',dropdownParent:$('#documentModal'),placeholder:'Cari kode / nama',ajax:{url:'<?=base_url('inventoryprofessional/lookup_items')?>',dataType:'json',delay:250,data:p=>({q:p.term||'',warehouse_id:$('#fromWarehouse').val()||$('#toWarehouse').val()}),processResults:r=>({results:r.results})}}).on('select2:select',function(e){tr.data('item',e.params.data);tr.find('.line-cost').val(Number(e.params.data.harga_beli||0).toLocaleString('id-ID'));if(String(e.params.data.tracking_type).toUpperCase()==='UNIT'&&['out','transfer','damaged','lost'].includes($('#documentType').val()))tr.find('.line-qty').val('1');tr.find('.line-barcode').val(null).trigger('change');tr.find('.line-batch').val(null).trigger('change')});tr.find('.line-barcode').select2({width:'100%',dropdownParent:$('#documentModal'),placeholder:'Opsional',allowClear:true,ajax:{url:'<?=base_url('inventoryprofessional/lookup_barcodes')?>',dataType:'json',delay:200,data:p=>({q:p.term||'',item_id:tr.find('.line-item').val(),warehouse_id:$('#fromWarehouse').val()}),processResults:r=>({results:r.results})}});tr.find('.line-batch').select2({width:'100%',dropdownParent:$('#documentModal'),placeholder:'Opsional',allowClear:true,ajax:{url:'<?=base_url('inventoryprofessional/lookup_batches')?>',dataType:'json',delay:200,data:p=>({q:p.term||'',item_id:tr.find('.line-item').val()}),processResults:r=>({results:r.results})}})}
function toggleType(){let t=$('#documentType').val(),needsFrom=['out','transfer','adjustment','opname','damaged','lost'].includes(t),needsTo=['opening','in','transfer'].includes(t);$('.warehouse-from').toggle(needsFrom);$('.warehouse-to').toggle(needsTo);$('.direction-column').toggle(t==='adjustment');$('.line-cost').prop('disabled',['out','transfer','opname','damaged','lost'].includes(t));$('.line-qty').attr('placeholder',t==='opname'?'Hasil hitung fisik':'Qty')}
$('#newDocument').on('click',function(){$('#documentForm')[0].reset();selectedFiles=[];renderAttachments();$('#documentDate').val('<?=date('Y-m-d')?>');$('#idempotencyKey').val('WEB-INV-'+Date.now()+'-'+Math.random().toString(36).slice(2));$('#documentLines').empty();addLine();toggleType();bootstrap.Modal.getOrCreateInstance('#documentModal').show()});$('#addLine').on('click',addLine);$('#documentType').on('change',toggleType);$(document).on('click','.remove-line',function(){if($('.document-line').length>1)$(this).closest('tr').remove()});
let selectedFiles=[];$(document).on('click','#chooseAttachments',()=>$('#documentAttachments').click());$('#documentAttachments').on('change',function(){selectedFiles=selectedFiles.concat(Array.from(this.files));this.value='';renderAttachments()});function renderAttachments(){let h=selectedFiles.map((f,i)=>{let preview=f.type.indexOf('image/')===0?'<img src="'+URL.createObjectURL(f)+'" alt="">':'<i class="bi bi-file-earmark-pdf attachment-file-icon"></i>';return'<div class="attachment-tile">'+preview+'<button type="button" class="remove-attachment" data-index="'+i+'" aria-label="Hapus"><i class="bi bi-x"></i></button><small>'+esc(f.name)+'</small></div>'}).join('');$('#attachmentNames').html(h+'<button type="button" class="attachment-add" id="chooseAttachments"><i class="bi bi-plus-lg"></i><span>Tambah</span></button>')}$(document).on('click','.remove-attachment',function(){selectedFiles.splice(Number($(this).data('index')),1);renderAttachments()});
$('#documentForm').on('submit',function(e){e.preventDefault();let lines=[];$('.document-line').each(function(){let r=$(this);lines.push({item_id:r.find('.line-item').val(),barcode_id:r.find('.line-barcode').val(),batch_id:r.find('.line-batch').val(),movement_direction:r.find('.line-direction').val(),qty:r.find('.line-qty').val().replace(/\./g,'').replace(',','.'),unit_cost:r.find('.line-cost').val().replace(/\./g,'').replace(',','.'),reason:r.find('.line-reason').val()})});if(lines.some(x=>!x.item_id||!x.qty))return AppAlert.toast('Barang dan qty setiap baris wajib diisi.','warning');let btn=$('#saveDocument').prop('disabled',true);api('<?=base_url('inventoryprofessional/create')?>',{type:$('#documentType').val(),date:$('#documentDate').val(),from_warehouse_id:$('#fromWarehouse').val(),to_warehouse_id:$('#toWarehouse').val(),source_type:$('#sourceType').val(),source_id:$('#sourceId').val(),reason:$('#documentReason').val(),idempotency_key:$('#idempotencyKey').val(),lines:JSON.stringify(lines)}).done(async function(r){let files=selectedFiles.slice();try{for(let f of files){let fd=new FormData();fd.append(APP_CSRF.name,APP_CSRF.value);fd.append('entity_type','stock_document');fd.append('entity_id',r.document_id);fd.append('attachment',f);await $.ajax({url:'<?=base_url('accountingattachments/upload')?>',type:'POST',data:fd,processData:false,contentType:false})}bootstrap.Modal.getInstance(document.getElementById('documentModal')).hide();table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}catch(x){AppAlert.toast('Draft tersimpan, tetapi ada lampiran yang gagal diunggah. Buka detail untuk memeriksa.','warning')}}).fail(fail).always(()=>btn.prop('disabled',false))});
$(document).on('click','.btn-state',function(){let b=$(this),action=b.data('action'),word=action==='submit'?'mengajukan':action==='approve'?'menyetujui':'memposting';AppAlert.confirm({title:'Konfirmasi '+word+' dokumen',text:'Pastikan detail barang dan gudang sudah benar.'}).then(x=>{if(!x.isConfirmed)return;b.prop('disabled',true);api('<?=base_url('inventoryprofessional')?>/'+action+'/'+b.data('id')).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail).always(()=>b.prop('disabled',false))})});
$(document).on('click','.btn-reject',function(){let id=$(this).data('id');AppAlert.prompt({title:'Tolak dokumen stok',input:'textarea',inputLabel:'Alasan penolakan',inputValidator:v=>!v.trim()?'Alasan wajib diisi.':undefined,confirmButtonText:'Tolak Dokumen'}).then(x=>{if(x.isConfirmed)api('<?=base_url('inventoryprofessional/reject')?>/'+id,{reason:x.value}).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail)})});
$(document).on('click','.btn-reverse',function(){let id=$(this).data('id');AppAlert.prompt({title:'Reversal dokumen stok',html:'<label class="form-label d-block text-start">Tanggal reversal</label><input id="reverseDate" type="date" class="form-control mb-3" value="<?=date('Y-m-d')?>"><label class="form-label d-block text-start">Alasan</label><textarea id="reverseReason" class="form-control"></textarea>',preConfirm:()=>{let d=$('#reverseDate').val(),r=$('#reverseReason').val().trim();if(!d||!r){Swal.showValidationMessage('Tanggal dan alasan wajib diisi.');return false}return{date:d,reason:r}},confirmButtonText:'Posting Reversal'}).then(x=>{if(x.isConfirmed)api('<?=base_url('inventoryprofessional/reverse')?>/'+id,x.value).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail)})});
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#detailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#detailModal').show();$.get('<?=base_url('inventoryprofessional/document_detail')?>/'+id).done(function(r){let d=r.document,lines=d.lines.map(l=>'<tr><td>'+esc(l.kode_detail+' - '+l.nama_barang)+'</td><td>'+esc(l.barcode)+(l.serial_number?'<small class="d-block">SN '+esc(l.serial_number)+'</small>':'')+'</td><td>'+esc(l.batch_no)+'</td><td class="text-end">'+fmt(d.document_type==='opname'?l.counted_qty:l.qty)+' '+esc(l.unit)+'</td><td class="text-end">'+money(l.unit_cost)+'</td><td>'+esc(l.reason)+'</td></tr>').join(''),hist=d.history.map(h=>'<li><strong>'+esc(labels[h.to_status]||h.to_status)+'</strong><span>'+esc(h.user_name)+' · '+esc(h.created_at)+'</span><span>'+esc(h.notes)+'</span></li>').join(''),atts=d.attachments.map(a=>'<button class="btn btn-sm btn-outline-secondary attachment-preview" data-url="'+a.preview_url+'"><i class="bi bi-paperclip"></i> '+esc(a.original_name)+'</button>').join(' ');$('#detailTitle').text(d.document_no);$('#detailBody').html('<div class="document-meta"><span>Jenis<strong>'+esc(labels[d.document_type]||d.document_type)+'</strong></span><span>Tanggal<strong>'+esc(d.document_date)+'</strong></span><span>Status<strong><span class="status-chip status-'+d.status+'">'+esc(labels[d.status]||d.status)+'</span></strong></span><span>Gudang Asal<strong>'+esc(d.from_warehouse)+'</strong></span><span>Gudang Tujuan<strong>'+esc(d.to_warehouse)+'</strong></span><span>Dokumen Sumber<strong>'+esc(d.source_type)+' #'+esc(d.source_id)+'</strong></span><span>Pembuat<strong>'+esc(d.creator_name)+'</strong></span><span>Pengaju<strong>'+esc(d.submitter_name)+'</strong></span><span>Approver / Poster<strong>'+esc(d.approver_name)+' / '+esc(d.poster_name)+'</strong></span></div><div class="alert alert-light border mt-3"><strong>Alasan:</strong> '+esc(d.reason)+'</div><div class="table-responsive"><table class="table inventory-table"><thead><tr><th>Barang</th><th>Barcode/SN</th><th>Batch</th><th class="text-end">Qty</th><th class="text-end">Biaya</th><th>Catatan</th></tr></thead><tbody>'+lines+'</tbody></table></div><h6>Lampiran</h6><div class="mb-3 d-flex flex-wrap gap-2">'+(atts||'<span class="text-muted">Tidak ada lampiran.</span>')+'</div><h6>Riwayat Status</h6><ul class="timeline-list">'+hist+'</ul>');$('#detailActions').html('<button class="btn btn-outline-danger btn-pdf" data-id="'+d.id+'"><i class="bi bi-file-earmark-pdf"></i> PDF</button><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button>')}).fail(fail)});
$(document).on('click','.btn-pdf',function(){let id=$(this).data('id');$('#pdfModal iframe').attr('src','<?=base_url('inventoryprofessional/document_pdf')?>/'+id);bootstrap.Modal.getOrCreateInstance('#pdfModal').show()});
$(document).on('click','.attachment-preview',function(){$('#pdfModal iframe').attr('src',$(this).data('url'));bootstrap.Modal.getOrCreateInstance('#pdfModal').show()});
function initReservationSelects(){$('#reservationItem').select2({width:'100%',dropdownParent:$('#reservationModal'),placeholder:'Cari barang',ajax:{url:'<?=base_url('inventoryprofessional/lookup_items')?>',dataType:'json',delay:250,data:p=>({q:p.term||'',warehouse_id:$('#reservationWarehouse').val()}),processResults:r=>({results:r.results})}}).on('select2:select',function(e){let x=e.params.data;$('#reservationAvailability').html('<div><span>Stok fisik</span><strong>'+fmt(x.on_hand)+'</strong></div><div><span>Sudah direservasi</span><strong>'+fmt(x.reserved)+'</strong></div><div><span>Tersedia</span><strong>'+fmt(Number(x.on_hand)-Number(x.reserved))+'</strong></div>')});$('#reservationBarcode').select2({width:'100%',dropdownParent:$('#reservationModal'),placeholder:'Opsional',allowClear:true,ajax:{url:'<?=base_url('inventoryprofessional/lookup_barcodes')?>',dataType:'json',delay:200,data:p=>({q:p.term||'',item_id:$('#reservationItem').val(),warehouse_id:$('#reservationWarehouse').val()}),processResults:r=>({results:r.results})}})}initReservationSelects();
$('#newReservation').on('click',function(){$('#reservationForm')[0].reset();$('#reservationItem,#reservationBarcode').val(null).trigger('change');bootstrap.Modal.getOrCreateInstance('#reservationModal').show()});$('#reservationForm').on('submit',function(e){e.preventDefault();let b=$(this).find('button[type=submit]').prop('disabled',true);api('<?=base_url('inventoryprofessional/reserve')?>',{item_id:$('#reservationItem').val(),warehouse_id:$('#reservationWarehouse').val(),barcode_id:$('#reservationBarcode').val(),qty:$('#reservationQty').val().replace(',','.'),source_type:$('#reservationSourceType').val(),source_id:$('#reservationSourceId').val(),expires_at:$('#reservationExpiry').val(),notes:$('#reservationNotes').val()}).done(r=>{bootstrap.Modal.getInstance(document.getElementById('reservationModal')).hide();loadReservations();AppAlert.toast(r.message,'success')}).fail(fail).always(()=>b.prop('disabled',false))});
function loadReservations(){$.get('<?=base_url('inventoryprofessional/reservation_data')?>').done(r=>{$('#reservationTable tbody').html(r.data.map(x=>'<tr><td><strong>'+esc(x.reservation_no)+'</strong><small class="d-block text-muted">'+esc(x.barcode||x.serial_number)+'</small></td><td>'+esc(x.kode_detail+' - '+x.nama_barang)+'</td><td>'+esc(x.warehouse_name)+'</td><td>'+esc(x.source_type)+' #'+esc(x.source_id)+'</td><td>'+fmt(Number(x.qty)-Number(x.released_qty))+' / '+fmt(x.qty)+'</td><td>'+esc(x.expires_at)+'</td><td><span class="status-chip status-'+x.status+'">'+esc(x.status)+'</span></td><td>'+reservationActions(x)+'</td></tr>').join('')||'<tr><td colspan="8" class="text-center text-muted py-4">Belum ada reservasi.</td></tr>')})}function reservationActions(x){if(!permissions.update||!['active','partially_released'].includes(x.status))return'-';return'<div class="btn-group btn-group-sm"><button class="btn btn-outline-primary reservation-action" data-action="release" data-id="'+x.id+'" data-max="'+(Number(x.qty)-Number(x.released_qty))+'">Lepas</button><button class="btn btn-outline-success reservation-action" data-action="consume" data-id="'+x.id+'" data-max="'+(Number(x.qty)-Number(x.released_qty))+'">Gunakan</button><button class="btn btn-outline-danger reservation-action" data-action="cancel" data-id="'+x.id+'" data-max="'+(Number(x.qty)-Number(x.released_qty))+'">Batal</button></div>'}$(document).on('click','.reservation-action',function(){let b=$(this),action=b.data('action'),max=b.data('max');AppAlert.prompt({title:'Perbarui reservasi',html:action==='cancel'?'<p>Seluruh sisa reservasi akan dibatalkan.</p><textarea id="resNotes" class="form-control" placeholder="Catatan"></textarea>':'<label class="form-label d-block text-start">Qty (maks. '+fmt(max)+')</label><input id="resQty" class="form-control" value="'+max+'"><textarea id="resNotes" class="form-control mt-2" placeholder="Catatan"></textarea>',preConfirm:()=>({qty:action==='cancel'?0:$('#resQty').val().replace(',','.'),notes:$('#resNotes').val()})}).then(v=>{if(v.isConfirmed)api('<?=base_url('inventoryprofessional/update_reservation')?>/'+b.data('id'),{action:action,qty:v.value.qty,notes:v.value.notes}).done(r=>{loadReservations();AppAlert.toast(r.message,'success')}).fail(fail)})});loadReservations();
let focus=new URLSearchParams(location.search).get('focus');if(focus)setTimeout(()=>$('.btn-detail[data-id="'+focus+'"]').first().click(),800);
});
</script>
@@ -0,0 +1,10 @@
<!doctype html>
<html><head><meta charset="utf-8"><style>
body{font-family:DejaVu Sans,sans-serif;font-size:8px;color:#253047}h1{font-size:17px;margin:0}.head{border-bottom:2px solid #ee7800;padding-bottom:10px;margin-bottom:14px}.company{font-size:11px;font-weight:700}.period{color:#657086}table{width:100%;border-collapse:collapse}th{background:#edf2f8;text-align:left}th,td{border:1px solid #dfe5ed;padding:5px;vertical-align:top}.num{text-align:right}.foot{margin-top:12px;color:#788292}
</style></head><body>
<div class="head"><div class="company"><?=html_escape($company->name??$company->company_name??'Perusahaan')?></div><h1><?=html_escape($definition['title'])?></h1><div class="period">Periode <?=date('d/m/Y',strtotime($filters['date_from']))?> sampai <?=date('d/m/Y',strtotime($filters['date_to']))?></div></div>
<table><thead><tr><?php foreach($definition['columns']as$label):?><th><?=html_escape($label)?></th><?php endforeach?></tr></thead><tbody>
<?php if(!$rows):?><tr><td colspan="<?=count($definition['columns'])?>" style="text-align:center">Tidak ada data pada filter yang dipilih.</td></tr><?php endif?>
<?php foreach($rows as$row):?><tr><?php foreach($definition['columns']as$key=>$label):$value=$row->$key??null;$numeric=is_numeric($value)&&!in_array($key,array('source_id'),true);$money=in_array($key,array('inventory_value','value','unit_cost','ledger_value','stock_log_value','gl_value','difference'),true);?><td class="<?=$numeric?'num':''?>"><?php if($money):?>Rp <?=number_format((float)$value,2,',','.')?><?php elseif($numeric):?><?=number_format((float)$value,abs((float)$value-round((float)$value))<.0001?0:4,',','.')?><?php else:?><?=html_escape($value===null||$value===''?'-':$value)?><?php endif?></td><?php endforeach?></tr><?php endforeach?>
</tbody></table><div class="foot">Dicetak <?=date('d/m/Y H:i')?>. Sumber saldo: inventory ledger. Filter dan histori dokumen dipertahankan untuk kebutuhan audit.</div>
</body></html>
@@ -0,0 +1,63 @@
<?php
$inventory_title='Laporan Persediaan';
$inventory_subtitle='Laporan operasional, penelusuran barcode, nilai stok, dan rekonsiliasi accounting dalam satu tempat.';
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
?>
<div class="row g-3">
<div class="col-xl-9">
<section class="inventory-card">
<div class="inventory-card-head">
<div><h5 id="reportTitle">Nilai Persediaan</h5><p>Gunakan filter untuk menelusuri saldo dan dokumen sumber.</p></div>
<?php if($inventory_permissions['export']):?><div class="d-flex gap-2"><button class="btn btn-sm btn-outline-success" id="exportExcel"><i class="bi bi-file-earmark-spreadsheet"></i> Excel</button><button class="btn btn-sm btn-outline-danger" id="exportPdf"><i class="bi bi-file-earmark-pdf"></i> PDF</button></div><?php endif?>
</div>
<div class="p-3 border-bottom inventory-toolbar">
<div class="filter-field"><label class="form-label">Jenis Laporan</label><select id="reportType" class="form-select"><option value="valuation">Nilai Persediaan</option><option value="stock_card">Kartu Stok</option><option value="location">Stok per Gudang & Rak</option><option value="barcode">Barcode & Serial</option><option value="batch">Batch & Kedaluwarsa</option><option value="reservation">Reservasi Stok</option><option value="reorder">Stok Minimum / Reorder</option><option value="slow">Slow-moving / Dead Stock</option><option value="damage">Barang Rusak / Hilang</option><option value="opname">Stock Opname</option><option value="reconciliation">Riwayat Rekonsiliasi</option></select></div>
<div class="filter-field"><label class="form-label">Gudang</label><select id="reportWarehouse" class="form-select"><option value="">Semua Gudang</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div>
<div class="filter-field item-filter"><label class="form-label">Barang</label><select id="reportItem" class="form-select"></select></div>
<div class="filter-field date-filter"><label class="form-label">Dari Tanggal</label><input id="dateFrom" type="date" class="form-control" value="<?=date('Y-m-01')?>"></div>
<div class="filter-field date-filter"><label class="form-label">Sampai Tanggal</label><input id="dateTo" type="date" class="form-control" value="<?=date('Y-m-d')?>"></div>
<div class="filter-field slow-filter d-none"><label class="form-label">Tidak Bergerak</label><select id="reportDays" class="form-select"><option value="30">Minimal 30 hari</option><option value="60">Minimal 60 hari</option><option value="90" selected>Minimal 90 hari</option><option value="180">Minimal 180 hari</option></select></div>
<button class="btn btn-primary align-self-end" id="loadReport"><i class="bi bi-search"></i> Tampilkan</button>
</div>
<div class="table-responsive"><table class="table inventory-table align-middle mb-0 w-100" id="reportTable"><thead></thead><tbody></tbody></table></div>
</section>
</div>
<div class="col-xl-3">
<section class="inventory-card">
<div class="inventory-card-head"><div><h5>Rekonsiliasi</h5><p>Ledger dibandingkan dengan stock log, barcode, cache, dan GL.</p></div></div>
<div class="p-3"><label class="form-label">Posisi per Tanggal</label><input id="reconcileDate" type="date" class="form-control" value="<?=date('Y-m-d')?>"><div id="reconcileResult" class="inventory-balance-box my-3"><div><span>Belum diperiksa.</span></div></div><?php if($inventory_permissions['post']):?><button id="runReconciliation" class="btn btn-outline-primary w-100"><i class="bi bi-arrow-repeat"></i> Jalankan Rekonsiliasi</button><?php endif?><div id="reconcileIssues" class="mt-3"></div><small class="d-block text-muted mt-3">Selisih tidak membuat jurnal koreksi otomatis. Anomali ditampilkan agar dapat ditelusuri ke dokumen sumber.</small></div>
</section>
</div>
</div>
<div class="modal fade inventory-modal" id="reportPdfModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Laporan Persediaan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0" style="height:min(82vh,900px)"><iframe title="Laporan persediaan" style="width:100%;height:100%;border:0"></iframe></div></div></div></div>
<script>
$(function(){
const csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null||v===''?'-':v).html(),qty=v=>Number(v||0).toLocaleString('id-ID',{maximumFractionDigits:4}),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
const reports={
valuation:{title:'Nilai Persediaan',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['costing_method','Metode'],['qty','Qty','qty'],['inventory_value','Nilai','money']]},
stock_card:{title:'Kartu Stok',cols:[['movement_date','Tanggal'],['document_no','Dokumen'],['warehouse_name','Gudang'],['direction','Arah'],['qty','Qty','qty'],['unit_cost','Biaya Satuan','money'],['value','Nilai','money'],['reason','Keterangan']]},
location:{title:'Stok per Gudang dan Rak',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['bin_code','Rak/Bin'],['qty','Qty','qty']]},
barcode:{title:'Barcode dan Serial',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['barcode','Barcode'],['serial_number','Serial'],['warehouse_name','Gudang'],['bin_code','Rak'],['batch_no','Batch'],['qty_sisa','Qty','qty'],['reserved_qty','Reservasi','qty'],['status','Status','status']]},
batch:{title:'Batch dan Kedaluwarsa',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['batch_no','Batch'],['warehouse_name','Gudang'],['manufactured_date','Produksi'],['expiry_date','Kedaluwarsa'],['qty','Qty','qty']]},
reservation:{title:'Reservasi Stok',cols:[['reservation_no','Reservasi'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['source_type','Jenis Sumber'],['source_id','ID Sumber'],['qty','Qty','qty'],['released_qty','Diselesaikan','qty'],['expires_at','Berlaku Sampai'],['status','Status','status']]},
reorder:{title:'Stok Minimum dan Reorder',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['qty','Qty','qty'],['reorder_point','Reorder Point','qty'],['minimum_stock','Minimum','qty']]},
slow:{title:'Slow-moving dan Dead Stock',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['stok','Stok','qty'],['last_movement_at','Terakhir Bergerak'],['idle_days','Tidak Bergerak (hari)','qty']]},
damage:{title:'Barang Rusak dan Hilang',cols:[['document_no','Dokumen'],['document_date','Tanggal'],['document_type','Jenis'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['qty','Qty','qty'],['value','Nilai','money'],['status','Status','status'],['reason','Alasan']]},
opname:{title:'Stock Opname',cols:[['document_no','Dokumen'],['document_date','Tanggal'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['system_qty','Stok Sistem','qty'],['counted_qty','Fisik','qty'],['difference','Selisih','qty'],['status','Status','status']]},
reconciliation:{title:'Riwayat Rekonsiliasi',cols:[['as_of_date','Tanggal'],['ledger_value','Ledger','money'],['stock_log_value','Stock Log','money'],['gl_value','General Ledger','money'],['difference','Selisih','money'],['barcode_variance_count','Selisih Barcode','qty'],['cache_variance_count','Selisih Cache','qty'],['status','Status','status']]}
};
let current=[],dataTable=null;
$('#reportItem').select2({width:'100%',placeholder:'Semua / cari barang',allowClear:true,ajax:{url:'<?=base_url('inventoryprofessional/lookup_items')?>',dataType:'json',delay:250,data:p=>({q:p.term||'',warehouse_id:$('#reportWarehouse').val()}),processResults:r=>({results:r.results})}});
function params(){return{type:$('#reportType').val(),warehouse_id:$('#reportWarehouse').val(),item_id:$('#reportItem').val(),date_from:$('#dateFrom').val(),date_to:$('#dateTo').val(),days:$('#reportDays').val()}}
function fail(x){AppAlert.toast(x.responseJSON&&x.responseJSON.message||'Laporan tidak dapat dimuat.','error')}
function cell(value,kind,key,row){if(key==='document_no'&&row.document_id&&['opening','in','out','transfer','adjustment','opname','damaged','lost'].includes(row.document_type))return'<a class="fw-semibold" href="<?=base_url('inventoryprofessional/operations')?>?focus='+encodeURIComponent(row.document_id)+'">'+esc(value)+'</a>';if(kind==='money')return money(value);if(kind==='qty')return qty(value);if(kind==='status')return'<span class="status-chip status-'+esc(value)+'">'+esc(String(value||'-').replaceAll('_',' '))+'</span>';return esc(value)}
function render(type,rows){current=rows;const def=reports[type];if(dataTable){dataTable.destroy();dataTable=null}$('#reportTable thead').html('<tr>'+def.cols.map(c=>'<th>'+esc(c[1])+'</th>').join('')+'</tr>');$('#reportTable tbody').html(rows.map(r=>'<tr>'+def.cols.map(c=>'<td class="'+(c[2]==='money'||c[2]==='qty'?'text-end':'')+'">'+cell(r[c[0]],c[2],c[0],r)+'</td>').join('')+'</tr>').join('')||'<tr><td colspan="'+def.cols.length+'" class="text-center text-muted py-5">Tidak ada data pada filter yang dipilih.</td></tr>');if(rows.length)dataTable=$('#reportTable').DataTable({responsive:true,pageLength:25,order:[],searchDelay:250,language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}})}
function load(){let type=$('#reportType').val(),b=$('#loadReport').prop('disabled',true);$('#reportTitle').text(reports[type].title);$('#reportTable tbody').html('<tr><td class="text-center py-5"><span class="spinner-border spinner-border-sm"></span> Memuat laporan...</td></tr>');$.get('<?=base_url('inventoryprofessional/report_data')?>',params()).done(r=>render(type,r.data)).fail(fail).always(()=>b.prop('disabled',false))}
$('#reportType').on('change',function(){let type=this.value;$('.slow-filter').toggleClass('d-none',type!=='slow');$('.item-filter').toggleClass('d-none',type==='reconciliation');$('.date-filter').toggleClass('d-none',['valuation','location','barcode','batch','reorder','slow'].includes(type));load()});$('#loadReport').on('click',load);
$('#exportPdf').on('click',function(){let q=$.param(params());$('#reportPdfModal .modal-title').text(reports[$('#reportType').val()].title);$('#reportPdfModal iframe').attr('src','<?=base_url('inventoryprofessional/report_pdf')?>?'+q);bootstrap.Modal.getOrCreateInstance(document.getElementById('reportPdfModal')).show()});
$('#exportExcel').on('click',function(){window.location.href='<?=base_url('inventoryprofessional/report_excel')?>?'+$.param(params())});
function issueList(details){let rows=[];(details.ledger_vs_stock_log||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>'+esc(x.warehouse_name)+' · Selisih qty '+qty(x.qty_difference)+' · Selisih nilai '+money(x.value_difference)+'</span></li>'));(details.barcode||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>'+esc(x.warehouse_name)+' · Ledger '+qty(x.ledger_qty)+' · Barcode '+qty(x.barcode_qty)+' · Selisih '+qty(x.difference)+'</span></li>'));(details.cache||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>Cache '+qty(x.cache_qty)+' · Ledger '+qty(x.ledger_qty)+'</span></li>'));return rows.length?'<div class="alert alert-warning py-2"><strong>Sumber anomali</strong><ul class="timeline-list mt-2 mb-0">'+rows.slice(0,12).join('')+'</ul></div>':'<div class="alert alert-success py-2">Tidak ada rincian anomali operasional.</div>'}
$('#runReconciliation').on('click',function(){let b=$(this).prop('disabled',true);$.post('<?=base_url('inventoryprofessional/reconciliation')?>',Object.assign({date:$('#reconcileDate').val()},csrf())).done(r=>{let x=r.result;$('#reconcileResult').html('<div><span>Inventory Ledger</span><strong>'+money(x.ledger_value)+'</strong></div><div><span>Stock Log</span><strong>'+money(x.stock_log_value)+'</strong></div><div><span>General Ledger</span><strong>'+money(x.gl_value)+'</strong></div><div><span>Selisih Ledger–GL</span><strong>'+money(x.difference)+'</strong></div><div><span>Selisih Cache</span><strong>'+qty(x.cache_variance_count)+' barang</strong></div><div><span>Selisih Barcode</span><strong>'+qty(x.barcode_variance_count)+' posisi</strong></div><div><span>Status</span><strong><span class="status-chip status-'+(x.status==='matched'?'approved':'rejected')+'">'+esc(x.status)+'</span></strong></div>');$('#reconcileIssues').html(issueList(x.details||{}));AppAlert.toast(r.message,x.status==='matched'?'success':'warning')}).fail(fail).always(()=>b.prop('disabled',false))});
load();
});
</script>
+6 -2
View File
@@ -137,6 +137,7 @@
<label for="harga" class="form-label">Harga</label>
<input type="text" id="harga" class="form-control format-rupiah" min="0">
</div>
<div class="row g-2 mb-2"><div class="col"><label for="discount_amount" class="form-label">Diskon Nominal</label><input type="number" id="discount_amount" class="form-control" min="0" step="0.01" value="0"></div><div class="col"><label for="tax_rate" class="form-label">Pajak (%)</label><input type="number" id="tax_rate" class="form-control" min="0" step="0.01" value="0"></div></div>
<!-- CICILAN -->
<div class="mb-3 mt-3" id="is_cicilan_group">
@@ -689,7 +690,9 @@ $(function(){
nama_item: nama_item,
item_id: item_id || null,
qty: parseFloat($('#qty').val()) || 0,
harga: parseFloat($('#harga').val()) || 0,
harga: window.parseMoney($('#harga').val()),
discount_amount: window.parseMoney($('#discount_amount').val()),
tax_rate: parseFloat($('#tax_rate').val()) || 0,
keterangan: $('#keterangan').val().trim(),
barcode_id: $('#barcode_select').val() || 0,
@@ -1210,4 +1213,5 @@ $(function(){
$(this).addClass('table-secondary');
});
</script>
</script>
<div class="container pb-4"><div class="row g-3"><div class="col-md-6"><div class="card"><div class="card-body"><h6>Lampiran Invoice</h6><form method="post" enctype="multipart/form-data" action="<?=base_url('receivableattachments/upload')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="entity_type" value="invoice"><input type="hidden" name="entity_id" value="<?=(int)$invoice_id?>"><input class="form-control mb-2" type="file" name="attachment" accept=".pdf,.jpg,.jpeg,.png,.webp" required><button class="btn btn-outline-primary">Unggah Lampiran</button></form></div></div></div><div class="col-md-6"><div class="card"><div class="card-body"><h6>Reminder Tagihan</h6><form method="post" action="<?=base_url('receivablereminders/queue/'.$invoice_id)?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><select class="form-select mb-2" name="channel"><option value="email">Email</option><option value="whatsapp">WhatsApp</option><option value="phone">Telepon</option><option value="manual">Manual</option></select><input class="form-control mb-2" type="datetime-local" name="scheduled_at"><button class="btn btn-outline-secondary">Jadwalkan Reminder</button></form></div></div></div></div></div>
+114
View File
@@ -0,0 +1,114 @@
<?php
$p=$permissions;
$creating=$mode==='create';
$draft=$invoice&&$invoice->workflow_status==='draft';
$editable=$draft&&$p['update'];
$csrfName=$this->security->get_csrf_token_name();
$csrfHash=$this->security->get_csrf_hash();
$lineDate=$invoice?max($invoice->tanggal,date('Y-m-d')):date('Y-m-d');
function invLabel($value){$labels=array('draft'=>'Draft','submitted'=>'Menunggu Approval','approved'=>'Disetujui','posted'=>'Posted','one_time'=>'Sekali Transaksi','running'=>'Berjalan / Akumulasi','accrual'=>'Diakui saat Invoice Diposting','on_payment'=>'Diakui Proporsional Setelah Dibayar','not_delivered'=>'Belum Dikirim','partial'=>'Sebagian Dikirim','delivered'=>'Sudah Dikirim');return$labels[$value]??ucwords(str_replace('_',' ',$value));}
?>
<link rel="stylesheet" href="<?=base_url('assets/css/invoices-professional.css').'?v='.filemtime(FCPATH.'assets/css/invoices-professional.css')?>">
<link rel="stylesheet" href="<?=base_url('assets/css/invoice-editor-modal.css').'?v='.filemtime(FCPATH.'assets/css/invoice-editor-modal.css')?>">
<section class="invoice-workspace invoice-editor-page p-0">
<header class="invoice-hero">
<div><span><?=$creating?'INVOICE BARU':'KELOLA INVOICE'?></span><h5><?=$creating?'Buat Invoice':html_escape($invoice->no_invoice)?></h5><p><?=$creating?'Tentukan customer dan aturan penagihan. Item dapat ditambahkan setelah Draft tersimpan.':'Item dapat ditambahkan bertahap dan langsung tersimpan pada Draft invoice.'?></p></div>
<div class="hero-actions"><a class="btn btn-light" href="<?=base_url('invoices')?>"><i class="bi bi-arrow-left"></i> Daftar Invoice</a><?php if($invoice&&$p['export']):?><button class="btn btn-outline-danger open-invoice-pdf" data-url="<?=base_url('invoices/pdf/'.$invoice->id)?>"><i class="bi bi-file-pdf"></i> PDF</button><?php endif?></div>
</header>
<?php if($invoice):?><div class="invoice-progress"><span class="done"><i class="bi bi-check-lg"></i> Header Draft</span><span class="<?=$invoice->delivery_status==='delivered'?'done':''?>"><i class="bi bi-truck"></i> <?=$invoice->inventory_line_count?'Pengiriman':'Tanpa Surat Jalan'?></span><span class="<?=in_array($invoice->workflow_status,array('submitted','approved','posted'),true)?'done':''?>"><i class="bi bi-send"></i> Diajukan</span><span class="<?=$invoice->workflow_status==='posted'?'done':''?>"><i class="bi bi-patch-check"></i> Approved & Posted</span></div><?php endif?>
<div class="invoice-editor-grid <?=$creating?'is-creating':''?>">
<main class="invoice-editor-main">
<div class="card editor-card item-list-card">
<div class="card-header"><div><b>Daftar Item Invoice</b><small><?=$creating?'Daftar item akan aktif setelah invoice disimpan.':'Data dibaca langsung dari database dan aman untuk invoice akumulasi.'?></small></div><?php if($invoice):?><span id="lineCountBadge" class="count-badge">0 item</span><?php endif?></div>
<div class="card-body <?=$creating?'empty-editor':''?>">
<?php if($creating):?><div class="invoice-empty"><i class="bi bi-receipt-cutoff"></i><h5>Invoice belum disimpan</h5><p>Lengkapi data invoice pada panel sebelah kiri.</p></div><?php else:?><div class="table-responsive"><table class="table align-middle w-100" id="invoiceLinesTable"><thead><tr><th>Tanggal</th><th>Barang / Jasa / Tabungan</th><th>Alokasi Akun</th><th>Barcode / Serial</th><th class="text-end">Qty</th><th class="text-end">Harga</th><th class="text-end">Diskon</th><th class="text-end">Pajak</th><th class="text-end">Subtotal</th><th>Aksi</th></tr></thead></table></div><?php endif?>
</div>
<?php if($invoice):?><div class="invoice-summary"><div><span>Subtotal Bersih</span><b id="summaryNet">Rp 0,00</b></div><div><span>Diskon</span><b id="summaryDiscount">Rp 0,00</b></div><div><span>Pajak</span><b id="summaryTax">Rp 0,00</b></div><div class="grand"><span>Total Invoice</span><strong id="summaryTotal">Rp 0,00</strong></div></div><?php endif?>
</div>
</main>
<aside class="invoice-editor-sidebar">
<div class="invoice-editor-sidebar-inner">
<?php if($creating):?>
<div class="card editor-card customer-rule-card">
<div class="card-header"><div><b>Customer & Aturan Invoice</b><small>Header tersimpan sebagai Draft tanpa jurnal dan mutasi stok.</small></div></div>
<div class="card-body"><form id="invoiceHeaderForm" data-ajax="true">
<input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><input type="hidden" name="idempotency_key" value="<?=bin2hex(random_bytes(20))?>"><input type="hidden" name="source_sales_order_id" value="<?=$source_order?:''?>">
<div class="mb-3"><label class="form-label">Customer <span class="text-danger">*</span></label><select class="form-select ajax-customer" name="customer_id" required></select><div id="selectedCustomerPreview" class="selected-customer-preview d-none"></div></div>
<input type="hidden" name="recognition_policy" value="on_payment">
<div class="alert alert-light border mb-3"><small><b>Pengakuan pendapatan:</b> dilakukan proporsional saat pembayaran invoice diterima.</small></div>
<div class="row g-3"><div class="col-md-6"><label class="form-label">Tanggal Invoice</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-6"><label class="form-label">Jatuh Tempo</label><input class="form-control" type="date" name="due_date" value="<?=date('Y-m-d')?>" required></div></div>
<div class="mt-3"><label class="form-label">Catatan</label><textarea class="form-control" name="notes" rows="3" placeholder="Catatan atau aturan penagihan untuk customer"></textarea></div>
<button class="btn btn-primary w-100 mt-3" type="submit"><i class="bi bi-save"></i> Simpan Invoice & Lanjut</button>
</form></div>
</div>
<?php else:?>
<div class="card editor-card customer-rule-card">
<div class="card-header"><div><b>Detail Customer</b><small>Informasi customer dan aturan invoice</small></div><div class="d-flex gap-2 align-items-center"><?php if($editable&&!(int)$invoice->line_count):?><button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#headerEditModal"><i class="bi bi-pencil"></i> Koreksi</button><?php endif?><span class="invoice-status status-<?=html_escape($invoice->workflow_status)?>"><?=invLabel($invoice->workflow_status)?></span></div></div>
<div class="card-body">
<h5><?=html_escape($invoice->customer_name)?></h5><p class="text-muted mb-3"><?=nl2br(html_escape($invoice->customer_address?:'-'))?><br><?=html_escape($invoice->customer_phone?:'-')?> · <?=html_escape($invoice->customer_email?:'-')?></p>
<dl class="invoice-rule-list"><div><dt>Tanggal / Jatuh Tempo</dt><dd><?=html_escape($invoice->tanggal.' / '.$invoice->jatuh_tempo)?></dd></div><div><dt>Pengakuan</dt><dd><?=invLabel($invoice->recognition_policy)?></dd></div><div><dt>Pengiriman</dt><dd><?=$invoice->inventory_line_count?invLabel($invoice->delivery_status):'Tidak Memerlukan Surat Jalan'?></dd></div><div><dt>Pembuat</dt><dd><?=html_escape($invoice->creator_name?:'-')?></dd></div><?php if($invoice->approved_by):?><div><dt>Disetujui oleh</dt><dd><?=html_escape($invoice->approver_name?:'-')?></dd></div><?php endif?></dl>
<small class="auto-rule-note"><i class="bi bi-magic"></i> Jenis dan periode invoice ditentukan otomatis dari tanggal item.</small>
<?php if($invoice->rejection_reason):?><div class="alert alert-danger mt-3 mb-0"><b>Alasan penolakan</b><br><?=nl2br(html_escape($invoice->rejection_reason))?></div><?php endif?><?php if($invoice->keterangan):?><div class="invoice-note"><small>Catatan</small><?=nl2br(html_escape($invoice->keterangan))?></div><?php endif?>
</div>
</div>
<?php if($editable):?><button class="btn btn-primary btn-lg w-100 add-invoice-item-button" type="button" data-bs-toggle="modal" data-bs-target="#invoiceItemModal"><i class="bi bi-plus-lg"></i> Tambah Item</button><?php if((int)$invoice->pending_delivery_count>0):?><button class="btn btn-outline-primary w-100 generate-delivery"><i class="bi bi-truck"></i> Buat Surat Jalan</button><?php endif?><?php endif?>
<?php endif?>
<?php if($invoice):?><div class="card editor-card invoice-sidebar-actions"><div class="card-body"><div class="sidebar-next-status"><small>Status selanjutnya</small><b><?=$draft?($invoice->pending_delivery_count?'Buat dan posting Surat Jalan, lalu ajukan invoice.':'Ajukan invoice untuk approval.'):invLabel($invoice->workflow_status)?></b></div><div class="sidebar-action-buttons"><?php if($draft&&$p['submit']):?><button class="btn btn-primary submit-invoice"><i class="bi bi-send"></i> Ajukan Invoice</button><?php endif?><?php if($draft&&$p['delete']):?><button class="btn btn-outline-danger delete-invoice"><i class="bi bi-trash"></i> Batalkan Draft</button><?php endif?><?php if($invoice->workflow_status==='submitted'&&$p['approve']):?><button class="btn btn-success approve-invoice"><i class="bi bi-check-lg"></i> Setujui & Posting</button><button class="btn btn-outline-danger reject-invoice"><i class="bi bi-x-lg"></i> Tolak</button><?php endif?><?php if($invoice->workflow_status==='posted'&&in_array($invoice->status,array('unpaid','partial'),true)&&$p['post']):?><a class="btn btn-warning" href="<?=base_url('receivables?invoice_id='.$invoice->id)?>"><i class="bi bi-cash-coin"></i> Terima Pembayaran</a><?php endif?></div></div></div><?php endif?>
</div>
</aside>
</div>
</section>
<?php if($invoice&&$editable):?>
<div class="modal fade invoice-item-modal" id="invoiceItemModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><form id="invoiceLineForm" data-ajax="true">
<div class="modal-header"><div><h5 class="modal-title">Tambah Beberapa Item Invoice</h5><small>Susun maksimal 100 barang, jasa, atau tabungan dalam satu kali penyimpanan.</small></div><button class="btn-close" type="button" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><input type="hidden" name="idempotency_key" id="lineIdempotency" value="<?=bin2hex(random_bytes(20))?>">
<section class="invoice-batch-common">
<div class="row g-3 align-items-end">
<div class="col-sm-6"><label class="form-label">Tanggal Transaksi <span class="text-danger">*</span></label><input class="form-control" type="date" name="service_date" value="<?=html_escape($lineDate)?>" required><small class="form-text">Satu tanggal untuk seluruh item dalam penambahan ini.</small></div>
<div class="col-sm-6"><label class="form-label">Diskon Penambahan</label><input class="form-control money-input" name="batch_discount" inputmode="decimal" value="0"><small class="form-text">Dialokasikan proporsional ke semua item.</small></div>
</div>
</section>
<div class="invoice-batch-composer">
<input type="hidden" name="editing_line_id" value="">
<div class="line-type-switch mb-3"><label><input type="radio" name="line_type" value="inventory" checked><span><i class="bi bi-box-seam"></i> Barang Gudang</span></label><label><input type="radio" name="line_type" value="service"><span><i class="bi bi-tools"></i> Jasa / Nonbarang</span></label><label><input type="radio" name="line_type" value="savings"><span><i class="bi bi-piggy-bank"></i> Tabungan</span></label></div>
<div class="row g-4">
<div class="col-lg-6">
<div class="mb-3 sales-account-field"><label class="form-label">Akun Pendapatan Item <span class="text-danger">*</span></label><select class="form-select ajax-account" name="revenue_account_id"></select><small class="form-text">Dapat berbeda untuk barang dan jasa berikutnya.</small></div>
<div class="inventory-only"><div class="mb-3"><label class="form-label">Gudang <span class="text-danger">*</span></label><select class="form-select ajax-warehouse" name="warehouse_id"></select></div><div class="mb-3"><label class="form-label">Barang <span class="text-danger">*</span></label><select class="form-select ajax-item" name="item_id"></select><small id="trackingHint" class="form-text">Pilih gudang dan barang terlebih dahulu.</small></div></div>
<div class="noninventory-only d-none"><div class="mb-3"><label class="form-label" id="nonInventoryDescriptionLabel">Nama Jasa / Deskripsi <span class="text-danger">*</span></label><input class="form-control" name="description" placeholder="Tuliskan deskripsi transaksi"></div><div class="service-unit-field"><label class="form-label">Satuan</label><input class="form-control" name="unit" value="Jasa"></div></div>
<div class="savings-only d-none mt-3"><label class="form-label">Akun Simpanan Customer (Passiva) <span class="text-danger">*</span></label><select class="form-select ajax-liability-account" name="savings_account_id"></select></div>
</div>
<div class="col-lg-6">
<div class="inventory-only barcode-panel mb-3"><label class="form-label">Barcode / Serial</label><div class="barcode-scan"><input class="form-control" id="barcodeScan" placeholder="Scan atau ketik barcode/serial exact" autocomplete="off"><button class="btn btn-outline-primary" type="button" id="scanBarcodeButton"><i class="bi bi-qr-code-scan"></i></button></div><select class="form-select ajax-barcode mt-2" id="barcodeSelect" multiple></select><div id="selectedBarcodes" class="selected-barcodes"></div></div>
<div class="row g-3"><div class="col-6 line-qty-field"><label class="form-label">Qty</label><input class="form-control" name="qty" inputmode="decimal" value="1" required></div><div class="col-6 line-value-field"><label class="form-label" id="lineValueLabel">Harga Satuan / Nominal</label><input class="form-control money-input" name="unit_price" inputmode="decimal" required></div><div class="col-6 sales-tax-field"><label class="form-label">Pajak (%)</label><input class="form-control" name="tax_rate" type="number" min="0" step="0.01" value="0"></div><div class="col-6 d-flex align-items-end stage-line-field"><button class="btn btn-outline-primary w-100" type="button" id="stageInvoiceLine"><i class="bi bi-plus-lg"></i> Masukkan ke Daftar</button></div></div>
<div class="mt-3"><label class="form-label">Keterangan</label><textarea class="form-control" name="notes" rows="2" placeholder="Rincian tambahan barang atau jasa"></textarea></div>
</div>
</div>
</div>
<section class="invoice-batch-list mt-4">
<div class="d-flex justify-content-between align-items-center mb-2"><div><b>Item yang Akan Ditambahkan</b><small>Belum tersimpan sebelum tombol simpan ditekan.</small></div><span class="count-badge" id="batchLineCount">0 item</span></div>
<div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Barang / Jasa / Tabungan</th><th>Alokasi Akun</th><th>Jenis</th><th class="text-end">Qty</th><th class="text-end">Harga</th><th class="text-end">Pajak</th><th class="text-end">Bruto</th><th class="text-center">Aksi</th></tr></thead><tbody id="invoiceBatchLines"><tr class="batch-empty-row"><td colspan="8">Belum ada item. Isi form di atas lalu klik “Masukkan ke Daftar”.</td></tr></tbody></table></div>
</section>
</div>
<div class="modal-footer invoice-item-footer"><div class="batch-live-summary"><span>Bruto <b id="batchGrossTotal">Rp 0,00</b></span><span>Diskon <b id="batchDiscountTotal">Rp 0,00</b></span><span>Total + Pajak <strong id="lineLiveTotal">Rp 0,00</strong></span></div><div><button class="btn btn-light" type="button" data-bs-dismiss="modal">Tutup</button><button class="btn btn-primary batch-submit-button" type="submit" disabled><i class="bi bi-save"></i> Simpan 0 Item</button></div></div>
</form></div></div></div>
<?php endif?>
<div class="modal fade" id="invoicePdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5>Pratinjau Invoice</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau Invoice"></iframe></div></div></div></div>
<?php if($invoice&&$editable&&!(int)$invoice->line_count):?>
<div class="modal fade" id="headerEditModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"><div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><form id="invoiceHeaderEditForm" data-ajax="true">
<div class="modal-header"><h5>Koreksi Data Invoice</h5><button class="btn-close" type="button" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><input type="hidden" name="recognition_policy" value="on_payment"><div class="mb-3"><label class="form-label">Customer</label><select class="form-select ajax-customer-edit" name="customer_id"><option value="<?=$invoice->customer_id?>" selected><?=html_escape($invoice->customer_name)?></option></select></div><div class="row g-3"><div class="col-md-6"><label class="form-label">Tanggal Invoice</label><input class="form-control" type="date" name="date" value="<?=$invoice->tanggal?>" required></div><div class="col-md-6"><label class="form-label">Jatuh Tempo</label><input class="form-control" type="date" name="due_date" value="<?=$invoice->jatuh_tempo?>" required></div></div><div class="mt-3"><label class="form-label">Catatan</label><textarea class="form-control" name="notes" rows="3"><?=html_escape($invoice->keterangan)?></textarea></div></div>
<div class="modal-footer"><button class="btn btn-light" type="button" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" type="submit">Simpan Koreksi</button></div>
</form></div></div></div>
<?php endif?>
<script>window.INVOICE_PAGE=<?=json_encode(array('base'=>base_url(),'mode'=>$mode,'invoiceId'=>$invoice?(int)$invoice->id:0,'csrfName'=>$csrfName,'csrfHash'=>$csrfHash,'permissions'=>$p,'sourceOrder'=>(int)$source_order),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/invoices-professional.js').'?v='.filemtime(FCPATH.'assets/js/invoices-professional.js')?>"></script>
+36
View File
@@ -0,0 +1,36 @@
<?php $p=$permissions;$csrfName=$this->security->get_csrf_token_name();$csrfHash=$this->security->get_csrf_hash(); ?>
<link rel="stylesheet" href="<?=base_url('assets/css/invoices-professional.css').'?v='.filemtime(FCPATH.'assets/css/invoices-professional.css')?>">
<section class="invoice-workspace">
<header class="invoice-hero">
<div><span>SIKLUS PENJUALAN</span><h2>Invoice Penjualan</h2><p>Kelola tagihan customer, pengiriman barang bila diperlukan, dan penerimaan pembayaran.</p></div>
<?php if($p['create']):?><a class="btn btn-primary" href="<?=base_url('invoices/create')?>"><i class="bi bi-plus-lg"></i> Buat Invoice</a><?php endif?>
</header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan">
<a href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a>
<a class="active" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span><?php if($summary['draft']):?><b><?=$summary['draft']?></b><?php endif?></a>
<a href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span><?php if($summary['delivery']):?><b><?=$summary['delivery']?></b><?php endif?></a>
<a href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span><?php if($summary['open_invoice']):?><b><?=$summary['open_invoice']?></b><?php endif?></a>
<a href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span><?php if($summary['return']):?><b><?=$summary['return']?></b><?php endif?></a>
<a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a>
</nav>
<div class="invoice-metrics">
<article><small>Penjualan bulan ini</small><strong>Rp <?=number_format($summary['month_sales'],2,',','.')?></strong></article>
<article><small>Invoice belum lunas</small><strong><?=$summary['open_invoice']?></strong><span><?=$summary['overdue']?> sudah jatuh tempo</span></article>
<article><small>Draft / approval</small><strong><?=$summary['draft']?></strong><span>Memerlukan tindakan</span></article>
<article><small>Pengiriman Draft</small><strong><?=$summary['delivery']?></strong><span>Belum mengurangi stok</span></article>
</div>
<div class="card invoice-list-card">
<div class="card-header"><div><b>Daftar Invoice</b><small>Gunakan filter untuk menemukan invoice tanpa memuat seluruh data.</small></div><button class="btn btn-sm btn-outline-secondary" id="toggleInvoiceFilters"><i class="bi bi-funnel"></i> Filter</button></div>
<div class="invoice-filters" id="invoiceFilters">
<div><label>Status proses</label><select class="form-select" id="filterWorkflow"><option value="">Semua</option><option value="draft">Draft</option><option value="submitted">Menunggu Approval</option><option value="posted">Posted</option></select></div>
<div><label>Status pembayaran</label><select class="form-select" id="filterPayment"><option value="">Semua</option><option value="unpaid">Belum Lunas</option><option value="partial">Sebagian</option><option value="paid">Lunas</option></select></div>
<div><label>Jatuh tempo</label><select class="form-select" id="filterDue"><option value="">Semua</option><option value="not_due">Belum jatuh tempo</option><option value="overdue">Sudah jatuh tempo</option></select></div>
<div><label>Dari tanggal</label><input class="form-control" type="date" id="filterFrom"></div><div><label>Sampai tanggal</label><input class="form-control" type="date" id="filterTo"></div>
<div class="filter-action"><button class="btn btn-primary" id="applyInvoiceFilter">Terapkan</button><button class="btn btn-light" id="resetInvoiceFilter">Reset</button></div>
</div>
<div class="table-responsive"><table class="table align-middle w-100" id="invoiceTable"><thead><tr><th>Invoice / Customer</th><th>Jatuh Tempo</th><th>Pengakuan</th><th class="text-end">Total</th><th class="text-end">Terbayar</th><th class="text-end">Sisa</th><th>Status</th><th>Pengiriman</th><th>Dibuat oleh</th><th>Aksi</th></tr></thead></table></div>
</div>
</section>
<div class="modal fade" id="invoicePdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Pratinjau Invoice</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau Invoice"></iframe></div></div></div></div>
<script>window.INVOICE_PAGE=<?=json_encode(array('base'=>base_url(),'mode'=>'list','csrfName'=>$csrfName,'csrfHash'=>$csrfHash,'permissions'=>$p),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/invoices-professional.js').'?v='.filemtime(FCPATH.'assets/js/invoices-professional.js')?>"></script>
+12 -3
View File
@@ -243,7 +243,7 @@ $(document).on('input','.qty,.harga',function(){
let tr = $(this).closest('tr');
let qty = parseFloat(tr.find('.qty').val()) || 0;
let harga = parseFloat(tr.find('.harga').val()) || 0;
let harga = window.parseMoney(tr.find('.harga').val());
let sub = qty * harga;
@@ -385,7 +385,7 @@ function hitungBayar(){
let totalBayar = 0;
$('#tableBayar .debit').each(function(){
totalBayar += parseFloat($(this).val()) || 0;
totalBayar += window.parseMoney($(this).val());
});
let invoiceTotal = parseFloat($('#invoiceTotal').text().replace(/\./g,'')) || 0;
@@ -423,7 +423,7 @@ $('#btnProsesBayar').click(function(){
$('#tableBayar tbody tr').each(function(){
let account_id = $(this).find('.account_id').val();
let debit = parseFloat($(this).find('.debit').val()) || 0;
let debit = window.parseMoney($(this).find('.debit').val());
if(account_id && debit > 0){
detail.push({ account_id, amount: debit });
@@ -570,6 +570,15 @@ $(document).on('click', '.btn-delete', function () {
});
</script>
<script>
async function stage5InvoiceAction(action,id){
if(!confirm('Lanjutkan tindakan '+action+' untuk invoice ini?')) return;
const routes={submit:'submit_invoice',approve:'approve_invoice',post:'post_invoice'};
const body=new URLSearchParams(); body.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');
const response=await fetch('<?=base_url('receivables/')?>'+routes[action]+'/'+id,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body});
const result=await response.json(); alert(result.message); if(result.status) location.reload();
}
</script>
+54 -92
View File
@@ -1,3 +1,4 @@
<?php $inventory_title='Kelola Barcode & Serial';$inventory_subtitle='Lengkapi serial unit pending sebelum diaktifkan.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<style>
body {
background: #f5f7fb;
@@ -21,13 +22,13 @@ body {
.header-gradient {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff;
padding: 22px 28px;
padding: 14px 28px;
}
.info-box {
background: rgba(255,255,255,.18);
border-radius: 12px;
padding: 12px 16px;
padding: 10px 16px;
}
.progress {
@@ -140,10 +141,10 @@ body {
<div class="header-gradient">
<div class="row align-items-center">
<div class="col-md-8">
<h3 class="mb-1">
<h5 class="mb-1">
<i class="fa fa-barcode"></i>
Input Serial Number
</h3>
</h5>
<div class="small">
Silahkan scan Serial Number perangkat.
Data akan otomatis tersimpan.
@@ -154,9 +155,9 @@ body {
<div class="small">
Progress
</div>
<h5 class="font-weight-bold mb-2" id="progress_text">
<h6 class="font-weight-bold mb-2" id="progress_text">
0 / 0 Barcode
</h5>
</h6>
<div class="progress">
<div class="progress-bar bg-success" style="width:35%"></div>
</div>
@@ -259,13 +260,21 @@ body {
<div class="col-lg-8">
<div class="card table-card">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
<h5 class="mb-0">
Daftar Barcode
</h5>
<span class="badge badge-warning px-3 py-2">
20 Data
</span>
<div class="d-flex align-items-center gap-2">
<input type="search" id="barcodeSearch" class="form-control form-control-sm" placeholder="Cari barcode / SN" autocomplete="off">
<span class="badge badge-warning px-3 py-2" id="barcodeCount">0 Data</span>
</div>
</div>
</div>
<div class="card-footer bg-white d-flex justify-content-between align-items-center">
<small id="barcodePageInfo" class="text-muted">Memuat data...</small>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" id="barcodePrev">Sebelumnya</button>
<button type="button" class="btn btn-outline-secondary" id="barcodeNext">Berikutnya</button>
</div>
</div>
<div class="table-responsive">
@@ -277,7 +286,7 @@ body {
<th>Barcode</th>
<th>Serial Number</th>
<th width="120">Status</th>
<th width="80" class="text-center">Action</th>
<th width="150" class="text-center">Action</th>
</tr>
</thead>
<tbody>
@@ -313,9 +322,18 @@ body {
let ITEM_ID = `<?= $item_id; ?>` ;
let isSaving = false;
let barcodeStart = 0;
const barcodeLength = 50;
let barcodeSearchTimer = null;
$(document).ready(function () {
loadData();
$("#barcodeSearch").on("input", function () {
clearTimeout(barcodeSearchTimer);
barcodeSearchTimer = setTimeout(function () { barcodeStart = 0; loadData(); }, 300);
});
$("#barcodePrev").on("click", function () { barcodeStart = Math.max(0, barcodeStart - barcodeLength); loadData(); });
$("#barcodeNext").on("click", function () { barcodeStart += barcodeLength; loadData(); });
$("#serial_number").focus();
$("#serial_number").on("keypress", function (e) {
if (e.which == 13) {
@@ -328,7 +346,11 @@ $(document).ready(function () {
});
function loadData() {
$.getJSON( "<?= base_url(); ?>" + "items/load_data_barcode/" + ITEM_ID)
$.getJSON( "<?= base_url(); ?>" + "items/load_data_barcode/" + ITEM_ID, {
start: barcodeStart,
length: barcodeLength,
search: $("#barcodeSearch").val() || ""
})
.done(function (r) {
render(r);
})
@@ -360,7 +382,7 @@ function saveSerial() {
},
success: function (r) {
isSaving = false;
render(r);
if (r.status) loadData(); else render(r);
},
error: function () {
isSaving = false;
@@ -455,18 +477,20 @@ function render(r) {
// =======================
let html = "";
const escapeHtml = function (value) { return $("<div>").text(value == null ? "" : String(value)).html(); };
$.each(r.list, function (i, v) {
html += "<tr>";
html += "<td>" + (i + 1) + "</td>";
html += "<td><strong>" + v.barcode + "</strong></td>";
html += "<td>" + (v.serial_number ? v.serial_number : "-") + "</td>";
html += "<td>" + (r.start + i + 1) + "</td>";
html += "<td><strong>" + escapeHtml(v.barcode) + "</strong></td>";
html += "<td>" + escapeHtml(v.serial_number ? v.serial_number : "-") + "</td>";
html += "<td>";
if (v.serial_number) {
html += '<span class="badge-success-soft"><i class="fa fa-check mr-1"></i>Selesai</span>';
if (v.status === 'available') {
html += '<span class="badge-success-soft"><i class="fa fa-check mr-1"></i>Aktif</span>';
} else if (v.serial_number) {
html += '<span class="badge-warning-soft">Siap Diposting</span>';
} else {
html += '<span class="badge-warning-soft">Menunggu</span>';
}
@@ -475,14 +499,12 @@ function render(r) {
html += '<td class="text-center">';
if (r.item_status == "draft") {
html += '<button class="btn btn-sm btn-outline-danger btnDelete" data-id="' + v.id + '">Delete</button>';
if (v.status === 'available') {
html += '<button class="btn btn-sm btn-outline-secondary" disabled>Sudah Aktif</button>';
} else if (v.serial_number) {
html += '<button class="btn btn-sm btn-outline-danger btnDelete" data-id="' + v.id + '">Hapus SN</button>';
} else {
html += '<button class="btn btn-sm btn-outline-secondary" disabled>Locked</button>';
html += '<button class="btn btn-sm btn-outline-secondary" disabled>Belum Siap</button>';
}
html += "</td>";
@@ -491,75 +513,15 @@ function render(r) {
});
$("#tableBarcode tbody").html(html);
$("#barcodeCount").text(r.total + " Data");
const shownTo = Math.min(r.start + r.list.length, r.filtered);
$("#barcodePageInfo").text(r.filtered ? ((r.start + 1) + "–" + shownTo + " dari " + r.filtered) : "Tidak ada data");
$("#barcodePrev").prop("disabled", r.start <= 0);
$("#barcodeNext").prop("disabled", r.start + r.length >= r.filtered);
}
// function render(r) {
// if (!r.status) {
// $("#serial_number")
// .val("")
// .prop("disabled", false)
// .focus();
// Swal.fire({
// icon: "error",
// title: "Oops...",
// text: r.message
// });
// return;
// }
// if (r.next == null) {
// $("#barcode_id").val("");
// $(".barcode-code").html("SELESAI");
// $("#serial_number")
// .val("")
// .prop("disabled", true);
// $(".progress-bar")
// .css("width", "100%");
// $("#progress_text").html(r.done + " / " + r.total);
// Swal.fire({
// icon: "success",
// title: "Selesai",
// text: "Semua Serial Number berhasil diinput."
// });
// return;
// }
// // Barcode Aktif
// $("#barcode_id").val(r.next.id);
// $(".barcode-code").text(r.next.barcode);
// // Progress
// $("#progress_text").text(r.done + " / " + r.total);
// $(".progress-bar")
// .css("width", r.percent + "%");
// // Table
// let html = "";
// $.each(r.list, function (i, v) {
// html += "<tr>";
// html += "<td>" + (i + 1) + "</td>";
// html += "<td><strong>" + v.barcode + "</strong></td>";
// html += "<td>" + (v.serial_number ? v.serial_number : "-") + "</td>";
// html += "<td>";
// if (v.serial_number) {
// html += '<span class="badge-success-soft"><i class="fa fa-check mr-1"></i>Selesai</span>';
// } else {
// html += '<span class="badge-warning-soft">Menunggu</span>';
// }
// html += "</td>";
// html += '<td class="text-center">';
// html += '<button class="btn btn-sm btn-outline-danger btnDelete" data-id="' + v.id + '">';
// html += 'Delete';
// html += '</button>';
// html += '</td>';
// html += "</tr>";
// });
// $("#tableBarcode tbody").html(html);
// // Reset Input
// $("#serial_number")
// .val("")
// .prop("disabled", false)
// .focus();
// }
$(document).on("click", ".btnDelete", function () {
let id = $(this).data("id");
Swal.fire({
@@ -576,7 +538,7 @@ $(document).on("click", ".btnDelete", function () {
barcode_id: id
},
function (r) {
render(r);
if (r.status) loadData(); else render(r);
},
"json"
);
+13 -878
View File
@@ -1,881 +1,16 @@
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<h5>Data Barang Dalam Perjalanan</h5>
<div class="text-end">
<button class="btn btn-warning btn-add btn-add-item mr-2">Tambah Barang</button>
</div>
</div>
<div class="table-responsive">
<table id="tableItems" class="table modern-table w-100">
<thead>
<tr>
<th>No</th>
<th>Tanggal Beli</th>
<th>Kode Barang</th>
<th>Nama</th>
<th style="max-width: 150px;">Gudang</th>
<th>Stok</th>
<th>Harga Beli</th>
<th>Harga Jual</th>
<th>Create at</th>
<th>Aksi</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
<!-- MODAL -->
<div class="modal fade" id="modalItem">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content">
<div class="modal-header bg-warning text-white">
<h5>Item</h5>
</div>
<div class="modal-body">
<input type="hidden" id="id">
<div class="row">
<div class="col-md-6">
<div id="group_tanggal">
<label>Tanggal Pembelian</label>
<input type="date" id="tanggal_beli" class="form-control">
</div>
</div>
<div class="col-md-6">
<div id="group_kode">
<label class="mt-2">Kode Barang</label>
<select id="kode_barang" class="form-control select-search"></select>
</div>
</div>
</div>
<label>Nama Barang</label>
<input type="text" id="nama_barang" class="form-control">
<div class="row">
<div class="col-md-2">
<div id="group_qty">
<label class="mt-2">Jumlah Barang</label>
<input type="number" id="qty" class="form-control">
</div>
</div>
<div class="col-md-2">
<div id="group_tracking_qty">
<label class="mt-2">Jml / (Roll, Box, Dus)</label>
<input type="number" id="tracking_qty" class="form-control">
</div>
</div>
<div class="col-md-4">
<label class="mt-2">Harga Beli</label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="text" id="harga_beli" class="form-control format-rupiah">
</div>
</div>
<div class="col-md-4">
<label class="mt-2">Harga Jual</label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="text" id="harga_jual" class="form-control format-rupiah">
</div>
</div>
</div>
<div id="group_warehouse">
<label class="mt-2">Gudang</label>
<select id="warehouse_id" class="form-control"></select>
</div>
<!-- <div class="row">
<label class="mt-2">Akun Kas</label>
<div class="col-md-7">
<div id="group_account">
<select id="account_kas" class="form-control"></select>
</div>
</div>
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="text" id="account[]" class="form-control format-rupiah">
</div>
</div>
</div>
<button class="btn btn-warning btn-sm mt-4">Tambah Account</button> -->
<hr>
<label class="font-weight-bold">
Pembayaran
</label>
<div id="accountContainer">
<div class="row account-row mb-2">
<div class="col-md-6">
<select
name="account_kas[]"
class="form-control account_kas">
</select>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Rp</span>
<input
type="text"
name="nominal[]"
class="form-control nominal format-rupiah">
</div>
</div>
<div class="col-md-2">
<button
type="button"
class="btn btn-sm btn-danger btn-remove-account w-100"
disabled>
Hapus
</button>
</div>
</div>
</div>
<div class="mt-2">
<button
type="button"
id="btnTambahAccount"
class="btn btn-warning btn-sm">
+ Tambah Account
</button>
</div>
<div class="mt-3">
<div class="row">
<div class="col-md-4">
<!-- total qty * harga beli -->
<h5>
Total :
<span id="totalHppBarang">
Rp 0
</span>
</h5>
</div>
<div class="col-md-4">
<!-- total accounting -->
<h5>
Total :
<span id="totalAccount">
Rp 0
</span>
</h5>
</div>
<div class="col-md-4">
<!-- balance accounting + total hpp barang -->
<h5>
Total :
<span id="totalAll">
Rp 0
</span>
</h5>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" id="btnSimpan">Simpan</button>
</div>
</div>
</div>
</div>
<!-- MODAL DETAIL -->
<div class="modal fade" id="modalDetail">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-warning text-white">
<h5>Detail Item</h5>
</div>
<div class="modal-body" id="detailContent"></div>
</div>
</div>
</div>
<?php $inventory_title='Barang Pending';$inventory_subtitle='Penerimaan yang masih membutuhkan harga jual, serial number, atau aktivasi.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<div class="alert alert-info border-0"><i class="bi bi-info-circle me-2"></i>Daftar ini hanya berisi barang yang sudah diterima gudang tetapi belum sepenuhnya siap. Master barang dibuat dari proses Purchase Request.</div>
<section class="inventory-card"><div class="inventory-card-head"><div><h5>Pekerjaan Aktivasi Barang</h5><p>Isi harga jual di tabel, lengkapi serial bila diwajibkan, lalu posting unit yang sudah siap.</p></div><a href="<?=base_url('purchases?step=request')?>" class="btn btn-outline-primary"><i class="bi bi-cart"></i> Purchase Workflow</a></div><div class="table-responsive"><table id="tableItems" class="table inventory-table align-middle w-100 mb-0"><thead><tr><th>No</th><th>Tanggal Terima</th><th>Kode</th><th>Barang</th><th>Gudang</th><th>Barcode / Stok</th><th>Harga Beli</th><th>Harga Jual</th><th>Dibuat</th><th>Aksi</th></tr></thead></table></div></section>
<div class="modal fade inventory-modal" id="pendingDetailModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">BARANG PENDING</small><h5 class="modal-title">Detail Penerimaan Barang</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="pendingDetailBody"></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<div class="modal fade inventory-modal" id="barcodePrintModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Preview Barcode</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0" style="height:min(82vh,900px)"><iframe title="Preview Barcode" style="width:100%;height:100%;border:0"></iframe></div></div></div></div>
<script>
$(function(){
let action = 'add';
let selectedItemId = null;
let accountOption = '';
// ================= DATATABLE =================
let table = $('#tableItems').DataTable({
ajax: {
url: "<?= base_url('items/get_data'); ?>",
data: function (d) {
d.s = `draft`;
}
}
const csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null?'-':v).html(),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
const table=$('#tableItems').DataTable({responsive:true,processing:true,serverSide:true,searchDelay:350,ajax:{url:'<?=base_url('items/get_data')?>',data:d=>{d.s='draft';d.server=1}},pageLength:25,language:{search:'Cari:',processing:'Memuat data...',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}},initComplete:function(){let f=new URLSearchParams(location.search).get('focus');if(f)setTimeout(()=>$('.btn-detail[data-id="'+f+'"]').first().click(),300)}});
$(document).on('blur','.pending-sale-price',function(){let n=String(this.value).replace(/\./g,'').replace(',','.');if(Number(n)>0)this.value=Number(n).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2})});
$(document).on('click','.btn-updatePosting',function(){let id=$(this).data('id'),input=$('.pending-sale-price[data-id="'+id+'"]'),sale=Number(String(input.val()).replace(/\./g,'').replace(',','.')),buy=Number(input.data('buy')||0);if(!sale)return AppAlert.toast('Harga jual satuan wajib diisi.','warning');if(sale<buy)return AppAlert.toast('Harga jual tidak boleh lebih rendah dari harga beli.','error');let b=$(this);AppAlert.confirm({title:'Posting barang siap digunakan?',text:'Hanya barcode yang serialnya sudah lengkap yang akan diaktifkan.',confirmButtonText:'Posting Barang'}).then(r=>{if(!r.isConfirmed)return;b.prop('disabled',true);$.post('<?=base_url('items/posting')?>',Object.assign({id:id,harga_jual:sale},csrf())).done(x=>{if(!x.status)return AppAlert.toast(x.message,'error');table.ajax.reload(null,false);AppAlert.toast(x.message,'success')}).fail(()=>AppAlert.toast('Posting barang gagal.','error')).always(()=>b.prop('disabled',false))})});
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#pendingDetailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#pendingDetailModal').show();$.get('<?=base_url('items/detail')?>/'+id).done(r=>{let rows=(r.logs||[]).map(x=>'<tr><td>'+esc(x.created_at)+'</td><td>'+esc(x.gudang)+'</td><td>'+esc(x.tipe)+'</td><td class="text-end">'+Number(x.qty||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</td><td>'+esc(x.keterangan)+'</td></tr>').join('');$('#pendingDetailBody').html('<div class="document-meta"><span>Kode<strong>'+esc(r.item.kode_detail)+'</strong></span><span>Barang<strong>'+esc(r.item.nama_barang)+'</strong></span><span>Harga Beli<strong>'+money(r.item.harga_beli)+'</strong></span><span>Harga Jual<strong>'+money(r.item.harga_jual)+'</strong></span><span>Stok Diterima<strong>'+Number(r.item.stok||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</strong></span><span>Status<strong><span class="status-chip status-submitted">PENDING</span></strong></span></div><div class="table-responsive mt-3"><table class="table inventory-table"><thead><tr><th>Tanggal</th><th>Gudang</th><th>Arah</th><th class="text-end">Qty</th><th>Keterangan</th></tr></thead><tbody>'+rows+'</tbody></table></div><button type="button" class="btn btn-sm btn-outline-danger stock-pdf" data-id="'+id+'"><i class="bi bi-file-earmark-pdf"></i> PDF Audit</button>')})});
$(document).on('click','.stock-pdf',function(){$('#barcodePrintModal .modal-title').text('PDF Kartu Stok');$('#barcodePrintModal iframe').attr('src','<?=base_url('items/stock_log_pdf')?>/'+$(this).data('id'));bootstrap.Modal.getOrCreateInstance(document.getElementById('barcodePrintModal')).show()});
$(document).on('click','.btn-print',function(){let id=$(this).data('id');AppAlert.prompt({title:'Preview Barcode',html:'<label class="form-label d-block text-start">Jumlah kolom</label><select id="printColumn" class="form-select mb-3"><option value="2">2</option><option value="3" selected>3</option><option value="4">4</option></select><label class="form-label d-block text-start">Salinan</label><input id="printCopies" type="number" min="1" max="20" value="1" class="form-control">',preConfirm:()=>({column:$('#printColumn').val(),duplicat:$('#printCopies').val()}),confirmButtonText:'Preview'}).then(r=>{if(!r.isConfirmed)return;$('#barcodePrintModal .modal-title').text('Preview Barcode');$('#barcodePrintModal iframe').attr('src','<?=base_url('items/print_barcode')?>/'+id+'?column='+r.value.column+'&duplicat='+r.value.duplicat+'&status=all');bootstrap.Modal.getOrCreateInstance(document.getElementById('barcodePrintModal')).show()})});
});
// ================= LOAD AKUN KAS =================
// function loadKas(){
// $.get("<?= base_url('items/get_accounts'); ?>",res=>{
// let opt='<option value="">-- PILIH KAS --</option>';
// res.forEach(a=>{
// opt += `<option value="${a.id}">${a.nama_akun}</option>`;
// });
// $('#account_kas').html(opt);
// },'json');
// }
function loadKas(){
$.get("<?= base_url('items/get_accounts'); ?>",function(res){
accountOption = '<option value="">-- PILIH KAS --</option>';
res.forEach(function(a){
accountOption += `
<option value="${a.id}">
${a.nama_akun}
</option>
`;
});
$('.account_kas').html(accountOption);
},'json');
}
// ================= LOAD GUDANG =================
function loadGudang(){
$.get("<?= base_url('warehouses/list'); ?>",res=>{
let opt='<option value="">-- PILIH GUDANG --</option>';
res.forEach(w=>{
opt += `<option value="${w.id}">${w.nama}</option>`;
});
$('#warehouse_id, #adjust_warehouse').html(opt);
},'json');
}
function loadKode() {
$.get("<?= base_url('kodebarang/list'); ?>", function(res) {
let opt = '<option value="">-- PILIH KODE --</option>';
res.forEach(function(a){
opt += `
<option value="${a.id}"
data-type="${a.tracking_type}">
${a.kode_barang} - ${a.nama}
</option>
`;
});
$('#kode_barang').html(opt).trigger('change');
}, 'json');
}
function addAccountRow(){
let html = `
<div class="row account-row mb-2">
<div class="col-md-6">
<select
name="account_kas[]"
class="form-control account_kas">
${accountOption}
</select>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">
Rp
</span>
<input
type="text"
name="nominal[]"
class="form-control nominal format-rupiah">
</div>
</div>
<div class="col-md-2">
<button
type="button"
class="btn btn-sm btn-danger btn-remove-account w-100">
Hapus
</button>
</div>
</div>
`;
$('#accountContainer').append(html);
hitungTotal();
}
$(document).on('change', '#kode_barang', function () {
let type = $(this).find(':selected').data('type');
if(type == 'QTY'){
$('#group_tracking_qty').show();
}else{
$('#group_tracking_qty').hide();
$('#tracking_qty').val('');
}
});
$('#btnTambahAccount').click(function(){
addAccountRow();
});
$(document).on('click','.btn-remove-account',function(){
$(this).closest('.account-row').remove();
hitungTotal();
});
// function hitungTotal(){
// let total = 0;
// $('.nominal').each(function(){
// let angka = $(this)
// .val()
// .replace(/\./g,'')
// .replace(/,/g,'');
// total += Number(angka || 0);
// });
// $('#totalAccount').text(formatRupiah(total.toString()));
// }
$(document).on('keyup','.nominal',function(){
hitungTotal();
});
$(document).on('keyup','#harga_beli',function(){
hitungTotal();
});
$(document).on('keyup change','#qty',function(){
hitungTotal();
});
// ================= RESET FORM =================
function resetForm(){
$('#id').val('');
$('#nama_barang').val('');
$('#qty').val('');
$('#harga_beli').val('');
$('#harga_jual').val('');
$('#tanggal_beli').val('');
$('#tracking_qty').val('');
$('#group_tracking_qty').hide();
$('#accountContainer').html(`
<div class="row account-row mb-2">
<div class="col-md-6">
<select
name="account_kas[]"
class="form-control account_kas">
${accountOption}
</select>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Rp</span>
<input
type="text"
name="nominal[]"
class="form-control nominal format-rupiah">
</div>
</div>
<div class="col-md-2">
<button
class="btn btn-sm btn-danger btn-remove-account w-100"
disabled>
Hapus
</button>
</div>
</div>
`);
$('#totalAccount').text('Rp 0');
$('#totalHppBarang').text('Rp 0');
$('#totalAll')
.text('Balance')
.removeClass('text-danger')
.addClass('text-success');
$('#btnSimpan').prop('disabled',true);
}
// ================= INIT LOAD =================
loadGudang();
loadKas();
loadKode();
// ================= ADD =================
$('.btn-add-item').click(function(){
action='add';
resetForm();
$('#qty').closest('div').show();
$('#kode_barang').closest('div').show();
$('#warehouse_id').closest('div').show();
$('#account_kas').closest('div').show();
$('#modalItem').modal('show');
loadGudang();
loadKas();
loadKode();
});
// ================= EDIT =================
$(document).on('click','.btn-editItem',function(){
let id = $(this).data('id');
action='edit';
$.get("<?= base_url('items/detail_simple/'); ?>"+id,function(res){
$('#id').val(res.id);
$('#nama_barang').val(res.nama_barang);
$('#harga_beli').val(res.harga_beli);
$('#harga_jual').val(res.harga_jual);
// 🔥 FIX DI SINI
$('#group_qty').hide();
$('#group_account').hide();
$('#group_kode').hide();
$('#group_warehouse').hide();
$('#group_tanggal').hide();
$('#modalItem').modal('show');
},'json');
});
// ================= SAVE =================
$('#btnSimpan').click(function(){
let btn = $(this);
let qtys = parseFloat($('#qty').val()) || 0;
let totalHpp = qtys * rupiahToNumber($('#harga_beli').val());
let totalAccount = 0;
$('.nominal').each(function(){
totalAccount += rupiahToNumber($(this).val());
});
if(totalHpp != totalAccount){
Swal.fire({
icon:'warning',
title:'Belum Balance',
text:'Total pembayaran harus sama dengan Total Harga Beli.'
});
return;
}
let account = [];
$('.account-row').each(function(){
account.push({
account_id: $(this).find('.account_kas').val(),
nominal: $(this)
.find('.nominal')
.val()
});
});
// data.accounts = account;
let data = {
id: $('#id').val(),
kode_id: $('#kode_barang').val(),
nama_barang: $('#nama_barang').val(),
qty: $('#qty').val(),
harga_beli: $('#harga_beli').val(),
harga_jual: $('#harga_jual').val(),
warehouse_id: $('#warehouse_id').val(),
// account_kas: $('#account_kas').val(),
account_kas: account,
tanggal_beli: $('#tanggal_beli').val(),
tracking_qty: $('#tracking_qty').val(),
status: `draft`
};
let url = action==='add'
? "<?= base_url('items/save'); ?>"
: "<?= base_url('items/update'); ?>";
// 🔥 loading state
btn.prop('disabled', true).html('Menyimpan...');
$.post(url, data, function(res){
if(res.status){
$('#modalItem').modal('hide');
table.ajax.reload(null,false);
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: res.message || 'Data berhasil disimpan',
timer: 1500,
showConfirmButton: false
});
}else{
Swal.fire({
icon: 'error',
title: 'Gagal',
text: res.message || 'Terjadi kesalahan'
});
}
},'json')
.fail(function(){
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Koneksi ke server gagal'
});
})
.always(function(){
btn.prop('disabled', false).html('Simpan');
});
});
// ================= PRINT BARCODE =================
$(document).on('click', '.btn-print', function () {
let id = $(this).data('id');
Swal.fire({
title: 'Cetak Barcode',
html: `
<div class="text-left">
<div class="form-group">
<label for="print_column" class="font-weight-bold">
Jumlah Kolom
</label>
<select id="print_column" class="form-control">
<option value="1">1 Kolom</option>
<option value="2">2 Kolom</option>
<option value="3" selected>3 Kolom (Rekomendasi)</option>
</select>
</div>
<div class="form-group mb-0">
<label for="duplicat" class="font-weight-bold">
Jumlah Cetak per Barcode
</label>
<select id="duplicat" class="form-control">
<option value="1" selected>1 Label</option>
<option value="2">2 Label</option>
</select>
</div>
</div>
`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Cetak',
cancelButtonText: 'Batal',
confirmButtonColor: '#f0ad4e',
preConfirm: () => {
let column = $('#print_column').val();
let duplicat = $('#duplicat').val();
return {
column: column,
duplicat: duplicat
};
}
}).then((result) => {
if (!result.isConfirmed) {
return;
}
window.open(
"<?= base_url('items/print_barcode/'); ?>" +
id +
"?column=" + result.value.column +
"&duplicat=" + result.value.duplicat,
"_blank"
);
});
});
// ================= DELETE =================
$(document).on('click','.btn-delete',function(){
let id = $(this).data('id');
let btn = $(this);
Swal.fire({
title: 'Hapus data?',
text: "Data tidak bisa dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, hapus!',
cancelButtonText: 'Batal'
}).then((result) => {
if(result.isConfirmed){
btn.prop('disabled', true).html('Menghapus...');
$.get("<?= base_url('items/delete/'); ?>"+id,function(res){
if(res.status){
table.ajax.reload(null,false);
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: res.message || 'Data berhasil dihapus',
timer: 1500,
showConfirmButton: false
});
}else{
Swal.fire({
icon: 'error',
title: 'Gagal',
text: res.message || 'Gagal menghapus data'
});
}
},'json')
.fail(function(){
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Koneksi ke server gagal'
});
})
.always(function(){
btn.prop('disabled', false).html('Hapus');
});
}
});
});
// ================= POSTING =================
$(document).on('click','.btn-updatePosting',function(){
let id = $(this).data('id');
let btn = $(this);
if(btn.data('loading')) return;
Swal.fire({
title: 'Konfirmasi Penerimaan Barang',
text: 'Pastikan barang sudah diterima dan sesuai pesanan. Stok akan otomatis ditambahkan ke gudang.',
icon: 'info',
showCancelButton: true,
confirmButtonColor: '#28a745',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Ya, Terima Barang',
cancelButtonText: 'Batal'
}).then((result) => {
if(result.isConfirmed){
btn.data('loading', true)
.prop('disabled', true)
.html('Memposting...');
$.post("<?= base_url('items/posting'); ?>", {
id: id
// tambahkan CSRF kalau perlu
}, function(res){
if(res && res.status){
table.ajax.reload(null,false);
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: res.message || 'Data berhasil diposting',
timer: 1500,
showConfirmButton: false
});
}else{
Swal.fire({
icon: 'error',
title: 'Gagal',
text: res?.message || 'Gagal memposting data'
});
}
},'json')
.fail(function(xhr){
Swal.fire({
icon: 'error',
title: 'Error',
text: xhr.responseJSON?.message || 'Server error ('+xhr.status+')'
});
})
.always(function(){
btn.prop('disabled', false)
.html('Posting')
.data('loading', false);
});
}
});
});
// ================= DETAIL =================
$(document).on('click','.btn-detail',function(){
let id = $(this).data('id');
$.get("<?= base_url('items/detail/'); ?>"+id,function(res){
let html = `
<b>Nama :</b> ${res.item.nama_barang}<br>
<b>Harga Beli :</b> ${formatRupiah(res.item.harga_beli)}<br>
<b>Harga Jual :</b> ${formatRupiah(res.item.harga_jual)}
<hr>
<h6>Riwayat Stok</h6>
<table class="table table-sm table-bordered">
<tr>
<th>Tanggal</th>
<th>Gudang</th>
<th>Qty</th>
<th>Tipe</th>
<th>Keterangan</th>
</tr>
`;
res.logs.forEach(l=>{
html += `
<tr>
<td>${l.created_at ?? '-'}</td>
<td>${l.gudang ?? '-'}</td>
<td>${l.qty}</td>
<td>${l.tipe}</td>
<td>${l.keterangan ?? ''}</td>
</tr>`;
});
html += `</table>`;
$('#detailContent').html(html);
$('#modalDetail').modal('show');
},'json');
});
function rupiahToNumber(value){
if(!value) return 0;
let s = value.toString().trim();
s = s.replace(/\.(?=\d{3}(\.|$))/g, '');
s = s.replace(/,/g, '');
return parseFloat(s) || 0;
}
function hitungTotal(){
// ==========================
// HPP BARANG
// ==========================
let qty = parseFloat($('#qty').val()) || 0;
let hargaBeli = rupiahToNumber($('#harga_beli').val());
let totalHpp = qty * hargaBeli;
// ==========================
// ACCOUNTING
// ==========================
let totalAccount = 0;
$('.nominal').each(function(){
totalAccount += rupiahToNumber($(this).val());
});
// ==========================
// BALANCE
// ==========================
let balance = totalAccount - totalHpp;
$('#totalHppBarang').text(formatRupiah(totalHpp.toString()));
$('#totalAccount').text(formatRupiah(totalAccount.toString()));
if(balance == 0){
$('#totalAll')
.removeClass('text-danger')
.addClass('text-success')
.text('Balance');
$('#btnSimpan').prop('disabled',false);
}else{
let prefix = balance > 0 ? '+' : '-';
$('#totalAll')
.removeClass('text-success')
.addClass('text-danger')
.text(prefix + ' ' + formatRupiah(Math.abs(balance).toString()));
$('#btnSimpan').prop('disabled',true);
}
}
});
</script>
</script>
+12 -402
View File
@@ -1,406 +1,16 @@
<style>
body {
background: #f5f7fb;
}
/* ===========================
CARD
=========================== */
.main-card,
.scanner-card,
.table-card {
border: none;
border-radius: 18px;
overflow: hidden;
/*box-shadow: 0 8px 25px rgba(0, 0, 0, .08);*/
}
/* ===========================
HEADER
=========================== */
.header-gradient {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff;
padding: 22px 28px;
}
.info-box {
background: rgba(255,255,255,.18);
border-radius: 12px;
padding: 12px 16px;
}
.progress {
height: 8px;
border-radius: 20px;
}
/* ===========================
SCANNER
=========================== */
.barcode-avatar {
width: 110px;
height: 110px;
margin: auto;
border-radius: 50%;
background: #fff7e6;
color: #f59e0b;
display: flex;
justify-content: center;
align-items: center;
font-size: 45px;
}
.barcode-code {
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
}
.form-control-lg {
height: 60px;
font-size: 22px;
border-radius: 12px;
}
.form-control:focus {
border-color: #f59e0b;
box-shadow: 0 0 0 .2rem rgba(245, 158, 11, .2);
}
.scan-note {
color: #6c757d;
font-size: 13px;
}
.camera-btn {
height: 55px;
border-radius: 12px;
font-size: 17px;
}
/* ===========================
TABLE
=========================== */
.table thead th {
background: #d97706;
color: #fff;
border: none;
vertical-align: middle;
}
.table tbody td {
vertical-align: middle;
}
.table tbody tr:hover {
background: #fffaf0;
}
/* ===========================
BADGE
=========================== */
.badge-success-soft {
display: inline-block;
padding: 6px 14px;
border-radius: 30px;
background: #d1fae5;
color: #065f46;
font-weight: 600;
}
.badge-danger-soft {
display: inline-block;
padding: 6px 14px;
border-radius: 30px;
background: #fee2e2;
color: #991b1b;
font-weight: 600;
}
.badge-warning-soft {
display: inline-block;
padding: 6px 14px;
border-radius: 30px;
background: #fef3c7;
color: #92400e;
font-weight: 600;
}
#reader{
width:100%;
min-height:280px;
border-radius:12px;
overflow:hidden;
}
</style>
<div class="container mt-4">
<div class="card main-card">
<!-- HEADER -->
<div class="header-gradient">
<div class="row align-items-center">
<div class="col-md-8">
<h3 class="mb-1">
<i class="fa fa-barcode"></i>
Riwayat Perpindahan Barang
</h3>
<div class="small">
Silahkan scan / input QR id Barang
</div>
</div>
<div class="col-md-4 text-right">
</div>
</div>
</div>
<!-- BODY -->
<div class="card-body">
<div class="row">
<!-- LEFT -->
<div class="col-lg-4 mb-4">
<div class="card scanner-card">
<div class="card-body">
<div class="barcode-avatar">
<i class="bi bi-qr-code-scan"></i>
</div>
<div class="text-center mt-3">
<div class="scan-note mt-2">
Scan QR ID
</div>
</div>
<hr>
<center>
<div id="scannerArea">
<input
type="text"
id="serial_number"
class="form-control form-control-lg text-center"
placeholder="Scan / Input QR ID"
autocomplete="off"
autofocus>
<small class="scan-note">
Scanner akan otomatis memanggil riwayat.
</small>
<button
type="button"
class="btn btn-warning btn-block camera-btn mt-4"
id="btnCamera">
<i class="fa fa-camera mr-2"></i>
Scan dengan Kamera
</button>
</div>
<div id="cameraArea" style="display:none;">
<div id="reader"></div>
<small class="scan-note d-block mt-2">
Arahkan QR Code ke kamera.
</small>
<button
type="button"
class="btn btn-secondary btn-block mt-3"
id="btnCloseCamera">
Tutup Kamera
</button>
</div>
</center>
<input type="hidden" id="barcode_id">
</div>
</div>
</div>
<!-- RIGHT -->
<div class="col-lg-8">
<div class="card table-card">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
Riwayat Perpindahan Barang
</h5>
</div>
</div>
<div class="card-body border-bottom" id="itemInfo">
Belum ada data
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" id="tableBarcode">
<thead>
<tr>
<th width="60">No</th>
<th>Date</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>NOtes</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php $inventory_title='Riwayat Barcode & Serial';$inventory_subtitle='Scan atau cari barcode untuk melihat identitas, posisi, saldo, reservasi, dan seluruh perpindahannya.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<div class="row g-3">
<div class="col-lg-4"><section class="inventory-card h-100"><div class="inventory-card-head"><div><h5>Pencarian Unit</h5><p>Gunakan barcode, QR, atau serial number.</p></div></div><div class="p-3"><label class="form-label">Barcode / Serial Number</label><div class="input-group"><input id="scanCode" class="form-control form-control-lg" autocomplete="off" placeholder="Scan atau ketik kode"><button class="btn btn-primary" id="findBarcode"><i class="bi bi-search"></i></button></div><button class="btn btn-outline-primary w-100 mt-3" id="openCamera"><i class="bi bi-camera"></i> Buka Kamera Scanner</button><small class="text-muted d-block mt-3">Scanner hanya membaca data. Mutasi barang tetap dilakukan melalui dokumen sumber yang sesuai.</small></div></section></div>
<div class="col-lg-8"><section class="inventory-card h-100"><div class="inventory-card-head"><div><h5>Identitas dan Posisi Barang</h5><p id="scanHint">Belum ada barcode yang dipilih.</p></div></div><div class="p-3" id="scanResult"><div class="inventory-empty"><i class="bi bi-upc-scan"></i><strong>Siap memindai</strong><small>Hasil akan tampil di sini tanpa membuka halaman baru.</small></div></div></section></div>
</div>
<section class="inventory-card mt-3"><div class="inventory-card-head"><div><h5>Riwayat Perpindahan</h5><p>Urutan terbaru dari barcode atau serial yang dipilih.</p></div></div><div class="table-responsive"><table class="table inventory-table align-middle mb-0"><thead><tr><th>Waktu</th><th>Jenis</th><th>Dari</th><th>Ke</th><th class="text-end">Qty</th><th>Dokumen Sumber</th><th>Keterangan</th></tr></thead><tbody id="movementRows"><tr><td colspan="7" class="text-center text-muted py-5">Scan barcode untuk melihat riwayat.</td></tr></tbody></table></div></section>
<div class="modal fade inventory-modal" id="cameraModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Scanner Barcode / QR</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div id="cameraReader"></div></div></div></div></div>
<script>
$("#serial_number").keypress(function(e){
if(e.which==13){
searchHistory();
}
$(function(){
const esc=v=>$('<div>').text(v==null||v===''?'-':v).html(),qty=v=>Number(v||0).toLocaleString('id-ID',{maximumFractionDigits:4});let scanner=null;
function location(type,id){if(!type)return'-';return esc(String(type).replaceAll('_',' '))+(id?' #'+esc(id):'')}
function find(){let code=$('#scanCode').val().trim();if(!code)return AppAlert.toast('Barcode atau serial wajib diisi.','warning');let b=$('#findBarcode').prop('disabled',true);$('#scanResult').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat identitas barang...</div>');$.get('<?=base_url('inventoryprofessional/scan')?>',{code:code}).done(function(r){let x=r.item,available=Math.max(0,Number(x.qty_sisa)-Number(x.reserved_qty)-Number(x.return_reserved_qty));$('#scanHint').text(x.kode_detail+' - '+x.nama_barang);$('#scanResult').html('<div class="scan-product"><span class="status-chip status-'+esc(x.status)+'">'+esc(String(x.status).replaceAll('_',' '))+'</span><h5>'+esc(x.kode_detail+' - '+x.nama_barang)+'</h5><div class="scan-grid"><span>Barcode<strong>'+esc(x.barcode)+'</strong></span><span>Serial Number<strong>'+esc(x.serial_number)+'</strong></span><span>Gudang<strong>'+esc(x.warehouse_name)+'</strong></span><span>Rak / Bin<strong>'+esc(x.bin_code)+'</strong></span><span>Batch<strong>'+esc(x.batch_no)+'</strong></span><span>Satuan<strong>'+esc(x.unit)+'</strong></span><span>Qty Fisik<strong>'+qty(x.qty_sisa)+'</strong></span><span>Reservasi / Retur<strong>'+qty(Number(x.reserved_qty)+Number(x.return_reserved_qty))+'</strong></span><span>Qty Tersedia<strong>'+qty(available)+'</strong></span></div></div>');$('#movementRows').html((x.history||[]).map(m=>'<tr><td>'+esc(m.created_at)+'</td><td><span class="status-chip">'+esc(String(m.movement_type||'-').replaceAll('_',' '))+'</span></td><td>'+location(m.from_type,m.from_id)+'</td><td>'+location(m.to_type,m.to_id)+'</td><td class="text-end">'+qty(m.qty)+'</td><td>'+esc(m.reference_type)+(m.reference_id?' #'+esc(m.reference_id):'')+'</td><td>'+esc(m.notes)+'</td></tr>').join('')||'<tr><td colspan="7" class="text-center text-muted py-4">Belum ada riwayat perpindahan.</td></tr>')}).fail(x=>{let m=x.responseJSON&&x.responseJSON.message||'Barcode tidak ditemukan.';$('#scanResult').html('<div class="inventory-empty"><i class="bi bi-exclamation-circle"></i><strong>'+esc(m)+'</strong></div>');$('#movementRows').html('<tr><td colspan="7" class="text-center text-muted py-4">Tidak ada data.</td></tr>');AppAlert.toast(m,'error')}).always(()=>b.prop('disabled',false))}
$('#findBarcode').on('click',find);$('#scanCode').on('keydown',e=>{if(e.key==='Enter'){e.preventDefault();find()}});
$('#openCamera').on('click',function(){let modal=bootstrap.Modal.getOrCreateInstance(document.getElementById('cameraModal'));modal.show();setTimeout(()=>{if(typeof Html5QrcodeScanner==='undefined')return AppAlert.toast('Library scanner belum tersedia. Gunakan input manual.','warning');scanner=new Html5QrcodeScanner('cameraReader',{fps:10,qrbox:{width:240,height:160}},false);scanner.render(code=>{$('#scanCode').val(code);scanner.clear().catch(()=>{});modal.hide();find()},()=>{})},250)});$('#cameraModal').on('hidden.bs.modal',function(){if(scanner)scanner.clear().catch(()=>{});scanner=null;$('#cameraReader').empty()});
});
function searchHistory(){
let barcode=$("#serial_number").val().trim();
if(barcode=="") return;
$.ajax({
url: `<?= base_url("items/scan_history"); ?>`,
type:"POST",
dataType:"json",
data:{
barcode:barcode
},
beforeSend:function(){
$("#serial_number").prop("disabled",true);
},
success:function(r){
$("#serial_number")
.prop("disabled",false)
.val("")
.focus();
if(!r.status){
Swal.fire({
icon:"error",
text:r.message
});
return;
}
renderItem(r.item);
renderHistory(r.history);
}
});
}
function renderItem(item){
let html='';
html+='<div class="row">';
html+=' <div class="col-md-6">';
html+=' <h4 class="mb-3"><b>'+item.nama_barang+'</b></h4>';
html+=' <div class="row mb-2">';
html+=' <div class="col-4 font-weight-bold">Kode</div>';
html+=' <div class="col-1">:</div>';
html+=' <div class="col-7">'+item.kode_detail+'</div>';
html+=' </div>';
html+=' <div class="row mb-2">';
html+=' <div class="col-4 font-weight-bold">Barcode</div>';
html+=' <div class="col-1">:</div>';
html+=' <div class="col-7">'+item.barcode+'</div>';
html+=' </div>';
html+=' </div>';
html+=' <div class="col-md-6">';
html+=' <div class="row mb-2">';
html+=' <div class="col-4 font-weight-bold">Serial Number</div>';
html+=' <div class="col-1">:</div>';
html+=' <div class="col-7">'+(item.serial_number ? item.serial_number : '-')+'</div>';
html+=' </div>';
html+=' <div class="row mb-2">';
html+=' <div class="col-4 font-weight-bold">Status</div>';
html+=' <div class="col-1">:</div>';
html+=' <div class="col-7">'+item.status+'</div>';
html+=' </div>';
html+=' <div class="row mb-2">';
html+=' <div class="col-4 font-weight-bold">Qty</div>';
html+=' <div class="col-1">:</div>';
html+=' <div class="col-7">'+item.qty_sisa+' / '+item.qty_awal+'</div>';
html+=' </div>';
html+=' </div>';
html+='</div>';
$("#itemInfo").html(html);
}
function renderHistory(list){
let html='';
$.each(list,function(i,v){
html+="<tr>";
html+="<td>"+(i+1)+"</td>";
html+="<td>"+v.created_at+"</td>";
html+="<td>"+v.movement_type+"</td>";
html+="<td>"+(v.from_warehouse??v.from_type)+"</td>";
html+="<td>"+(v.to_warehouse??v.to_type)+"</td>";
html+="<td>"+v.notes+"</td>";
html+="</tr>";
});
$("#tableBarcode tbody").html(html);
}
let html5QrCode=null;
$("#btnCamera").click(function(){
$("#scannerArea").hide();
$("#cameraArea").show();
html5QrCode=new Html5Qrcode("reader");
html5QrCode.start(
{
facingMode:"environment"
},
{
fps:10,
qrbox:220
},
function(decodedText){
html5QrCode.stop().then(function(){
$("#cameraArea").hide();
$("#scannerArea").show();
$("#serial_number")
.val(decodedText);
saveSerial();
});
}
);
});
$("#btnCloseCamera").click(function(){
if(html5QrCode){
html5QrCode.stop().then(function(){
$("#cameraArea").hide();
$("#scannerArea").show();
$("#serial_number").focus();
});
}
});
</script>
File diff suppressed because it is too large Load Diff
+60 -21
View File
@@ -34,9 +34,10 @@
<th>No</th>
<th>Tanggal</th>
<th>No Ref</th>
<th>Keterangan</th>
<th style="max-width: 600px">Keterangan</th>
<th class="text-end">Debit</th>
<th class="text-end">Kredit</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
@@ -46,7 +47,7 @@
<td colspan="4" class="text-end">TOTAL</td>
<td class="text-end" id="totalDebitTable">0</td>
<td class="text-end" id="totalKreditTable">0</td>
<td></td>
<td></td><td></td>
</tr>
</tfoot>
</table>
@@ -71,8 +72,8 @@
<!-- MODE -->
<div class="mb-3">
<div class="btn-group w-100">
<button class="btn btn-warning active" id="modeAuto">Otomatis</button>
<button class="btn btn-outline-secondary" id="modeManual">Manual</button>
<button type="button" class="btn btn-warning active" id="modeManual">Jurnal Manual</button>
<button type="button" class="btn btn-outline-secondary" id="modeAuto">Template Jurnal</button>
</div>
</div>
@@ -138,7 +139,8 @@
<strong>Tanggal:</strong> <span id="d_tanggal"></span><br>
<strong>No Ref:</strong> <span id="d_noref"></span><br>
<strong>Keterangan:</strong> <span id="d_keterangan"></span><br>
<strong>Dibuat oleh:</strong> <span id="d_createdby"></span>
<strong>Dibuat oleh:</strong> <span id="d_createdby"></span><br>
<strong>Status:</strong> <span id="d_status"></span>
</div>
<table class="table table-bordered">
@@ -160,6 +162,9 @@
</tfoot>
</table>
<h6 class="mt-3">Riwayat Status</h6>
<div id="statusHistory" class="small text-muted"></div>
</div>
</div>
@@ -177,7 +182,7 @@
$(function(){
let accounts = [];
let mode = 'auto';
let mode = 'manual';
// ================= LOAD ACCOUNTS
function loadAccounts(){
@@ -282,7 +287,7 @@ $('#modeAuto').click(function(){
mode = 'auto';
$(this).addClass('btn-warning active').removeClass('btn-outline-warning');
$(this).addClass('btn-warning active').removeClass('btn-outline-secondary');
$('#modeManual').removeClass('btn-warning active').addClass('btn-outline-secondary');
$('#autoSection').show();
@@ -305,7 +310,7 @@ $('#modeManual').click(function(){
mode = 'manual';
$(this).addClass('btn-warning active').removeClass('btn-outline-secondary');
$('#modeAuto').removeClass('btn-warning active').addClass('btn-outline-warning');
$('#modeAuto').removeClass('btn-warning active').addClass('btn-outline-secondary');
$('#autoSection').hide();
$('#tableDetail tbody').html('');
@@ -313,6 +318,7 @@ $('#modeManual').click(function(){
$('.btn-add-row').show(); // ⬅️ tampilkan lagi
addRow();
addRow();
});
// ================= PILIH TEMPLATE
@@ -342,8 +348,8 @@ $('#modeManual').click(function(){
let td=0, tk=0;
$('#tableDetail tbody tr').each(function(){
td += parseFloat($(this).find('.debit').val()) || 0;
tk += parseFloat($(this).find('.kredit').val()) || 0;
td += window.parseMoney($(this).find('.debit').val());
tk += window.parseMoney($(this).find('.kredit').val());
});
$('#formTotalDebit').text(td.toLocaleString('id-ID'));
@@ -367,7 +373,7 @@ $('.btn-add').click(async function(){
$('#formTotalDebit').text('0');
$('#formTotalKredit').text('0');
$('#modeAuto').click(); // default
$('#modeManual').click(); // default jurnal manual dengan dua akun
});
$('#btnSimpan').click(function(){
@@ -381,8 +387,10 @@ $('#btnSimpan').click(function(){
$('#tableDetail tbody tr').each(function(){
let d = parseFloat($(this).find('.debit').val()) || 0;
let k = parseFloat($(this).find('.kredit').val()) || 0;
let d = window.parseMoney($(this).find('.debit').val());
let k = window.parseMoney($(this).find('.kredit').val());
if(d <= 0 && k <= 0) return;
account_id.push($(this).find('.account_id').val());
debit.push(d);
@@ -393,13 +401,18 @@ $('#btnSimpan').click(function(){
});
// ✅ VALIDASI DI SINI
if(totalDebit !== totalKredit){
if(Math.abs(totalDebit - totalKredit) > 0.009){
Swal.fire('Warning','Total Debit dan Kredit harus sama!','warning');
return;
}
if(totalDebit === 0 && totalKredit === 0){
Swal.fire('Warning','Harap Isi Semua Kolom!','warning'); // ✅ FIX add all collumn must be filled
Swal.fire('Warning','Minimal satu pasangan debit dan kredit harus diisi.','warning');
return;
}
if(account_id.length < 2 || account_id.some(id => !id)){
Swal.fire('Warning','Setiap baris yang memiliki nominal wajib memilih akun.','warning');
return;
}
@@ -412,7 +425,6 @@ $('#btnSimpan').click(function(){
Swal.fire('Warning','Tanggal tidak boleh kosong!','warning');
return;
}
// lanjut simpan
$.post("<?= base_url('jurnal/save'); ?>", {
tanggal: $('#tanggal').val(),
@@ -474,13 +486,37 @@ let table = $('#tableJurnal').DataTable({
showCancelButton:true
}).then(r=>{
if(r.isConfirmed){
$.get("<?= base_url('jurnal/delete/'); ?>"+id,function(){
table.ajax.reload(null,false);
});
$.post("<?= base_url('jurnal/delete/'); ?>"+id,{},function(res){
if(res.status){ table.ajax.reload(null,false); Swal.fire('Sukses',res.message,'success'); }
else Swal.fire('Error',res.message,'error');
},'json');
}
});
});
function workflowPost(endpoint, id, data, confirmation){
Swal.fire({title:confirmation,icon:'question',showCancelButton:true}).then(function(result){
if(!result.isConfirmed) return;
$.post("<?= base_url('jurnal/'); ?>"+endpoint+'/'+id, data || {}, function(res){
if(res.status){ table.ajax.reload(null,false); Swal.fire('Sukses',res.message,'success'); }
else Swal.fire('Error',res.message,'error');
},'json').fail(function(xhr){
Swal.fire('Error', xhr.responseJSON?.message || 'Proses workflow gagal.','error');
});
});
}
$(document).on('click','.btn-submit',function(){ workflowPost('submit',$(this).data('id'),{},'Ajukan jurnal ini?'); });
$(document).on('click','.btn-approve',function(){ workflowPost('approve',$(this).data('id'),{},'Setujui jurnal ini?'); });
$(document).on('click','.btn-reject',async function(){
const result=await Swal.fire({title:'Tolak jurnal',input:'textarea',inputLabel:'Alasan',showCancelButton:true,inputValidator:v=>!v?'Alasan wajib diisi':undefined});
if(result.isConfirmed) workflowPost('reject',$(this).data('id'),{reason:result.value},'Konfirmasi penolakan jurnal?');
});
$(document).on('click','.btn-reverse',async function(){
const result=await Swal.fire({title:'Reversal jurnal',html:'<input type="date" id="reverseDate" class="swal2-input" value="<?= date('Y-m-d') ?>"><textarea id="reverseReason" class="swal2-textarea" placeholder="Alasan reversal"></textarea>',showCancelButton:true,preConfirm:()=>{const date=$('#reverseDate').val(),reason=$('#reverseReason').val().trim();if(!date||!reason){Swal.showValidationMessage('Tanggal dan alasan wajib diisi');return false;}return {date,reason};}});
if(result.isConfirmed) workflowPost('reverse',$(this).data('id'),result.value,'Buat jurnal reversal?');
});
});
// DETAIL
@@ -493,7 +529,10 @@ $(document).on('click','.btn-detail',function(){
$('#d_tanggal').text(res.header.tanggal);
$('#d_noref').text(res.header.no_ref);
$('#d_keterangan').text(res.header.keterangan);
$('#d_createdby').text(res.user.nama);
$('#d_createdby').text(res.user ? res.user.nama : '-');
$('#d_status').text(String(res.header.status || '').toUpperCase());
let history=(res.history||[]).map(h=>`${h.from_status||'-'} → ${h.to_status} oleh ${h.changed_by_name||'-'} (${h.changed_at})${h.notes?' — '+h.notes:''}`).join('<br>');
$('#statusHistory').html(history || 'Belum ada perubahan status.');
let html = '';
let totalDebit = 0;
@@ -524,4 +563,4 @@ $(document).on('click','.btn-detail',function(){
});
</script>
</script>
+28 -36
View File
@@ -1,11 +1,11 @@
<!--Lokasi nya : /views/employees/employees.php-->
<?php $inventory_title='Jenis / Kode Barang';$inventory_subtitle='Atur identitas, satuan, cara tracking, serial, dan pembentukan harga beli.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<h5>Kode Barang</h5>
<button class="btn btn-warning btn-add">Tambah</button>
<div><h5 class="mb-1">Jenis / Kode Barang</h5><small class="text-muted">Master yang sudah digunakan dinonaktifkan, bukan dihapus.</small></div>
<?php if($this->session->userdata('role')==='Admin'||check_permission('items','can_create')):?><button class="btn btn-primary btn-add"><i class="bi bi-plus-lg"></i> Tambah</button><?php endif?>
</div>
<div class="table-responsive">
@@ -16,9 +16,11 @@
<th>Kode</th>
<th>Nama</th>
<th>Limit Stok</th>
<th>Tracking Type</th>
<th>Need Serial</th>
<th>Tracking</th>
<th>Perlu Serial</th>
<th>Biaya ke Harga Beli</th>
<th>Unit</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
@@ -64,6 +66,12 @@
<option value="true">Ya</option>
</select>
<div class="form-check mt-3 py-3 border rounded bg-light" style="padding-left: 30px">
<input class="form-check-input" type="checkbox" id="allocate_purchase_cost" value="1">
<label class="form-check-label fw-semibold" for="allocate_purchase_cost">Masukkan biaya pembelian ke harga beli satuan</label>
<small class="d-block text-muted">Ongkir, asuransi, dan layanan bersih dari diskon/cashback dialokasikan per unit saat penerimaan.</small>
</div>
<label class="mt-2">Unit</label>
<select id="unit" class="form-control">
<option value="PCS">PCS</option>
@@ -71,6 +79,11 @@
<option value="Lembar">Lembar</option>
</select>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" id="is_active" checked>
<label class="form-check-label" for="is_active">Aktif dan dapat dipilih pada transaksi baru</label>
</div>
</div>
<div class="modal-footer">
@@ -101,7 +114,9 @@ $(function(){
$('#limit_stock').val('');
$('#tracking_type').val('UNIT');
$('#need_serial_number').val('false');
$('#allocate_purchase_cost').prop('checked',false);
$('#unit').val('PCS');
$('#is_active').prop('checked',true);
}
// ADD
@@ -123,7 +138,9 @@ $(function(){
$('#limit_stock').val(res.limit_stock);
$('#tracking_type').val((res.tracking_type || 'UNIT').toUpperCase());
$('#need_serial_number').val((res.need_serial_number || 'false').toLowerCase());
$('#allocate_purchase_cost').prop('checked', Number(res.allocate_purchase_cost || 0) === 1);
$('#unit').val(res.unit || 'PCS');
$('#is_active').prop('checked',Number(res.is_active || 0) === 1);
action = 'edit';
$('#modalKodebarang').modal('show');
@@ -141,9 +158,13 @@ $(function(){
limit_stock: $('#limit_stock').val(),
tracking_type: $('#tracking_type').val(),
need_serial_number: $('#need_serial_number').val(),
unit: $('#unit').val()
allocate_purchase_cost: $('#allocate_purchase_cost').is(':checked') ? 1 : 0,
unit: $('#unit').val(),
is_active: $('#is_active').is(':checked') ? 1 : 0
};
if(window.APP_CSRF) data[APP_CSRF.name]=APP_CSRF.value;
if(!data.kode || !data.nama){
Swal.fire('Warning','Kode dan nama barang wajib diisi','warning');
return;
@@ -171,34 +192,5 @@ $(function(){
},'json');
});
// DELETE
$(document).on('click','.btn-delete',function(){
let id = $(this).data('id');
Swal.fire({
title:'Hapus kode barang?',
text:'Pastikan tidak dipakai item',
icon:'warning',
showCancelButton:true
}).then(r=>{
if(r.isConfirmed){
$.get("<?= base_url('kodebarang/delete/'); ?>"+id,function(res){
if(res.status){
table.ajax.reload(null,false);
Swal.fire('Sukses',res.message,'success');
} else {
Swal.fire('Error',res.message,'error');
}
},'json');
}
});
});
});
</script>
</script>
+65 -259
View File
@@ -1,261 +1,67 @@
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<?php
$CI=&get_instance();
$routeClass=strtolower((string)$CI->router->fetch_class());
$routeMethod=strtolower((string)$CI->router->fetch_method());
if(!isset($active_menu)||$active_menu===''){
$routeMenus=array(
'attendancmonthly'=>'attendance_monthly',
'attendancemonthly'=>'attendance_monthly',
'attendancemonitoring'=>'attendance_monitoring',
'generateschedule'=>'generate_schedule',
'accountingperiods'=>'accounting_periods',
'inventoryprofessional'=>$routeMethod==='operations'?'inventory_operations':($routeMethod==='locations'?'inventory_locations':($routeMethod==='reports'?'inventory_reports':'inventory_overview')),
'professionalreports'=>'professional_reports',
'hrpayroll'=>'hr_payroll',
'fixedassets'=>'fixed_assets',
'kodebarang'=>'kode_barang',
'lokasiasset'=>'lokasiasset',
'teknisi'=>'peralatan_teknisi'
);
$active_menu=isset($routeMenus[$routeClass])?$routeMenus[$routeClass]:$routeClass;
if($routeClass==='invoices')$active_menu='sales_invoice';
if($routeClass==='items'&&$routeMethod==='draft_items')$active_menu='draft_items';
if($routeClass==='items'&&$routeMethod==='item_barcodes_history')$active_menu='item_historys';
}
$CI->config->load('ui_assets',true);
$uiAssets=$CI->config->item('ui_assets');
$dataPages=isset($uiAssets['ui_data_pages'])?$uiAssets['ui_data_pages']:array();
$selectPages=isset($uiAssets['ui_select_pages'])?$uiAssets['ui_select_pages']:array();
?></main></section></div>
<nav class="mobile-action-bar d-lg-none" aria-label="Aksi utama"><a href="<?=base_url()?>"><i class="bi bi-house"></i><span>Home</span></a><button data-bs-toggle="modal" data-bs-target="#quickActions"><i class="bi bi-lightning"></i><span>Aksi</span></button><a href="<?=base_url('inventoryprofessional')?>"><i class="bi bi-qr-code-scan"></i><span>Scan</span></a><button data-bs-toggle="offcanvas" data-bs-target="#mobileMenu"><i class="bi bi-grid"></i><span>Menu</span></button></nav>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" onerror="this.onerror=null;this.src='<?=base_url('assets/vendor/bootstrap.bundle.min.js')?>'"></script>
<?php if(in_array($active_menu,array('inventory_professional','inventory_overview','item_historys','camera_qr','fixed_assets'),true)):?><script src="https://unpkg.com/html5-qrcode"></script><?php endif?>
<script>window.APP_BASE_URL=<?=json_encode(base_url())?>;window.APP_CSRF={name:<?=json_encode($this->security->get_csrf_token_name())?>,value:<?=json_encode($this->security->get_csrf_hash())?>};</script><script src="<?=base_url('assets/js/app-ui.min.js').'?v='.filemtime(FCPATH.'assets/js/app-ui.min.js')?>" defer></script><script src="<?=base_url('assets/js/app-network.js').'?v='.filemtime(FCPATH.'assets/js/app-network.js')?>" defer></script>
<script>
// =====================================================
// 🔥 FORMAT NUMBER
// contoh:
// 1000 => 1,000
// 1000.5 => 1,000.5
// 1000.55 => 1,000.55
// =====================================================
function formatNumber(value){
if(value === null || value === undefined){
return '';
}
value = value.toString();
// hanya angka dan titik
value = value.replace(/[^0-9.]/g, '');
// hanya boleh 1 titik
let parts = value.split('.');
let integerPart = parts[0] || '';
let decimalPart = parts[1] || '';
// format ribuan pakai koma
integerPart = integerPart.replace(/,/g, '');
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// max 2 digit decimal
decimalPart = decimalPart.substring(0,2);
// kalau ada desimal
if(parts.length > 1 || value.endsWith('.')){
return integerPart + '.' + decimalPart;
}
return integerPart;
}
// =====================================================
// 🔥 CLEAN NUMBER
// hasil:
// 1,000.25 => 1000.25
// =====================================================
function cleanNumber(value){
if(!value) return '';
return value.toString().replace(/,/g, '');
}
// =====================================================
// 🔥 AUTO FORMAT INPUT
// =====================================================
$(document).on('input', '.format-rupiah', function(){
let cursorPos = this.selectionStart;
let beforeLength = $(this).val().length;
let formatted = formatNumber($(this).val());
$(this).val(formatted);
let afterLength = formatted.length;
cursorPos = cursorPos + (afterLength - beforeLength);
this.setSelectionRange(cursorPos, cursorPos);
});
// =====================================================
// 🔥 FORMAT SAAT BLUR
// =====================================================
$(document).on('blur', '.format-rupiah', function(){
$(this).val(
formatNumber($(this).val())
);
});
// =====================================================
// 🔥 AUTO CLEAN SAAT SUBMIT
// =====================================================
$(document).on('submit', 'form', function(){
$(this).find('.format-rupiah').each(function(){
$(this).val(
cleanNumber($(this).val())
);
});
});
// =====================================================
// 🔥 AUTO FORMAT VALUE AWAL
// =====================================================
$(document).ready(function(){
$('.format-rupiah').each(function(){
let val = $(this).val();
if(val !== ''){
$(this).val(
formatNumber(val)
);
}
});
});
// =====================================================
// 🔥 OVERRIDE jQuery .val()
// =====================================================
(function($){
const originalVal = $.fn.val;
$.fn.val = function(value){
// setter
if(typeof value !== 'undefined'){
if(this.hasClass('format-rupiah')){
return originalVal.call(
this,
formatNumber(value)
);
}
return originalVal.apply(this, arguments);
}
// getter
if(this.hasClass('format-rupiah')){
return cleanNumber(
originalVal.call(this)
);
}
return originalVal.call(this);
};
})(jQuery);
(function(){
if(!window.Swal)return;
var nativeFire=Swal.fire.bind(Swal);
window.AppAlert={
toast:function(message,icon){return nativeFire({toast:true,position:'top-end',showConfirmButton:false,timer:3200,timerProgressBar:true,icon:icon||'success',title:message});},
confirm:function(options){return nativeFire(Object.assign({icon:'warning',position:'center',showCancelButton:true,confirmButtonText:'Ya, lanjutkan',cancelButtonText:'Batal',reverseButtons:true,focusCancel:true},options||{}));},
prompt:function(options){return nativeFire(Object.assign({position:'center',showCancelButton:true,confirmButtonText:'Simpan',cancelButtonText:'Batal'},options||{}));}
};
// Menyeragamkan pemanggilan Swal lama: pesan biasa menjadi toast kanan atas,
// sedangkan konfirmasi dan form input tetap berupa dialog di tengah.
Swal.fire=function(first,text,icon){
var options=typeof first==='object'?Object.assign({},first):{title:first,text:text,icon:icon};
var requiresInteraction=!!(options.showCancelButton||options.showDenyButton||options.input||options.preConfirm||options.inputValidator);
if(requiresInteraction){options.position='center';return nativeFire(options);}
return nativeFire(Object.assign(options,{toast:true,position:'top-end',showConfirmButton:false,timer:options.timer||3200,timerProgressBar:options.timerProgressBar!==false}));
};
document.querySelectorAll('.modal-dialog').forEach(function(dialog){dialog.classList.add('modal-dialog-centered');});
document.querySelectorAll('[data-app-toast]').forEach(function(el){AppAlert.toast(el.dataset.message||'',el.dataset.icon||'success');});
var focusId=new URLSearchParams(location.search).get('focus');
if(focusId){
var focusDocument=function(){var escaped=window.CSS&&CSS.escape?CSS.escape(focusId):focusId.replace(/[^0-9]/g,'');var candidates=document.querySelectorAll('[data-id="'+escaped+'"],a[href$="/'+escaped+'"],form[action$="/'+escaped+'"]');for(var i=0;i<candidates.length;i++){var row=candidates[i].closest('tr,.card');if(row&&row.offsetParent!==null&&!row.classList.contains('approval-focus')){row.classList.add('approval-focus');row.scrollIntoView({behavior:'smooth',block:'center'});return true;}}return false;};
if(!focusDocument()){var observer=new MutationObserver(function(){if(focusDocument())observer.disconnect();});observer.observe(document.body,{childList:true,subtree:true});setTimeout(function(){observer.disconnect();},8000);}
}
document.addEventListener('submit',function(event){
var form=event.target.closest('form.js-confirm');if(!form||form.dataset.confirmed==='1')return;
event.preventDefault();
AppAlert.confirm({title:form.dataset.confirmTitle||'Konfirmasi tindakan',text:form.dataset.confirmText||'Apakah Anda yakin?',confirmButtonText:form.dataset.confirmButton||'Ya, lanjutkan'}).then(function(result){if(result.isConfirmed){form.dataset.confirmed='1';form.requestSubmit();}});
},true);
})();
</script>
<script>
function formatRupiah(num){
return 'Rp ' + new Intl.NumberFormat('id-ID').format(num || 0);
}
function formatTglIndo(dateString){
if(!dateString) return '-';
let date = new Date(dateString);
const bulan = [
"Januari","Februari","Maret","April","Mei","Juni",
"Juli","Agustus","September","Oktober","November","Desember"
];
let tgl = date.getDate();
let bln = bulan[date.getMonth()];
let thn = date.getFullYear();
return `${tgl} ${bln} ${thn}`;
}
</script>
<script>
$(function(){
function initSelectSearch(scope = document){
if (typeof $.fn.select2 === 'undefined') {
console.error('Select2 belum ke-load!');
return;
}
$(scope).find('select.select-search').each(function(){
let $this = $(this);
if ($this.hasClass("select2-hidden-accessible")) return;
let parent = $this.closest('.modal');
parent = parent.length ? parent : $(document.body);
$this.select2({
width: '100%',
placeholder: $this.attr('placeholder') || '-- Pilih --',
allowClear: true,
dropdownParent: parent
});
});
}
// ✅ INIT AWAL
initSelectSearch();
// $(window).on('load', function(){
// setTimeout(() => {
// initSelectSearch();
// }, 100);
// });
// ✅ AUTO INIT untuk dynamic content (modal, ajax, dll)
const observer = new MutationObserver(function(mutations){
mutations.forEach(function(mutation){
mutation.addedNodes.forEach(function(node){
if (node.nodeType === 1){ // element
initSelectSearch(node);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
</script>
</body>
</html>
<?php if(in_array($active_menu,$selectPages,true)):?><script>$(function(){$('select.select-search').each(function(){if(!$(this).hasClass('select2-hidden-accessible'))$(this).select2({width:'100%',dropdownParent:$(this).closest('.modal').length?$(this).closest('.modal'):$(document.body)});});});</script><?php endif?>
</body></html>
+44 -415
View File
@@ -1,415 +1,44 @@
<?php $role = $this->session->userdata('role'); ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accounting</title>
<link rel="icon" href="<?= base_url('assets/img/accounting-color.png') ?>" type="image/png">
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<!-- Google Font -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="<?= base_url('assets/css/style-custom.css'); ?>" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.4.1/css/responsive.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.4.1/js/dataTables.responsive.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
<!-- Daterange Picker -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<!-- Select2 STABLE -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
<!-- Camera Scanner -->
<script src="https://unpkg.com/html5-qrcode"></script>
</head>
<body>
<!-- ───────────────────────── NAVBAR ───────────────────────── -->
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container">
<!-- Mobile menu button -->
<button class="btn text-white d-lg-none me-2" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu">
<i class="bi bi-list fs-3"></i>
</button>
<img src="<?= base_url('assets/img/accounting-white.png'); ?>" width="50px">
<div class="collapse navbar-collapse d-none d-lg-flex">
<ul class="navbar-nav ms-4">
<!-- DASHBOARD -->
<li class="nav-item">
<a class="nav-link <?= ($active_menu == 'dashboard') ? 'active' : ''?>" href="<?= base_url(); ?>">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($active_menu == 'jurnal') ? 'active' : ''?>" href="<?= base_url('jurnal'); ?>">
<i class="bi bi-journal-plus"></i> Jurnal Umum
</a>
</li>
<?php if($role == 'Admin'): ?>
<li class="nav-item">
<a class="nav-link <?= ($active_menu == 'buku_besar') ? 'active' : ''?>" href="<?= base_url('bukubesar'); ?>">
<i class="bi bi-book"></i> Buku Besar
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['laba_rugi','neraca','neraca_saldo']) ? 'active' : '' ?>" href="#" data-bs-toggle="dropdown">
<i class="bi bi-graph-up"></i> Laporan
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item <?= ($active_menu == 'laba_rugi') ? 'active' : '' ?>" href="<?= base_url('labarugi'); ?>">
<i class="bi bi-graph-up-arrow me-2"></i> Laba Rugi</a>
</li>
<li><a class="dropdown-item <?= ($active_menu == 'neraca') ? 'active' : '' ?>" href="<?= base_url('neraca'); ?>">
<i class="bi bi-bar-chart-line me-2"></i> Neraca</a>
</li>
<li><a class="dropdown-item <?= ($active_menu == 'neraca_saldo') ? 'active' : '' ?>" href="<?= base_url('neracasaldo'); ?>">
<i class="bi bi-clipboard-data me-2"></i> Laporan Harian</a>
</li>
</ul>
</li>
<?php endif; ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['accounts','basejurnal']) ? 'active' : '' ?>" href="#" data-bs-toggle="dropdown">
<i class="bi bi-list-nested"></i> Daftar Akun
</a>
<ul class="dropdown-menu">
<?php if($role == 'Admin'): ?>
<li>
<a class="dropdown-item <?= ($active_menu == 'accounts') ? 'active' : '' ?>"
href="<?= base_url('accounts'); ?>">
<i class="bi bi-list-nested"></i> Base Akun
</a>
</li>
<?php endif; ?>
<li>
<a class="dropdown-item <?= ($active_menu == 'basejurnal') ? 'active' : '' ?>"
href="<?= base_url('basejurnal'); ?>">
<i class="bi bi-arrow-left-right"></i> Base Jurnal
</a>
</li>
</ul>
</li>
<!--<li class="nav-item">-->
<!-- <a class="nav-link <?= ($active_menu == 'asset') ? 'active' : ''?>" href="<?= base_url('asset'); ?>">-->
<!-- <i class="bi bi-book"></i> Asset-->
<!-- </a>-->
<!--</li>-->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['asset','lokasiasset']) ? 'active' : '' ?>"
href="#" data-bs-toggle="dropdown">
<i class="bi bi-archive"></i> Asset
</a>
<ul class="dropdown-menu">
<!-- Daftar Asset -->
<li>
<a class="dropdown-item <?= ($active_menu == 'asset') ? 'active' : '' ?>"
href="<?= base_url('asset'); ?>">
<i class="bi bi-archive me-2"></i> Daftar Asset
</a>
</li>
<li><hr class="dropdown-divider"></li>
<!-- Lokasi Asset -->
<li>
<a class="dropdown-item <?= ($active_menu == 'lokasiasset') ? 'active' : '' ?>"
href="<?= base_url('lokasiasset'); ?>">
<i class="bi bi-geo-alt me-2"></i> Lokasi Asset
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['warehouses','items', 'kode_barang', 'draft_items', 'peralatan_teknisi']) ? 'active' : '' ?>" href="#" data-bs-toggle="dropdown">
<i class="bi bi-box-seam"></i> Gudang
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item <?= ($active_menu == 'warehouses') ? 'active' : '' ?>"
href="<?= base_url('warehouses'); ?>">
<i class="bi bi-building me-2"></i> Daftar Gudang
</a>
</li><hr class="dropdown-divider">
<li>
<a class="dropdown-item <?= ($active_menu == 'draft_items') ? 'active' : '' ?>"
href="<?= base_url('items/draft_items'); ?>">
<i class="bi bi-truck me-2"></i> Daftar Pending
</a>
</li>
<li>
<a class="dropdown-item <?= ($active_menu == 'items') ? 'active' : '' ?>"
href="<?= base_url('items'); ?>">
<i class="bi bi-box me-2"></i> Daftar Barang
</a>
</li><hr class="dropdown-divider">
<li>
<a class="dropdown-item <?= ($active_menu == 'item_historys') ? 'active' : '' ?>"
href="<?= base_url('items/item_barcodes_history'); ?>">
<i class="bi bi-clock me-2"></i> History Barang
</a>
</li>
<li>
<a class="dropdown-item <?= ($active_menu == 'kode_barang') ? 'active' : '' ?>"
href="<?= base_url('kodebarang'); ?>">
<i class="bi bi-code me-2"></i> Daftar Kode Barang
</a>
</li><hr class="dropdown-divider">
<li>
<a class="dropdown-item <?= ($active_menu == 'peralatan_teknisi') ? 'active' : '' ?>"
href="<?= base_url('teknisi'); ?>">
<i class="bi bi-tools me-2"></i> Peralatan Teknisi
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['customers','invoice_unpaid','invoice_paid','payments','invoice_draft']) ? 'active' : '' ?>" href="#" data-bs-toggle="dropdown">
<i class="bi bi-receipt"></i> Invoice
</a>
<ul class="dropdown-menu">
<!-- Customers -->
<li>
<a class="dropdown-item <?= ($active_menu == 'customers') ? 'active' : '' ?>" href="<?= base_url('customers'); ?>">
<i class="bi bi-people me-2"></i> Customers
</a>
</li>
<li><hr class="dropdown-divider"></li>
<!-- Draft -->
<li>
<a class="dropdown-item <?= ($active_menu == 'invoice_draft') ? 'active' : '' ?>" href="<?= base_url('invoices/draft'); ?>">
<i class="bi bi-pencil-square me-2"></i> Draft Invoice
</a>
</li>
<!-- Riwayat Invoice -->
<li>
<a class="dropdown-item <?= ($active_menu == 'invoice_paid') ? 'active' : '' ?>" href="<?= base_url('invoices/paid'); ?>">
<i class="bi bi-check-circle me-2"></i> Riwayat Invoice
</a>
</li>
<!-- Pembayaran -->
<!-- <li>
<a class="dropdown-item <?= ($active_menu == 'payments') ? 'active' : '' ?>" href="<?= base_url('payments'); ?>">
<i class="bi bi-cash-stack me-2"></i> Pembayaran
</a>
</li> -->
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['employees','shift','holidays','attendance_monitoring','leave_requests','generate_schedule']) ? 'active' : '' ?>"
href="#"
data-bs-toggle="dropdown">
<i class="bi bi-people-fill me-1"></i>
Kepegawaian
</a>
<ul class="dropdown-menu shadow border-0 rounded-4 p-2">
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'employees') ? 'active' : '' ?>"
href="<?= base_url('employees'); ?>">
<i class="bi bi-person-vcard-fill me-2"></i>
Data Karyawan
</a>
</li>
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'generate_schedule') ? 'active' : '' ?>"
href="<?= base_url('generateschedule'); ?>">
<i class="bi bi-calendar3-week-fill me-2"></i>
Jadwal Kerja
</a>
</li>
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'attendance_monitoring') ? 'active' : '' ?>"
href="<?= base_url('attendancemonitoring'); ?>">
<i class="bi bi-fingerprint me-2"></i>
Monitoring Absensi
</a>
</li>
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'attendance_monthly') ? 'active' : '' ?>"
href="<?= base_url('attendancemonthly'); ?>">
<i class="bi bi-calendar2-date me-2"></i>
Absensi Bulanan
</a>
</li>
<hr class="dropdown-divider">
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'shift') ? 'active' : '' ?>"
href="<?= base_url('shifts'); ?>">
<i class="bi bi-alarm-fill me-2"></i>
Data Shift
</a>
</li>
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'holidays') ? 'active' : '' ?>"
href="<?= base_url('holidays'); ?>">
<i class="bi bi-calendar2-heart-fill me-2"></i>
Hari Libur
</a>
</li>
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'leave_requests') ? 'active' : '' ?>"
href="<?= base_url('leaverequests'); ?>">
<i class="bi bi-calendar-check-fill me-2"></i>
Cuti & Izin
</a>
</li>
<hr class="dropdown-divider">
<li>
<a class="dropdown-item rounded-3 <?= ($active_menu == 'payroll') ? 'active' : '' ?>"
href="<?= base_url('payroll'); ?>">
<i class="bi bi-cash-stack me-2"></i>
Penggajihan
</a>
</li>
</ul>
</li>
<?php if($role == 'Admin'): ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($active_menu, ['umum','users']) ? 'active' : '' ?>" href="#" data-bs-toggle="dropdown">
<i class="bi bi-gear"></i>
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item <?= ($active_menu == 'umum') ? 'active' : '' ?>" href="<?= base_url('umum'); ?>">
<i class="bi bi-sliders me-2"></i> Umum</a>
</li>
<li><a class="dropdown-item <?= ($active_menu == 'users') ? 'active' : '' ?>" href="<?= base_url('users'); ?>">
<i class="bi bi-person-gear me-2"></i> Users</a>
</li>
</ul>
</li>
<?php endif; ?>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="btn btn-premium dropdown-toggle" href="#" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-1"></i> <?= $this->session->username; ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item <?= ($active_menu == 'profile') ? 'active' : '' ?>" href="<?= base_url('profile'); ?>">
<i class="bi bi-person me-2"></i> Profile</a></li>
<li><a class="dropdown-item" href="<?= base_url('auth/logout'); ?>">
<i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div style="height: 70px;"></div>
<!-- ───────────────────────── SIDEBAR MOBILE ───────────────────────── -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
<span class="brand-mobile">🌿 Sekolah Islami</span>
<button class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link <?= ($active_menu == 'dashboard') ? 'active' : ''?>" href="<?= base_url(); ?>">
<span class="menu-wrap"><i class="bi bi-speedometer2"></i> Dashboard</span>
</a>
</li>
</ul>
<div class="sidebar-divider"></div>
<a href="<?= base_url('logout'); ?>" class="btn btn-logout">
<i class="bi bi-box-arrow-right me-2"></i> Logout
</a>
</div>
</div>
<?php
$active_menu=isset($active_menu)?$active_menu:'dashboard';
$CI=&get_instance();
$CI->load->library('NavigationService');
$menu_items=$CI->navigationservice->items();
$breadcrumbs=$CI->navigationservice->breadcrumb($active_menu);
$quick_actions=$CI->navigationservice->quickActions();
$page_title=end($breadcrumbs);
$CI->config->load('ui_assets',true);
$uiAssets=$CI->config->item('ui_assets');
$dataPages=isset($uiAssets['ui_data_pages'])?$uiAssets['ui_data_pages']:array();
$selectPages=isset($uiAssets['ui_select_pages'])?$uiAssets['ui_select_pages']:array();
$userName=$this->session->userdata('nama')?:$this->session->userdata('username')?:'Pengguna';
$userRole=$this->session->userdata('role')?:'User';
?><!doctype html><html lang="id"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#ee7800">
<meta name="csrf-name" content="<?=$this->security->get_csrf_token_name()?>"><meta name="csrf-token" content="<?=$this->security->get_csrf_hash()?>">
<title><?=html_escape($page_title)?> · Accounting</title>
<link rel="manifest" href="<?=base_url('manifest.webmanifest')?>"><link rel="icon" href="<?=base_url('assets/img/pwa-192.png')?>"><link rel="apple-touch-icon" href="<?=base_url('assets/img/pwa-192.png')?>">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" onerror="this.onerror=null;this.href='<?=base_url('assets/vendor/bootstrap.min.css')?>'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<?php if(in_array($active_menu,$dataPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css"><?php endif?>
<?php if(in_array($active_menu,$selectPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css"><?php endif?>
<link rel="stylesheet" href="<?=base_url('assets/css/style-custom.css').'?v='.filemtime(FCPATH.'assets/css/style-custom.css')?>"><link rel="stylesheet" href="<?=base_url('assets/css/app-final.css').'?v='.filemtime(FCPATH.'assets/css/app-final.css')?>">
<script src="<?=base_url('assets/vendor/jquery.min.js')?>"></script>
<?php if(in_array($active_menu,$dataPages,true)):?><script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-responsive@2.5.0/js/dataTables.responsive.min.js"></script><?php endif?>
<?php if(in_array($active_menu,$selectPages,true)):?><script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script><?php endif?>
</head><body class="app-v2" data-page="<?=html_escape($active_menu)?>">
<a class="visually-hidden-focusable skip-link" href="#mainContent">Lewati ke konten</a>
<div class="app-shell">
<aside class="app-sidebar d-none d-lg-flex">
<a class="sidebar-brand" href="<?=base_url()?>"><span class="brand-mark"><img src="<?=base_url('assets/img/accounting-white.png')?>" alt=""></span><span><strong>Accounting</strong><small>Business Suite</small></span></a>
<div class="sidebar-search"><i class="bi bi-search"></i><input class="menu-filter" type="search" placeholder="Cari menu..." aria-label="Cari menu"></div>
<nav class="sidebar-nav" aria-label="Navigasi utama"><?php $this->load->view('partials/navigation_items',compact('menu_items','active_menu'))?></nav>
<div class="sidebar-user"><span class="user-avatar"><?=html_escape(strtoupper(substr($userName,0,1)))?></span><span class="user-copy"><strong><?=html_escape($userName)?></strong><small><?=html_escape($userRole)?></small></span><a href="<?=base_url('auth/logout')?>" title="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
</aside>
<section class="app-workspace">
<header class="app-topbar">
<div class="topbar-leading"><button class="icon-button d-lg-none" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-label="Buka menu"><i class="bi bi-list"></i></button><div><span class="topbar-kicker">Accounting Workspace</span><h1><?=html_escape($page_title)?></h1></div></div>
<div class="topbar-actions"><button class="icon-button" data-bs-toggle="modal" data-bs-target="#menuSearchModal" aria-label="Cari menu"><i class="bi bi-search"></i></button><button class="icon-button quick-button" data-bs-toggle="modal" data-bs-target="#quickActions" aria-label="Aksi cepat"><i class="bi bi-lightning-charge"></i></button><a class="icon-button d-lg-none" href="<?=base_url('auth/logout')?>" aria-label="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
</header>
<div class="offcanvas offcanvas-start app-mobile-drawer" id="mobileMenu" tabindex="-1"><div class="offcanvas-header"><a class="drawer-brand" href="<?=base_url()?>"><span class="brand-mark"><img src="<?=base_url('assets/img/accounting-white.png')?>" alt=""></span><span><strong>Accounting</strong><small>Business Suite</small></span></a><button class="btn-close" data-bs-dismiss="offcanvas" aria-label="Tutup"></button></div><div class="offcanvas-body"><div class="sidebar-search"><i class="bi bi-search"></i><input class="menu-filter" type="search" placeholder="Cari menu..."></div><nav class="sidebar-nav mobile-nav"><?php $this->load->view('partials/navigation_items',compact('menu_items','active_menu'))?></nav></div></div>
<div class="modal fade" id="menuSearchModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">NAVIGASI</small><h5 class="modal-title">Cari menu</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="command-search"><i class="bi bi-search"></i><input class="form-control form-control-lg menu-filter" type="search" placeholder="Ketik nama menu..." autofocus></div><div class="search-menu-results mt-3"></div></div></div></div></div>
<div class="modal fade" id="quickActions" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">SHORTCUT</small><h5 class="modal-title">Aksi cepat</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body quick-grid"><?php foreach($quick_actions as$q):?><a class="quick-action" href="<?=base_url($q['url'])?>"><i class="bi <?=$q['icon']?>"></i><span><?=html_escape($q['label'])?></span></a><?php endforeach?></div></div></div></div>
<main id="mainContent" class="app-content"><div class="app-breadcrumb"><nav aria-label="breadcrumb"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="<?=base_url()?>"><i class="bi bi-house-door"></i></a></li><?php foreach($breadcrumbs as$i=>$crumb):?><li class="breadcrumb-item <?=$i===count($breadcrumbs)-1?'active':''?>"><?=html_escape($crumb)?></li><?php endforeach?></ol></nav></div><div id="globalLoading" class="global-loading" hidden><div class="loading-card"><div class="spinner-border"></div><span>Memproses data…</span></div></div>
@@ -0,0 +1,14 @@
<?php foreach($menu_items as$item):
$children=isset($item['children'])?$item['children']:array();
$active=$active_menu===$item['key'];
foreach($children as$child)$active=$active||$active_menu===$child['key'];
$searchLabel=strtolower($item['label'].' '.implode(' ',array_column($children,'label')));
?>
<?php if($children):?>
<details class="app-menu-item nav-group <?=$active?'is-active':''?>" data-menu-label="<?=html_escape($searchLabel)?>" <?=$active?'open':''?>>
<summary class="nav-entry"><span class="nav-entry-icon"><i class="bi <?=$item['icon']?>"></i></span><span class="nav-entry-label"><?=html_escape($item['label'])?></span><?php if(!empty($item['badge'])):?><span class="nav-badge <?=html_escape($item['badge_class']??'')?>" title="<?=html_escape($item['badge_title']??'')?>"><?=(int)$item['badge']?></span><?php endif?><i class="bi bi-chevron-down nav-chevron"></i></summary>
<div class="nav-children"><?php foreach($children as$child):?><a class="nav-child <?=$active_menu===$child['key']?'active':''?>" href="<?=base_url($child['url'])?>"><span></span><?=html_escape($child['label'])?><?php if(!empty($child['badge'])):?><span class="nav-badge <?=html_escape($child['badge_class']??'')?> ms-auto" title="<?=html_escape($child['badge_title']??'')?>"><?=(int)$child['badge']?></span><?php endif?></a><?php endforeach?></div>
</details>
<?php else:?>
<div class="app-menu-item" data-menu-label="<?=html_escape($searchLabel)?>"><a class="nav-entry <?=$active?'active':''?>" href="<?=base_url($item['url'])?>"><span class="nav-entry-icon"><i class="bi <?=$item['icon']?>"></i></span><span class="nav-entry-label"><?=html_escape($item['label'])?></span><?php if(!empty($item['badge'])):?><span class="nav-badge <?=html_escape($item['badge_class']??'')?>" title="<?=html_escape($item['badge_title']??'')?>"><?=(int)$item['badge']?></span><?php endif?></a></div>
<?php endif;endforeach?>
+48
View File
@@ -0,0 +1,48 @@
<div class="container-fluid py-3">
<div class="mb-3"><h4 class="mb-1">Hutang & Pembayaran Supplier</h4><p class="text-muted mb-0">Pilih supplier dan invoice, tentukan nominal pembayaran di panel kanan, lalu gunakan satu atau beberapa akun Kas/Bank.</p></div>
<div class="row g-3">
<div class="col-lg-5"><div class="card h-100"><div class="card-header bg-body"><b>Daftar Tagihan Belum Lunas</b><small class="d-block text-muted">Daftar ini hanya untuk memilih tagihan; nominal pembayaran diisi di panel kanan.</small></div><div class="card-body">
<label class="form-label">Supplier</label><select id="supplierSelect" class="form-select select-search" data-placeholder="Pilih supplier"><option value=""></option><?php foreach($suppliers as$s):?><option value="<?=$s->id?>"><?=html_escape($s->supplier_code.' - '.$s->name)?></option><?php endforeach?></select>
<div id="billScopeWrap" class="mt-3 d-none"><label class="form-label">PO / Invoice Supplier</label><select id="billScope" class="form-select"><option value="all">Semua tagihan terbuka</option></select><small class="text-muted">Pilih semua untuk mengalokasikan pembayaran dari tagihan terlama, atau pilih satu invoice tertentu.</small></div>
<div id="billList" class="mt-3"><div class="text-center text-muted py-5"><i class="bi bi-receipt fs-2 d-block"></i>Pilih supplier untuk melihat tagihan.</div></div>
</div></div></div>
<div class="col-lg-7"><div class="card h-100"><div class="card-header bg-body"><b>Detail & Pembayaran</b><small class="d-block text-muted">Kredit dan cicilan boleh dibayar sebagian, maksimal sebesar sisa tagihan.</small></div><div class="card-body">
<div id="orderDetail" class="alert alert-light border text-muted">Pilih tagihan di sebelah kiri untuk melihat detail pesanan.</div>
<form id="supplierPayment" method="post" enctype="multipart/form-data" data-ajax="true" action="<?=base_url('payables/payment')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="supplier_id" id="paySupplier"><div id="allocationFields"></div>
<div class="row g-3"><div class="col-md-6"><label class="form-label">Tanggal Pembayaran</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-6"><label class="form-label">Referensi Bank</label><input class="form-control" name="reference_no" placeholder="Nomor transfer/cek"></div>
<div class="col-12"><label class="form-label">Sumber Kas/Bank dan Total Uang Keluar</label><div id="paymentSources"><div class="row g-2 source-row mb-2"><div class="col-7"><select class="form-select" name="source_account_id[]" required><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-4"><input class="form-control format-rupiah source-amount" name="source_amount[]" placeholder="Pokok + admin" inputmode="decimal" required></div><div class="col-1 d-grid"><button type="button" class="btn btn-outline-danger remove-source" title="Hapus sumber"><i class="bi bi-x-lg"></i></button></div></div></div><button type="button" class="btn btn-sm btn-outline-primary" id="addSource"><i class="bi bi-plus-lg"></i> Tambah Akun</button><small class="d-block text-muted mt-1">Isi nilai yang benar-benar keluar dari rekening. Sistem memisahkan admin bank dari pokok pembayaran.</small></div>
<div class="col-md-5"><label class="form-label">Admin Bank <span class="text-muted">(opsional)</span></label><input id="bankChargeAmount" class="form-control format-rupiah" name="bank_charge_amount" value="0" inputmode="decimal"><small class="text-muted">Ditambahkan ke total uang keluar.</small></div><div class="col-md-7"><label class="form-label">Akun Biaya Admin</label><select id="bankChargeAccount" class="form-select" name="bank_charge_account_id"><?php foreach($bank_charge_accounts as$a):?><option value="<?=$a->id?>" <?=$a->id===$bank_charge_account_id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div>
<div class="col-12"><div class="border rounded p-3 bg-light"><div class="d-flex justify-content-between"><span>Total tagihan dipilih</span><b id="selectedBalance">Rp 0,00</b></div><div class="d-flex justify-content-between"><span>Pokok pembayaran</span><b id="principalTotal">Rp 0,00</b></div><div class="d-flex justify-content-between"><span>Admin bank</span><b id="bankChargeDisplay">Rp 0,00</b></div><div class="d-flex justify-content-between fw-semibold"><span>Total sumber Kas/Bank</span><b id="sourceTotal">Rp 0,00</b></div><div class="d-flex justify-content-between"><span>Dialokasikan ke invoice</span><b id="allocatedTotal">Rp 0,00</b></div><div class="d-flex justify-content-between border-top mt-2 pt-2"><span>Sisa tagihan setelah pembayaran</span><b id="paymentDifference">Rp 0,00</b></div><div class="small text-muted mt-2" id="paymentGuidance">Nominal pembayaran boleh lebih kecil dari saldo untuk kredit atau cicilan.</div></div></div>
<div class="col-12"><div class="border rounded p-3 bg-light"><label class="form-label mb-1">Bukti Pembayaran <span class="text-danger">*</span></label><p class="small text-muted mb-2">Unggah bukti transfer, foto struk, atau dokumen pembayaran. Bisa memilih beberapa file sekaligus.</p><input id="paymentEvidence" class="attachment-picker-hidden" type="file" name="payment_documents[]" accept="image/*,.pdf,application/pdf" multiple><small class="text-muted">Wajib. Maksimal 10 file, masing-masing 5 MB.</small><div id="paymentEvidenceGallery" class="attachment-gallery"></div><div id="paymentEvidenceStatus" class="small text-danger mt-2"><i class="bi bi-exclamation-circle me-1"></i>Belum ada bukti pembayaran.</div></div></div>
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" name="allow_advance" value="1" id="allowAdvance"><label class="form-check-label" for="allowAdvance">Catat kelebihan pembayaran sebagai uang muka supplier</label></div></div><div class="col-12 text-end"><button class="btn btn-success" id="postPayment" disabled><i class="bi bi-check2-circle me-1"></i>Post Pembayaran</button></div>
</div>
</form><hr><h6>Riwayat Pembayaran Supplier</h6><div id="paymentHistory" class="text-muted">Belum ada supplier yang dipilih.</div>
</div></div></div>
</div>
<div class="card mt-3"><div class="card-header bg-body"><b>Ringkasan Aging Hutang</b></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Supplier</th><th class="text-end">Belum Jatuh Tempo</th><th class="text-end">1–30 Hari</th><th class="text-end">31–60 Hari</th><th class="text-end">61–90 Hari</th><th class="text-end">&gt;90 Hari</th><th class="text-end">Total</th><th></th></tr></thead><tbody><?php foreach($aging as$r):?><tr><td><?=html_escape($r->name)?></td><?php foreach(array('current_amount','days_1_30','days_31_60','days_61_90','over_90','total')as$f):?><td class="text-end"><?=number_format($r->$f,2,',','.')?></td><?php endforeach?><td><a class="btn btn-sm btn-outline-secondary" href="<?=base_url('payables/statement/'.$r->supplier_id)?>">Statement</a></td></tr><?php endforeach?></tbody></table></div></div>
</div>
<div class="modal fade attachment-preview-modal" id="paymentEvidencePreview" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content"><div class="modal-header py-2"><h5 class="modal-title text-truncate" id="paymentEvidencePreviewTitle">Preview Bukti Pembayaran</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button></div><div class="modal-body p-2 text-center" id="paymentEvidencePreviewBody"></div></div></div></div>
<style>
.attachment-picker-hidden{position:absolute!important;width:1px!important;height:1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important}.attachment-gallery{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}.attachment-add-tile{width:92px;height:92px;border:2px dashed var(--bs-border-color);border-radius:.5rem;background:var(--bs-body-bg);color:var(--bs-secondary-color);display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:.15s ease}.attachment-add-tile:hover{border-color:var(--bs-primary);color:var(--bs-primary);background:rgba(var(--bs-primary-rgb),.04)}.attachment-gallery .card{position:relative;width:92px;flex:0 0 92px;overflow:visible!important;cursor:pointer}.attachment-gallery img{width:100%;height:62px;object-fit:cover;border-radius:5px}.attachment-gallery .card-body{padding:.35rem}.attachment-gallery small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.attachment-remove{position:absolute;right:-7px;top:-7px;width:25px;height:25px;padding:0;border:2px solid #fff;border-radius:50%;background:#dc3545;color:#fff;line-height:18px;font-size:17px;font-weight:700;z-index:20;box-shadow:0 2px 5px rgba(0,0,0,.35)}.attachment-preview-modal{z-index:2070}.attachment-preview-modal+.modal-backdrop{z-index:2065}.attachment-preview-modal .modal-dialog{width:auto;max-width:min(94vw,1200px);margin:.75rem auto}.attachment-preview-modal .modal-content{max-height:94vh}.attachment-preview-modal .modal-body{overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:160px}.attachment-preview-modal img{display:block;max-width:100%;max-height:calc(94vh - 70px);width:auto;height:auto;object-fit:contain}.attachment-preview-modal iframe{display:block;width:min(90vw,1120px);height:calc(94vh - 70px);min-height:280px;border:0}@media(max-width:575.98px){.attachment-preview-modal .modal-dialog{max-width:calc(100vw - 1rem);margin:.5rem}.attachment-preview-modal iframe{width:calc(100vw - 1.25rem);height:calc(100vh - 85px)}.attachment-preview-modal img{max-height:calc(100vh - 85px)}}
</style>
<script>
document.addEventListener('DOMContentLoaded',function(){
const sel=document.getElementById('supplierSelect'),form=document.getElementById('supplierPayment'),list=document.getElementById('billList'),detail=document.getElementById('orderDetail'),history=document.getElementById('paymentHistory'),billScope=document.getElementById('billScope'),billScopeWrap=document.getElementById('billScopeWrap'),allocationFields=document.getElementById('allocationFields'),selectedBalance=document.getElementById('selectedBalance'),principalTotal=document.getElementById('principalTotal'),bankChargeAmount=document.getElementById('bankChargeAmount'),bankChargeAccount=document.getElementById('bankChargeAccount'),bankChargeDisplay=document.getElementById('bankChargeDisplay'),allocatedTotal=document.getElementById('allocatedTotal'),sourceTotal=document.getElementById('sourceTotal'),paymentDifference=document.getElementById('paymentDifference'),paymentGuidance=document.getElementById('paymentGuidance'),postPayment=document.getElementById('postPayment'),allowAdvance=document.getElementById('allowAdvance'),addSource=document.getElementById('addSource'),paymentSources=document.getElementById('paymentSources'),paySupplier=document.getElementById('paySupplier'),evidenceInput=document.getElementById('paymentEvidence'),evidenceGallery=document.getElementById('paymentEvidenceGallery'),evidenceStatus=document.getElementById('paymentEvidenceStatus'),previewTitle=document.getElementById('paymentEvidencePreviewTitle'),previewBody=document.getElementById('paymentEvidencePreviewBody'),globalLoading=document.getElementById('globalLoading');
const money=n=>'Rp '+Number(n||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2}),esc=v=>$('<div>').text(v??'-').html();let bills=[],evidenceFiles=[],previewUrls=[];
function num(v){return window.parseMoney?window.parseMoney(v):(Number(String(v||0).replace(/\./g,'').replace(',','.'))||0)}
function evidenceExtension(file){return String(file.name||'').split('.').pop().toLowerCase()}
function isPdf(file){return file.type==='application/pdf'||evidenceExtension(file)==='pdf'}
function validEvidence(file){let extension=evidenceExtension(file),allowed=['pdf','jpg','jpeg','jpe','jfif','png','webp','gif','bmp','tif','tiff','heic','heif','avif','jp2','j2k','jpf','jpg2','jpx','ico'];if(file.size>5*1024*1024){AppAlert.toast('Ukuran '+file.name+' melebihi 5 MB.','error');return false}if(!allowed.includes(extension)){AppAlert.toast('File '+file.name+' bukan gambar atau PDF.','error');return false}return true}
function syncEvidenceInput(){if(typeof DataTransfer==='undefined'){AppAlert.toast('Browser tidak mendukung daftar lampiran. Gunakan Chrome, Edge, Firefox, atau Safari terbaru.','error');return false}let transfer=new DataTransfer();evidenceFiles.forEach(file=>transfer.items.add(file));evidenceInput.files=transfer.files;return true}
function renderEvidence(){previewUrls.forEach(url=>URL.revokeObjectURL(url));previewUrls=[];evidenceGallery.innerHTML='';evidenceFiles.forEach(function(file,index){let url=URL.createObjectURL(file);previewUrls.push(url);let card=document.createElement('div');card.className='card';card.title='Klik untuk preview '+file.name;let body=document.createElement('div');body.className='card-body';if(isPdf(file)){let icon=document.createElement('div');icon.className='text-center py-2';icon.innerHTML='<i class="bi bi-file-earmark-pdf fs-2 text-danger"></i>';body.appendChild(icon)}else{let img=document.createElement('img');img.src=url;img.alt='Preview '+file.name;body.appendChild(img)}let name=document.createElement('small');name.textContent=file.name;body.appendChild(name);card.appendChild(body);let remove=document.createElement('button');remove.type='button';remove.className='attachment-remove';remove.innerHTML='&times;';remove.title='Hapus '+file.name;remove.setAttribute('aria-label','Hapus '+file.name);remove.addEventListener('click',function(event){event.preventDefault();event.stopPropagation();evidenceFiles.splice(index,1);syncEvidenceInput();renderEvidence();calculate()});card.addEventListener('click',function(){previewTitle.textContent=file.name;previewBody.innerHTML='';if(isPdf(file)){let frame=document.createElement('iframe');frame.src=url;frame.title=file.name;previewBody.appendChild(frame)}else{let image=document.createElement('img');image.src=url;image.alt=file.name;previewBody.appendChild(image)}bootstrap.Modal.getOrCreateInstance(document.getElementById('paymentEvidencePreview')).show()});card.appendChild(remove);evidenceGallery.appendChild(card)});let add=document.createElement('button');add.type='button';add.className='attachment-add-tile';add.innerHTML='<i class="bi bi-plus-lg fs-3"></i><small>Tambah</small>';add.title='Tambah bukti pembayaran';add.addEventListener('click',()=>evidenceInput.click());evidenceGallery.appendChild(add);evidenceStatus.className='small mt-2 '+(evidenceFiles.length?'text-success':'text-danger');evidenceStatus.innerHTML=evidenceFiles.length?'<i class="bi bi-check-circle me-1"></i>'+evidenceFiles.length+' bukti pembayaran siap diunggah.':'<i class="bi bi-exclamation-circle me-1"></i>Belum ada bukti pembayaran.'}
evidenceInput.addEventListener('change',function(){let additions=Array.from(this.files||[]),remaining=Math.max(0,10-evidenceFiles.length);if(additions.length>remaining)AppAlert.toast('Maksimal 10 bukti pembayaran. Hanya '+remaining+' file tambahan yang dapat dimasukkan.','error');additions.slice(0,remaining).forEach(function(file){let duplicate=evidenceFiles.some(existing=>existing.name===file.name&&existing.size===file.size&&existing.lastModified===file.lastModified);if(!duplicate&&validEvidence(file))evidenceFiles.push(file)});syncEvidenceInput();renderEvidence();calculate()});
document.getElementById('paymentEvidencePreview').addEventListener('hidden.bs.modal',function(){previewBody.innerHTML=''});renderEvidence();
function selectedBills(){let scope=billScope.value||'all';return bills.filter(b=>scope==='all'||String(b.id)===scope)}
function planLabel(value){return value==='installment'?'Cicilan':(value==='credit'?'Kredit/Jatuh Tempo':'Pembayaran')}
function calculate(){let selected=selectedBills(),available=selected.reduce((n,b)=>n+Number(b.balance||0),0),source=0,charge=Math.max(0,num(bankChargeAmount.value));document.querySelectorAll('.source-amount').forEach(x=>source+=num(x.value));let principal=Math.max(0,source-charge),remaining=Math.min(principal,available),allocated=0;document.querySelectorAll('.allocation').forEach(x=>x.value=0);selected.forEach(function(b){let value=Math.min(remaining,Number(b.balance||0)),hidden=document.querySelector('.allocation[data-id="'+b.id+'"]');if(hidden)hidden.value=value;allocated+=value;remaining-=value;});let excess=Math.max(0,principal-available),balance=Math.max(0,available-allocated),validCharge=!charge||!!bankChargeAccount.value;selectedBalance.textContent=money(available);principalTotal.textContent=money(principal);bankChargeDisplay.textContent=money(charge);sourceTotal.textContent=money(source);allocatedTotal.textContent=money(allocated);paymentDifference.textContent=money(balance);paymentDifference.className=balance<=.001?'text-success':'text-warning';let installment=selected.length===1&&selected[0].payment_plan==='installment',credit=selected.some(b=>b.payment_plan==='credit');if(source<=charge&&source>0)paymentGuidance.innerHTML='<span class="text-danger">Total sumber Kas/Bank harus lebih besar daripada admin bank.</span>';else if(!validCharge)paymentGuidance.innerHTML='<span class="text-danger">Pilih akun biaya admin bank.</span>';else if(excess>.001&&!allowAdvance.checked)paymentGuidance.innerHTML='<span class="text-danger">Pokok pembayaran melebihi sisa tagihan sebesar <b>'+money(excess)+'</b>. Kurangi nominal atau aktifkan uang muka.</span>';else if(excess>.001)paymentGuidance.innerHTML='<span class="text-primary">Kelebihan pokok <b>'+money(excess)+'</b> akan dicatat sebagai uang muka supplier. Admin '+money(charge)+' dicatat sebagai beban.</span>';else if(installment)paymentGuidance.textContent='Pokok cicilan '+money(principal)+' mengurangi jadwal terlama; admin '+money(charge)+' otomatis menjadi beban.';else if(credit)paymentGuidance.textContent='Pokok pembayaran '+money(principal)+' mengurangi hutang; admin '+money(charge)+' otomatis menjadi beban.';else paymentGuidance.textContent='Pokok pembayaran dialokasikan ke invoice terlama. Admin bank tidak mengurangi saldo invoice.';postPayment.disabled=!(selected.length&&principal>0&&source>charge&&validCharge&&evidenceFiles.length&&(excess<=.001||allowAdvance.checked))}
function showBill(id){let b=bills.find(x=>Number(x.id)===Number(id));if(!b)return;let overdue=b.due_state==='overdue',extra=Number(b.shipping_cost||0)+Number(b.insurance_cost||0)+Number(b.service_fee||0),deduction=Number(b.discount_amount||0)+Number(b.cashback_amount||0);detail.innerHTML='<div class="d-flex justify-content-between gap-2"><div><small class="text-muted">Invoice Supplier</small><h5>'+esc(b.internal_no)+' / '+esc(b.supplier_invoice_no)+'</h5></div><span class="badge bg-'+(overdue?'danger':'warning')+' align-self-start">'+(overdue?'Sudah jatuh tempo':'Belum jatuh tempo')+' · '+esc(b.due_date)+'</span></div><div class="row g-2 small mb-3"><div class="col-6">PO: <b>'+esc(b.po_no)+'</b></div><div class="col-6">Penerimaan: <b>'+esc(b.receipt_no)+'</b></div><div class="col-6">Rencana: <b>'+planLabel(b.payment_plan)+'</b></div><div class="col-6">Total Invoice/PO: <b>'+money(b.total)+'</b></div><div class="col-6">Sudah Dibayar: <b>'+money(b.paid_amount)+'</b></div><div class="col-6">Sisa Hutang: <b>'+money(b.balance)+'</b></div></div><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Barang</th><th class="text-end">Qty</th><th class="text-end">Harga</th><th class="text-end">Total</th></tr></thead><tbody>'+b.items.map(i=>'<tr><td>'+esc((i.kode_barang||'-')+' - '+(i.nama_barang||'-'))+'</td><td class="text-end">'+Number(i.qty||0).toLocaleString('id-ID')+'</td><td class="text-end">'+money(i.unit_price)+'</td><td class="text-end">'+money(i.line_total)+'</td></tr>').join('')+'</tbody></table></div><div class="row justify-content-end"><div class="col-md-7 small"><div class="d-flex justify-content-between"><span>Subtotal barang + pajak</span><span>'+money(Number(b.po_subtotal||0)+Number(b.po_tax_total||0))+'</span></div><div class="d-flex justify-content-between"><span>Ongkir, asuransi & layanan</span><span>'+money(extra)+'</span></div><div class="d-flex justify-content-between"><span>Diskon & cashback</span><span>- '+money(deduction)+'</span></div><div class="d-flex justify-content-between fw-bold border-top mt-1 pt-1"><span>Total Invoice/PO</span><span>'+money(b.total)+'</span></div></div></div>';if((b.installment_schedules||[]).length)detail.insertAdjacentHTML('beforeend','<h6 class="mt-3">Jadwal Cicilan</h6><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Ke</th><th>Jatuh Tempo</th><th class="text-end">Nominal</th><th class="text-end">Terbayar</th><th>Status</th></tr></thead><tbody>'+b.installment_schedules.map(s=>'<tr><td>'+Number(s.installment_no)+'</td><td>'+esc(s.due_date)+'</td><td class="text-end">'+money(s.amount)+'</td><td class="text-end">'+money(s.paid_amount)+'</td><td><span class="badge bg-'+(s.status==='paid'?'success':(s.status==='partial'?'warning':'secondary'))+'">'+esc(s.status)+'</span></td></tr>').join('')+'</tbody></table></div>');document.querySelectorAll('.bill-card').forEach(x=>x.classList.toggle('border-primary',Number(x.dataset.id)===Number(id)))}
function applyBillScope(){let selected=selectedBills();if(selected.length===1)showBill(selected[0].id);else if(selected.length>1)detail.innerHTML='<div class="alert alert-primary mb-3"><b>'+selected.length+' tagihan dipilih.</b><div>Total saldo yang dapat dibayar: '+money(selected.reduce((n,b)=>n+Number(b.balance||0),0))+'</div><div class="small mt-1">Pembayaran akan dialokasikan dari invoice dengan jatuh tempo terlama.</div></div>';calculate()}
async function load(){let id=sel.value;paySupplier.value=id;bills=[];billScopeWrap.classList.add('d-none');billScope.innerHTML='<option value="all">Semua tagihan terbuka</option>';allocationFields.innerHTML='';postPayment.disabled=true;if(!id){list.innerHTML='<div class="text-center text-muted py-5">Pilih supplier.</div>';detail.innerHTML='<div class="text-muted">Pilih supplier dan tagihan yang akan dibayar.</div>';calculate();return}list.innerHTML='<div class="text-center py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat tagihan...</div>';try{let r=await fetch('<?=base_url('payables/open_bills/')?>'+id,{headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),j=await r.json();if(!r.ok||!j.status)throw Error(j.message||'Gagal memuat tagihan');bills=j.bills||[];let pending=j.pending||[];allocationFields.innerHTML=bills.map(b=>'<input type="hidden" class="allocation" data-id="'+Number(b.id)+'" name="allocations['+Number(b.id)+']" value="0">').join('');billScope.innerHTML='<option value="all">Semua tagihan terbuka ('+bills.length+')</option>'+bills.map(b=>'<option value="'+Number(b.id)+'">'+esc((b.po_no||'-')+' / '+b.internal_no+' — '+money(b.balance))+'</option>').join('');billScopeWrap.classList.toggle('d-none',!bills.length);list.innerHTML=bills.map(b=>'<button type="button" class="bill-card text-start border rounded p-3 w-100 mb-2 bg-body" data-id="'+Number(b.id)+'"><div class="d-flex justify-content-between gap-2"><div><b>'+esc(b.po_no||'-')+'</b><small class="d-block">'+esc(b.internal_no)+' / '+esc(b.supplier_invoice_no)+'</small></div><span class="badge bg-'+(b.due_state==='overdue'?'danger':'warning')+' text-'+(b.due_state==='overdue'?'white':'dark')+'">'+planLabel(b.payment_plan)+'</span></div><div class="row g-1 small mt-2"><div class="col-6 text-muted">Total Invoice/PO</div><div class="col-6 text-end fw-semibold">'+money(b.total)+'</div><div class="col-6 text-muted">Sudah Dibayar</div><div class="col-6 text-end">'+money(b.paid_amount)+'</div><div class="col-6 text-muted">Sisa Tagihan</div><div class="col-6 text-end fw-bold text-primary">'+money(b.balance)+'</div></div><small class="text-muted">'+(b.due_state==='overdue'?'Sudah jatuh tempo':'Dapat dibayar sebelum jatuh tempo')+' · '+esc(b.due_date)+'</small></button>').join('');if(!bills.length){list.innerHTML=pending.length?'<div class="alert alert-warning"><b>'+pending.length+' invoice belum menjadi hutang.</b><div class="small mt-1">Selesaikan finalisasi di Purchase Workflow → Invoice Supplier.</div></div>':'<div class="alert alert-success">Tidak ada hutang terbuka untuk supplier ini.</div>';detail.innerHTML='<div class="text-muted">Tidak ada invoice berstatus Parsial yang dapat dibayar.</div>'}history.innerHTML=(j.history||[]).length?'<div class="table-responsive"><table class="table table-sm"><thead><tr><th>Tanggal/No.</th><th>Tagihan</th><th>Sumber</th><th class="text-end">Alokasi</th></tr></thead><tbody>'+j.history.map(h=>'<tr><td>'+esc(h.payment_date)+'<small class="d-block">'+esc(h.payment_no)+'</small></td><td>'+esc(h.internal_no||'Uang muka')+'</td><td>'+((h.sources||[]).map(s=>esc(s.kode_akun)+' '+money(s.amount)).join('<br>')||'-')+'</td><td class="text-end">'+money(h.allocated_amount||h.amount)+'</td></tr>').join('')+'</tbody></table></div>':'<div class="text-muted">Belum ada riwayat pembayaran.</div>';if(bills.length){billScope.value='all';applyBillScope()}else calculate()}catch(e){list.innerHTML='<div class="alert alert-danger"><b>Tagihan gagal dimuat.</b><div>'+esc(e.message)+'</div></div>';detail.innerHTML='<div class="alert alert-danger">Periksa koneksi dan data invoice supplier.</div>';calculate()}}
$(sel).on('change',load);billScope.addEventListener('change',applyBillScope);document.addEventListener('click',function(e){let card=e.target.closest('.bill-card');if(card){billScope.value=String(card.dataset.id);applyBillScope()}if(e.target.closest('.remove-source')){let rows=document.querySelectorAll('.source-row');if(rows.length>1)e.target.closest('.source-row').remove();else e.target.closest('.source-row').querySelector('.source-amount').value='';calculate()}});document.addEventListener('input',e=>{if(e.target.classList.contains('source-amount')||e.target===bankChargeAmount)calculate()});bankChargeAccount.addEventListener('change',calculate);allowAdvance.addEventListener('change',calculate);addSource.onclick=function(){let row=document.querySelector('.source-row').cloneNode(true);row.querySelector('.source-amount').value='';paymentSources.appendChild(row);calculate()};form.onsubmit=async function(e){e.preventDefault();calculate();if(postPayment.disabled){AppAlert.toast('Periksa tagihan, total sumber, admin bank, dan bukti pembayaran.','error');return}if(!evidenceFiles.length){AppAlert.toast('Bukti pembayaran wajib diunggah.','error');return}let gross=0;document.querySelectorAll('.source-amount').forEach(x=>gross+=num(x.value));let charge=Math.max(0,num(bankChargeAmount.value)),principal=Math.max(0,gross-charge),result=await Swal.fire({icon:'question',title:'Post pembayaran supplier?',html:'Pokok pembayaran <b>'+money(principal)+'</b><br>Admin bank <b>'+money(charge)+'</b><br>Total keluar Kas/Bank <b>'+money(gross)+'</b><br><small>Jurnal dan bukti pembayaran akan disimpan.</small>',showCancelButton:true,confirmButtonText:'Ya, post',cancelButtonText:'Batal'});if(!result.isConfirmed)return;postPayment.disabled=true;if(globalLoading)globalLoading.hidden=false;try{form.querySelectorAll('.format-rupiah').forEach(x=>x.value=num(x.value));let response=await fetch(form.action,{method:'POST',body:new FormData(form),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),raw=await response.text(),json;try{json=JSON.parse(raw)}catch(parseError){throw Error('Respons server tidak valid. Periksa log aplikasi.')}if(!response.ok||!json.status)throw Error(json.message||'Pembayaran gagal');if(globalLoading)globalLoading.hidden=true;await Swal.fire({toast:true,position:'top-end',icon:'success',title:json.message+' '+(json.result?.payment_no||''),showConfirmButton:false,timer:2200});location.reload()}catch(error){if(globalLoading)globalLoading.hidden=true;form.querySelectorAll('.format-rupiah').forEach(x=>{if(window.formatNumber)x.value=window.formatNumber(x.value)});Swal.fire({icon:'error',title:'Pembayaran gagal',text:error.message});calculate()}};calculate();
});
</script>
+1
View File
@@ -0,0 +1 @@
<div class="container py-3"><h4>Supplier Statement</h4><p><?=html_escape($supplier->name)?></p><div class="table-responsive"><table class="table"><thead><tr><th>Tanggal</th><th>Dokumen</th><th>Debit/Pembayaran</th><th>Kredit/Tagihan</th><th>Saldo</th></tr></thead><tbody><?php foreach($bills as$b):?><tr><td><?=$b->invoice_date?></td><td><?=html_escape($b->supplier_invoice_no)?></td><td></td><td><?=number_format($b->total,0,',','.')?></td><td><?=number_format($b->balance,0,',','.')?></td></tr><?php endforeach?><?php foreach($payments as$p):?><tr><td><?=$p->payment_date?></td><td><?=$p->payment_no?></td><td><?=number_format($p->amount,0,',','.')?></td><td></td><td></td></tr><?php endforeach?></tbody></table></div></div>
@@ -0,0 +1,16 @@
<?php
$techNavPermissions = isset($equipment_permissions) && is_array($equipment_permissions) ? $equipment_permissions : array();
$techNavPanel = isset($active_panel) ? (string)$active_panel : (isset($panel) ? (string)$panel : '');
$techNavDashboard = isset($equipment_dashboard) && is_array($equipment_dashboard) ? $equipment_dashboard : array();
$techNavInspectionCount = (int)($techNavDashboard['inspection_count'] ?? 0);
$techNavCanBatch = !empty($techNavPermissions['handover']) || !empty($techNavPermissions['transfer']) || !empty($techNavPermissions['return']);
?>
<nav class="tech-subnav" aria-label="Navigasi Peralatan Teknisi">
<a class="<?=$techNavPanel === 'overview' ? 'active' : ''?>" href="<?=base_url('teknisi')?>"><i class="bi bi-speedometer2"></i> Ringkasan</a>
<?php if($techNavCanBatch):?><a class="<?=$techNavPanel === 'batch' ? 'active' : ''?>" href="<?=base_url('teknisi/bulk_workflow')?>"><i class="bi bi-box-arrow-right"></i> Pergerakan Batch</a><?php endif?>
<a class="<?=$techNavPanel === 'technician' ? 'active' : ''?>" href="<?=base_url('teknisi')?>#technicians"><i class="bi bi-person-workspace"></i> Detail Teknisi</a>
<?php if(!empty($techNavPermissions['install'])):?><a class="<?=$techNavPanel === 'replacement' ? 'active' : ''?>" href="<?=base_url('teknisi/replacement_workflow')?>"><i class="bi bi-arrow-repeat"></i> Pemasangan &amp; Penggantian</a><?php endif?>
<?php if(!empty($techNavPermissions['inspect_view']) || !empty($techNavPermissions['inspect'])):?><a class="<?=$techNavPanel === 'inspection' ? 'active' : ''?>" href="<?=base_url('teknisi/inspection_queue')?>"><i class="bi bi-clipboard2-check"></i> Pemeriksaan<?php if($techNavInspectionCount > 0):?> <span class="badge"><?=$techNavInspectionCount?></span><?php endif?></a><?php endif?>
<?php if(!empty($techNavPermissions['view'])):?><a class="<?=$techNavPanel === 'customer' ? 'active' : ''?>" href="<?=base_url('teknisi/customer_register')?>"><i class="bi bi-router"></i> Barang Customer</a>
<a class="<?=$techNavPanel === 'history' ? 'active' : ''?>" href="<?=base_url('teknisi/log_barang')?>"><i class="bi bi-clock-history"></i> Riwayat &amp; PDF</a><?php endif?>
</nav>
@@ -0,0 +1,28 @@
window.technicianStatusCustomers=<?=json_encode(array_map(function($customer){return array('id'=>(int)$customer->id,'name'=>$customer->nama);},$customers),JSON_UNESCAPED_UNICODE|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>;
window.technicianStatusHtmlSafe=function(value){return $('<div>').text(value==null?'':value).html();};
$(document).off('click.technicianStatus','.btn-change-item-status').on('click.technicianStatus','.btn-change-item-status',function(){
const trigger=this,tracking=trigger.dataset.tracking==='UNIT'?'UNIT':'QTY',carried=Number(trigger.dataset.carried||0),installed=Number(trigger.dataset.installed||0),ownerElement=trigger.closest('.modal.show'),ownerModal=ownerElement&&window.bootstrap?bootstrap.Modal.getInstance(ownerElement):null;
if(ownerModal)ownerModal.hide();
const safe=window.technicianStatusHtmlSafe,customers=window.technicianStatusCustomers||[],statusOptions=[];
if(carried>0)statusOptions.push('<option value="installed">Dipasang pada Customer</option>');
if(installed>0)statusOptions.push('<option value="carried">Kembali Dibawa Teknisi</option>');
if(carried>0||installed>0)statusOptions.push('<option value="warehouse">Dikembalikan ke Gudang</option>');
const customerOptions=customers.map(customer=>'<option value="'+customer.id+'"'+(String(customer.id)===String(trigger.dataset.customer||'')?' selected':'')+'>'+safe(customer.name)+'</option>').join('');
const sourceOptions=(carried>0?'<option value="carried">Dari barang yang dibawa ('+carried.toLocaleString('id-ID')+')</option>':'')+(installed>0?'<option value="installed">Dari barang yang dipasang ('+installed.toLocaleString('id-ID')+')</option>':'');
const formHtml='<div class="text-start technician-status-form"><div class="alert alert-light border py-2"><small class="text-muted">Barang / Barcode</small><div class="fw-semibold">'+safe(trigger.dataset.item)+'</div><small>'+safe(trigger.dataset.barcode)+' · '+tracking+'</small><div class="small mt-1">Dibawa: <b>'+carried.toLocaleString('id-ID')+'</b> · Dipasang: <b>'+installed.toLocaleString('id-ID')+'</b></div></div><label class="form-label">Tanggal <span class="text-danger">*</span></label><input id="techStatusDate" type="date" class="form-control mb-3" value="<?=date('Y-m-d')?>"><label class="form-label">Status Tujuan <span class="text-danger">*</span></label><select id="techUsageStatus" class="form-select mb-3">'+statusOptions.join('')+'</select><div id="techSourceGroup" class="mb-3 d-none"><label class="form-label">Asal Barang <span class="text-danger">*</span></label><select id="techSourceStatus" class="form-select">'+sourceOptions+'</select></div><div id="techCustomerGroup" class="mb-3"><label class="form-label">Customer <span class="text-danger">*</span></label><select id="techCustomer" class="form-select"><option value="">Pilih customer</option>'+customerOptions+'</select></div><div id="techQtyGroup" class="mb-3 '+(tracking==='UNIT'?'d-none':'')+'"><label class="form-label">Jumlah yang Diubah <span class="text-danger">*</span></label><input id="techStatusQty" type="number" min="0.0001" step="0.0001" class="form-control" inputmode="decimal"><small id="techQtyHelp" class="text-muted"></small></div><label class="form-label">Keterangan <span class="text-danger">*</span></label><textarea id="techUsageNotes" class="form-control mb-3" rows="3" autocomplete="off" placeholder="Jelaskan alasan atau lokasi perubahan status"></textarea><label class="form-label">Gambar <small class="text-muted">(opsional, maksimal 5 MB)</small></label><input id="techUsageProof" type="file" class="form-control" accept="image/*"></div>';
const requestKey='TECH-'+Date.now()+'-'+Math.random().toString(36).slice(2);
Swal.fire({title:'Ubah Status Peralatan',html:formHtml,icon:'question',showCancelButton:true,confirmButtonText:'Simpan Status',cancelButtonText:'Batal',width:650,focusConfirm:false,allowOutsideClick:false,
didOpen:()=>{
const status=document.getElementById('techUsageStatus'),source=document.getElementById('techSourceStatus'),customerGroup=document.getElementById('techCustomerGroup'),sourceGroup=document.getElementById('techSourceGroup'),qty=document.getElementById('techStatusQty'),qtyHelp=document.getElementById('techQtyHelp');
const refresh=()=>{const target=status.value;customerGroup.classList.toggle('d-none',target!=='installed');sourceGroup.classList.toggle('d-none',target!=='warehouse'||!(carried>0&&installed>0));let available=target==='installed'?carried:(target==='carried'?installed:((source&&source.value==='installed')?installed:carried));if(qty){qty.max=available;qty.value=available;qtyHelp.textContent='Maksimal '+available.toLocaleString('id-ID')+' dari status asal.';}};
status.addEventListener('change',refresh);if(source)source.addEventListener('change',refresh);refresh();
const notes=document.getElementById('techUsageNotes');notes.removeAttribute('readonly');notes.disabled=false;setTimeout(()=>notes.focus(),50);
},
preConfirm:async()=>{
const status=document.getElementById('techUsageStatus').value,date=document.getElementById('techStatusDate').value,customer=document.getElementById('techCustomer').value,source=document.getElementById('techSourceStatus').value,notes=document.getElementById('techUsageNotes').value.trim(),proof=document.getElementById('techUsageProof').files[0],qty=tracking==='UNIT'?1:Number(document.getElementById('techStatusQty').value||0);
if(!date||!status||!notes){Swal.showValidationMessage('Tanggal, status tujuan, dan keterangan wajib diisi.');return false;}if(status==='installed'&&!customer){Swal.showValidationMessage('Customer wajib dipilih untuk barang yang dipasang.');return false;}if(tracking==='QTY'&&qty<=0){Swal.showValidationMessage('Jumlah yang diubah wajib lebih dari 0.');return false;}
const body=new FormData();body.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');body.append('barcode',trigger.dataset.barcode);body.append('user_id',trigger.dataset.user);body.append('status',status);body.append('date',date);body.append('customer_id',customer);body.append('source_status',source);body.append('qty',qty);body.append('notes',notes);body.append('idempotency_key',requestKey);if(proof)body.append('proof',proof);
try{const response=await fetch('<?=base_url('teknisi/update_item_status')?>',{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body}),result=await response.json().catch(()=>({status:false,message:'Respons server tidak valid.'}));if(!response.ok||!result.status)throw Error(result.message||'Status gagal diperbarui.');return result;}catch(error){Swal.showValidationMessage(error.message);return false;}
}
}).then(result=>{if(ownerModal)ownerModal.show();if(!result.isConfirmed)return;if(window.AppAlert)AppAlert.toast(result.value.message,'success');if(typeof window.afterTechnicianStatusChange==='function')window.afterTechnicianStatusChange(result.value);});
});
@@ -0,0 +1,56 @@
<?php
$inventory_title='Pergerakan Peralatan Batch';
$inventory_subtitle='Scan atau cari beberapa barang, lalu posting satu dokumen serah-terima, transfer, atau pengembalian.';
$perm=$equipment_permissions??array();
$active_panel='batch';
$canBatch=!empty($perm['handover'])||!empty($perm['transfer'])||!empty($perm['return']);
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
?>
<link rel="stylesheet" href="<?=base_url('assets/css/technician-equipment.css')?>?v=<?=filemtime(FCPATH.'assets/css/technician-equipment.css')?>">
<div class="tech-shell">
<?php $this->load->view('peralatan_teknisi/_module_nav',get_defined_vars()); ?>
<?php if(!$canBatch):?><div class="alert alert-warning">Role Anda belum mempunyai permission untuk memproses pergerakan batch.</div><?php else:?>
<form id="techBulkForm" data-ajax="true" enctype="multipart/form-data">
<section class="tech-card mt-0">
<div class="tech-card-head"><div><span class="tech-step-label">1 · Pilih proses satu kali</span><h5>Jenis Pergerakan</h5><p>Data header berlaku untuk seluruh barang dalam daftar.</p></div></div>
<div class="tech-bulk-actions" role="group" aria-label="Jenis pergerakan">
<?php if(!empty($perm['handover'])):?><button type="button" class="tech-bulk-action active" data-action="handover"><i class="bi bi-person-plus"></i><strong>Serahkan ke Teknisi</strong><small>Gudang → Teknisi</small></button><?php endif?>
<?php if(!empty($perm['transfer'])):?><button type="button" class="tech-bulk-action" data-action="transfer"><i class="bi bi-people"></i><strong>Transfer Teknisi</strong><small>Teknisi A → Teknisi B</small></button><?php endif?>
<?php if(!empty($perm['return'])):?><button type="button" class="tech-bulk-action" data-action="return"><i class="bi bi-box-arrow-in-down"></i><strong>Kembali ke Gudang</strong><small>Teknisi → Pemeriksaan</small></button><?php endif?>
</div>
<input type="hidden" name="action">
<input type="hidden" name="idempotency_key">
<input type="hidden" name="items_json">
<div class="tech-form-grid mt-3">
<label>Tanggal *<input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></label>
<label data-bulk-field="technician"><span data-tech-label>Karyawan Tujuan</span> *<select class="form-select select-search" name="technician_id" required><option value="">Cari karyawan</option><?php foreach($teknisi_list as$t):?><option value="<?=$t->id?>"><?=html_escape($t->employee_code.' - '.$t->full_name)?></option><?php endforeach?></select></label>
<label data-bulk-field="target" class="d-none">Karyawan Tujuan *<select class="form-select select-search" name="target_technician_id"><option value="">Cari karyawan tujuan</option><?php foreach(($target_teknisi_list??$teknisi_list) as$t):?><option value="<?=$t->id?>"><?=html_escape($t->employee_code.' - '.$t->full_name)?></option><?php endforeach?></select></label>
<label data-bulk-field="warehouse" class="d-none">Gudang Perusahaan<select class="form-select" name="warehouse_id"><option value="">Pilih gudang</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select><small id="techWarehouseHint" class="text-muted">Wajib hanya untuk barang milik perusahaan.</small></label>
<label class="full">Keterangan Transaksi<textarea class="form-control" name="notes" rows="2" placeholder="Opsional; sistem memberi keterangan standar bila dikosongkan"></textarea></label>
</div>
</section>
<section class="tech-card">
<div class="tech-card-head"><div><span class="tech-step-label">2 · Pilih barang</span><h5>Tambahkan Banyak Barang</h5><p>Pilih teknisi asal untuk menampilkan seluruh barang yang sedang dikuasai. Barcode dan pencarian tetap dapat digunakan untuk mempercepat pemilihan.</p></div><span class="tech-status"><strong id="techBulkCount">0</strong>&nbsp;dipilih</span></div>
<div class="tech-scan"><input class="form-control" id="techBulkSearch" autocomplete="off" placeholder="Scan barcode/SN atau ketik nama barang"><button type="button" class="btn btn-primary" id="techBulkSearchButton"><i class="bi bi-search"></i> Cari</button></div>
<div id="techBulkSearchResults" class="tech-search-results d-none"></div>
<div class="tech-technician-stock d-none mt-3" id="techTechnicianStock">
<div class="tech-stock-toolbar"><div><strong>Barang pada Teknisi Asal</strong><small id="techTechnicianStockInfo">Pilih barang yang akan diproses.</small></div><div class="d-flex gap-2"><button type="button" class="btn btn-outline-primary btn-sm" id="techSelectVisible"><i class="bi bi-check2-square"></i> Pilih Semua</button><button type="button" class="btn btn-outline-secondary btn-sm" id="techClearSelection">Kosongkan</button></div></div>
<div class="tech-table-wrap tech-stock-list"><table class="table tech-table align-middle mb-0"><thead><tr><th style="width:46px"><input class="form-check-input" type="checkbox" id="techToggleVisible" aria-label="Pilih semua barang yang tampil"></th><th>Barang</th><th>Barcode / Serial</th><th>Status</th><th>Jenis</th><th>Saldo</th></tr></thead><tbody id="techTechnicianStockBody"><tr><td colspan="6" class="tech-empty">Pilih teknisi asal.</td></tr></tbody></table></div>
</div>
<div class="tech-selected-head mt-3"><strong>Barang yang Dipilih</strong><small>Jumlah QTY dapat disesuaikan sebelum diposting.</small></div>
<div class="tech-table-wrap mt-2"><table class="table tech-table align-middle" id="techBulkItems"><thead><tr><th>Barang</th><th>Barcode / Serial</th><th>Jenis</th><th>Saldo</th><th style="width:150px">Jumlah Diproses</th><th>Keterangan Baris</th><th></th></tr></thead><tbody><tr class="tech-bulk-empty"><td colspan="7" class="tech-empty">Belum ada barang yang dipilih.</td></tr></tbody></table></div>
</section>
<section class="tech-card d-none" data-bulk-field="proof">
<div class="tech-card-head"><div><span class="tech-step-label">3 · Bukti batch</span><h5>Bukti Pengembalian</h5><p>Satu atau beberapa foto/PDF dapat menjadi bukti untuk seluruh barang dalam dokumen ini.</p></div></div>
<div class="tech-file-box" data-filebox><label class="tech-add-file"><i class="bi bi-plus-lg fs-4"></i><span class="small">Tambah</span><input class="d-none" type="file" name="attachments[]" accept="image/*,application/pdf" multiple></label></div>
</section>
<div class="tech-bulk-submit"><div><strong id="techBulkSummary">Belum ada barang dipilih</strong><small>Semua baris diposting sebagai satu dokumen dan satu transaksi database.</small></div><button class="btn btn-primary btn-lg" type="submit" disabled><i class="bi bi-check2-circle"></i> Posting Pergerakan</button></div>
</form>
<?php endif?>
</div>
<div class="modal fade tech-modal" id="techBulkPreviewModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Preview Bukti</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-2" style="height:min(82vh,850px)" data-preview-body></div></div></div></div>
<script>window.TECH_EQUIPMENT_BULK=<?=json_encode(array('base'=>base_url(),'permissions'=>$perm),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/technician-equipment-bulk.js')?>?v=<?=filemtime(FCPATH.'assets/js/technician-equipment-bulk.js')?>"></script>
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="id"><head><meta charset="utf-8"><style>
@page{margin:24px 26px}body{font-family:DejaVu Sans,sans-serif;color:#202733;font-size:9px}h1{font-size:18px;margin:0 0 3px;color:#172845}.muted{color:#68748a}.header{border-bottom:2px solid #f47c00;padding-bottom:10px;margin-bottom:12px}.meta{width:100%;margin:8px 0 14px}.meta td{padding:3px 8px 3px 0}table.data{width:100%;border-collapse:collapse}.data th{background:#172845;color:#fff;text-align:left;padding:7px 6px}.data td{border:1px solid #dfe5ee;padding:6px;vertical-align:top}.data tr:nth-child(even){background:#f7f9fc}.right{text-align:right}.note{white-space:pre-wrap}.footer{margin-top:15px;border-top:1px solid #dfe5ee;padding-top:7px;color:#68748a}
</style></head><body>
<?php $qtyFormat=function($value){$number=(float)$value;return number_format($number,abs($number-round($number))<.0001?0:4,',','.');};$typeLabels=array('opening'=>'Saldo Awal','handover'=>'Penyerahan','install'=>'Pemasangan','remove'=>'Pelepasan','transfer'=>'Transfer','return'=>'Pengembalian','return_customer'=>'Kembali ke Customer','inspection'=>'Pemeriksaan','replacement'=>'Penggantian','correction'=>'Koreksi'); ?>
<div class="header"><div class="muted"><?=html_escape($company->name??$company->company_name??'Perusahaan')?></div><h1>Log Keterangan Barang Customer</h1><div class="muted">Histori keterangan operasional setiap barang tanpa mengubah jurnal accounting</div></div>
<table class="meta"><tr><td><strong>Customer</strong><br><?=html_escape($customer->nama)?></td><td><strong>Jumlah Catatan</strong><br><?=number_format(count($rows),0,',','.')?></td><td><strong>Tanggal Cetak</strong><br><?=date('d/m/Y H:i')?></td><td><strong>Dicetak oleh</strong><br><?=html_escape($printed_by?:'-')?></td></tr></table>
<table class="data"><thead><tr><th>No</th><th>Tanggal / Dokumen</th><th>Barang</th><th>Barcode / Serial</th><th>Proses</th><th>Qty</th><th>Karyawan</th><th>Keterangan</th></tr></thead><tbody>
<?php foreach($rows as$index=>$row):?><tr><td><?=$index+1?></td><td><?=html_escape(date('d/m/Y',strtotime($row['document_date'])))?><br><span class="muted"><?=html_escape($row['document_no'])?></span></td><td><?=html_escape($row['display_name'])?></td><td><?=html_escape($row['barcode_snapshot']?:'-')?><br><span class="muted">SN: <?=html_escape($row['serial_number']?:'-')?></span></td><td><?=html_escape($typeLabels[$row['document_type']]??$row['document_type'])?></td><td class="right"><?=$qtyFormat($row['qty'])?></td><td><?=html_escape($row['technician_name']?:$row['actor_name']?:'-')?></td><td class="note"><?=nl2br(html_escape($row['note_text']))?></td></tr><?php endforeach?>
<?php if(!$rows):?><tr><td colspan="8" style="text-align:center;padding:20px">Belum ada log keterangan untuk customer ini.</td></tr><?php endif?></tbody></table>
<div class="footer">Sumber data: dokumen operasional Peralatan Teknisi. Histori tidak diambil dari perubahan jurnal accounting.</div>
</body></html>
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="id"><head><meta charset="utf-8"><style>
@page{margin:24px 26px}body{font-family:DejaVu Sans,sans-serif;color:#202733;font-size:10px}h1{font-size:18px;margin:0 0 3px;color:#172845}.muted{color:#68748a}.header{border-bottom:2px solid #f47c00;padding-bottom:10px;margin-bottom:12px}.meta{width:100%;margin:8px 0 14px}.meta td{padding:3px 8px 3px 0}table.data{width:100%;border-collapse:collapse}.data th{background:#172845;color:#fff;text-align:left;padding:7px 6px;font-size:9px}.data td{border:1px solid #dfe5ee;padding:6px;vertical-align:top}.data tr:nth-child(even){background:#f7f9fc}.right{text-align:right}.footer{margin-top:15px;border-top:1px solid #dfe5ee;padding-top:7px;color:#68748a}
</style></head><body>
<?php
$qtyFormat=function($value){$number=(float)$value;return number_format($number,abs($number-round($number))<.0001?0:4,',','.');};
$ownershipLabels=array('company'=>'Milik Perusahaan','customer'=>'Milik Customer','transferred_non_inventory'=>'Milik Perusahaan (Non-Persediaan)','unknown'=>'Belum Diketahui');
$statusLabels=array('carried'=>'Sedang Dibawa','installed'=>'Terpasang','removed'=>'Hasil Pelepasan','returned'=>'Dikembalikan','available'=>'Tersedia');
$conditionLabels=array('good'=>'Baik','inspection_required'=>'Perlu Diperiksa','repair_required'=>'Perlu Perbaikan','under_repair'=>'Dalam Perbaikan','damaged'=>'Rusak','lost'=>'Hilang','scrapped'=>'Dihapuskan');
$position=function($row){if($row['custodian_type']==='technician')return'Teknisi: '.($row['technician_name']?:'-');if($row['custodian_type']==='customer')return($row['usage_status']==='installed'?'Terpasang di ':'Tersimpan di ').($row['customer_name']?:'Customer');if($row['custodian_type']==='warehouse')return'Gudang perusahaan: '.($row['warehouse_name']?:'-');if($row['custodian_type']==='quarantine')return'Karantina: '.($row['warehouse_name']?:'-');if($row['custodian_type']==='service_center')return'Pusat perbaikan: '.($row['warehouse_name']?:'-');return$row['custodian_type']?:'-';};
?>
<div class="header"><div class="muted"><?=html_escape($company->name??$company->company_name??'Perusahaan')?></div><h1>Daftar Barang Customer</h1><div class="muted">Laporan posisi operasional non-accounting dan barang perusahaan yang ditempatkan pada customer</div></div>
<table class="meta"><tr><td><strong>Customer</strong><br><?=html_escape($customer->nama)?></td><td><strong>Filter Status</strong><br><?=html_escape($status_label)?></td><td><strong>Tanggal Cetak</strong><br><?=date('d/m/Y H:i')?></td><td><strong>Dicetak oleh</strong><br><?=html_escape($printed_by?:'-')?></td></tr></table>
<table class="data"><thead><tr><th>No</th><th>Barang</th><th>Barcode / Serial</th><th>Pemilik</th><th>Qty</th><th>Posisi</th><th>Status</th><th>Kondisi</th></tr></thead><tbody>
<?php foreach($rows as$index=>$row):?><tr><td><?=$index+1?></td><td><?=html_escape($row['display_name'])?><br><span class="muted"><?=html_escape($row['tracking_type'])?></span></td><td><?=html_escape($row['barcode_snapshot'])?><br><span class="muted">SN: <?=html_escape($row['serial_number']?:'-')?></span></td><td><?=html_escape($ownershipLabels[$row['ownership_type']]??$row['ownership_type'])?></td><td class="right"><?=$qtyFormat($row['qty'])?></td><td><?=html_escape($position($row))?></td><td><?=html_escape($statusLabels[$row['usage_status']]??$row['usage_status'])?></td><td><?=html_escape($conditionLabels[$row['condition_status']]??$row['condition_status'])?></td></tr><?php endforeach?>
<?php if(!$rows):?><tr><td colspan="8" style="text-align:center;padding:20px">Tidak ada barang sesuai filter yang dipilih.</td></tr><?php endif?></tbody></table>
<div class="footer">Dokumen ini tidak membuat atau mengubah jurnal accounting. Sumber data: saldo penguasaan Peralatan Teknisi.</div>
</body></html>
@@ -0,0 +1,30 @@
<?php
$inventory_title='Detail Peralatan Teknisi';
$inventory_subtitle='Saldo barang aktif dan riwayat perubahan status yang dapat ditelusuri.';
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
?>
<div class="container-fluid px-3 mt-3">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div><h4 class="mb-1">Detail Peralatan Teknisi</h4><p class="text-muted mb-0">Kelola barang dibawa, dipasang, dan dikembalikan tanpa menghilangkan histori.</p></div>
<div class="d-flex gap-2"><a href="<?=base_url('teknisi')?>" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Kembali</a><?php if($can_export):?><button type="button" id="technicianDetailPdf" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf me-1"></i>PDF Riwayat</button><?php endif?></div>
</div>
<div class="card border-0 shadow-sm mb-3"><div class="card-body"><div class="row g-3">
<div class="col-sm-6 col-xl-3"><small class="text-muted d-block">Nama Teknisi</small><strong><?=html_escape($technician->full_name)?></strong></div>
<div class="col-sm-6 col-xl-3"><small class="text-muted d-block">Kode Karyawan</small><strong><?=html_escape($technician->employee_code?:'-')?></strong></div>
<div class="col-sm-6 col-xl-3"><small class="text-muted d-block">Jabatan</small><strong><?=html_escape($technician->position_name?:'-')?></strong></div>
<div class="col-sm-6 col-xl-3"><small class="text-muted d-block">Departemen</small><strong><?=html_escape($technician->department_name?:'-')?></strong></div>
</div></div></div>
<div class="card border-0 shadow-sm mb-3"><div class="card-header bg-white py-3"><h5 class="mb-1">Barang Aktif pada Teknisi</h5><small class="text-muted">Barcode QTY dapat memiliki sebagian jumlah dibawa dan sebagian dipasang.</small></div><div class="table-responsive"><table id="technicianCurrentItems" class="table align-middle mb-0 w-100"><thead><tr><th>No</th><th>Tanggal</th><th>Barcode</th><th>Barang</th><th>Serial Number</th><th>Jumlah Aktif</th><th>Status</th><th>Aksi</th></tr></thead><tbody></tbody></table></div></div>
<div class="card border-0 shadow-sm"><div class="card-header bg-white py-3"><h5 class="mb-1">Riwayat Perubahan Status</h5><small class="text-muted">Setiap perpindahan disimpan sebagai audit trail dan tidak menimpa kejadian sebelumnya.</small></div><div class="table-responsive"><table id="technicianStatusHistory" class="table align-middle mb-0 w-100"><thead><tr><th>No</th><th>Tanggal</th><th>Barang</th><th>Barcode</th><th>Perubahan</th><th>Jumlah</th><th>Customer</th><th>Keterangan</th><th>Diproses Oleh</th><th>Bukti</th></tr></thead><tbody></tbody></table></div></div>
</div>
<?php if($can_export):?><div class="modal fade" id="technicianDetailPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content" style="height:min(90vh,900px)"><div class="modal-header"><h5 class="modal-title">PDF Riwayat Peralatan <?=html_escape($technician->full_name)?></h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="PDF riwayat peralatan teknisi" style="width:100%;height:100%;border:0"></iframe></div></div></div></div><?php endif?>
<script>
$(function(){
const current=$('#technicianCurrentItems').DataTable({processing:true,serverSide:true,ajax:{url:'<?=base_url('teknisi/get_teknisi_items')?>',data:{user_id:<?=(int)$technician->id?>}},columns:[0,1,2,3,4,5,6,7].map(i=>({data:i})),columnDefs:[{targets:[7],orderable:false,searchable:false}],order:[[1,'desc']],pageLength:25,responsive:true});
const history=$('#technicianStatusHistory').DataTable({processing:true,serverSide:false,ajax:{url:'<?=base_url('teknisi/get_status_history_data/'.(int)$technician->id)?>',dataSrc:r=>r&&r.status&&Array.isArray(r.data)?r.data:[]},columns:[0,1,2,3,4,5,6,7,8,9].map(i=>({data:i})),columnDefs:[{targets:[9],orderable:false,searchable:false}],order:[],pageLength:25,responsive:true});
window.afterTechnicianStatusChange=function(){current.ajax.reload(null,false);history.ajax.reload(null,false);};
<?php $this->load->view('peralatan_teknisi/_status_change_js',get_defined_vars());?>
$('#technicianDetailPdf').on('click',function(){const modal=document.getElementById('technicianDetailPdfModal');modal.querySelector('iframe').src='<?=base_url('teknisi/log_pdf')?>?user_id=<?=(int)$technician->id?>';bootstrap.Modal.getOrCreateInstance(modal).show();});
$('#technicianDetailPdfModal').on('hidden.bs.modal',function(){$(this).find('iframe').attr('src','about:blank')});
});
</script>
@@ -0,0 +1,28 @@
<?php $inventory_title='Detail Peralatan Teknisi';$inventory_subtitle='Posisi barang, pemasangan customer, hasil pelepasan, pemeriksaan, dan histori dalam satu halaman.';$perm=$equipment_permissions??array();$active_panel='technician';function tech_detail_qty($v){$n=(float)$v;return abs($n-round($n))<.0001?number_format($n,0,',','.'):number_format($n,4,',','.');}function tech_detail_filters($row){$filters=array();if($row['custodian_type']==='technician'&&$row['usage_status']==='carried')$filters[]='carried';if($row['custodian_type']==='customer'&&$row['usage_status']==='installed')$filters[]='installed';if($row['usage_status']==='removed')$filters[]='removed';if($row['condition_status']==='inspection_required')$filters[]='inspection';return implode(' ',$filters);} ?>
<?php $this->load->view('inventory_professional/_module_nav',get_defined_vars()); ?>
<link rel="stylesheet" href="<?=base_url('assets/css/technician-equipment.css')?>?v=<?=filemtime(FCPATH.'assets/css/technician-equipment.css')?>">
<div class="tech-shell">
<?php $this->load->view('peralatan_teknisi/_module_nav',get_defined_vars()); ?>
<section class="tech-card"><div class="tech-card-head"><div><span class="tech-step-label">Penanggung jawab operasional</span><h4 class="mb-1"><?=html_escape($technician->full_name)?></h4><p><?=html_escape(($technician->employee_code??'-').' · '.($technician->department_name??'-').' · '.($technician->position_name??'-'))?></p></div><div class="tech-doc-actions"><a class="btn btn-outline-primary" href="<?=base_url('teknisi')?>#scanner"><i class="bi bi-qr-code-scan"></i> Scan Pergerakan</a><?php if(!empty($perm['export'])):?><button type="button" id="techTechnicianInventoryPdf" class="btn btn-outline-danger tech-detail-pdf" data-base-url="<?=base_url('teknisi/technician_inventory_pdf/'.(int)$technician->id)?>" data-url="<?=base_url('teknisi/technician_inventory_pdf/'.(int)$technician->id.'?status=carried')?>"><i class="bi bi-file-pdf"></i> PDF Sedang Dibawa</button><?php endif?></div></div></section>
<section class="tech-card"><ul class="nav nav-pills gap-2 mb-3" id="techDetailTabs"><li><button class="nav-link active" data-filter="carried">Sedang Dibawa</button></li><li><button class="nav-link" data-filter="installed">Terpasang di Customer</button></li><li><button class="nav-link" data-filter="removed">Hasil Pelepasan</button></li><li><button class="nav-link" data-filter="inspection">Menunggu Pemeriksaan</button></li><li><button class="nav-link" data-filter="all">Semua Posisi</button></li></ul><div class="tech-table-wrap"><table class="table tech-table align-middle" id="techDetailTable"><thead><tr><th>Barcode / Serial</th><th>Barang</th><th>Kepemilikan</th><th>Jumlah</th><th>Customer / Posisi</th><th>Status</th><th>Kondisi</th><th>Aksi</th></tr></thead><tbody>
<?php foreach($allocations as$a):?><tr data-filters="<?=html_escape(tech_detail_filters($a))?>"><td><strong><?=html_escape($a['barcode_snapshot'])?></strong><br><small><?=html_escape($a['serial_number']?:$a['external_serial']?:'-')?></small></td><td><?=html_escape($a['display_name'])?></td><td><?=$a['ownership_type']==='company'?'Perusahaan':'Customer / Non-Persediaan'?></td><td><?=tech_detail_qty($a['qty'])?></td><td><?=html_escape($a['customer_name']?:$a['technician_name']?:$a['warehouse_name']?:'-')?></td><td><span class="tech-status"><?=html_escape($a['usage_status'])?></span></td><td><?=html_escape($a['condition_status'])?></td><td><a class="btn btn-outline-primary btn-sm" href="<?=base_url('teknisi?scan='.rawurlencode($a['barcode_snapshot']))?>#scanner"><i class="bi bi-qr-code-scan"></i> Proses</a></td></tr><?php endforeach?>
<tr id="techDetailEmpty" style="display:none"><td colspan="8" class="tech-empty">Tidak ada barang dengan status yang dipilih.</td></tr></tbody></table></div></section>
<section class="tech-card"><div class="tech-card-head"><div><h5>Riwayat Dokumen</h5><p>Histori tidak dapat dihapus atau diedit.</p></div></div><div class="tech-table-wrap"><table class="table tech-table align-middle"><thead><tr><th>Dokumen</th><th>Tanggal</th><th>Proses</th><th>Customer</th><th>Keterangan</th><th>PDF</th></tr></thead><tbody><?php foreach($documents as$d):?><tr><td><strong><?=html_escape($d['document_no'])?></strong></td><td><?=html_escape($d['document_date'])?></td><td><?=html_escape($d['document_type'])?></td><td><?=html_escape($d['customer_name']?:'-')?></td><td><?=html_escape($d['notes']?:'-')?></td><td><?php if($perm['export']):?><button type="button" class="btn btn-outline-danger btn-sm tech-detail-pdf" data-url="<?=base_url('teknisi/equipment_document_pdf/'.(int)$d['id'])?>"><i class="bi bi-file-pdf"></i></button><?php endif?></td></tr><?php endforeach?><?php if(empty($documents)):?><tr><td colspan="6" class="tech-empty">Belum ada riwayat.</td></tr><?php endif?></tbody></table></div></section>
</div>
<?php if($perm['export']):?><div class="modal fade tech-modal" id="techDetailPdfModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content" style="height:min(90vh,900px)"><div class="modal-header"><h5 class="modal-title">PDF Dokumen Peralatan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="PDF Dokumen Peralatan" style="width:100%;height:100%;border:0"></iframe></div></div></div></div><?php endif?>
<script>
(function(){
var labels={carried:'Sedang Dibawa',installed:'Terpasang di Customer',removed:'Hasil Pelepasan',inspection:'Menunggu Pemeriksaan',all:'Semua Posisi'};
var reportButton=document.getElementById('techTechnicianInventoryPdf');
function applyFilter(filter){
document.querySelectorAll('#techDetailTabs button').forEach(function(button){button.classList.toggle('active',button.dataset.filter===filter)});
var visible=0;document.querySelectorAll('#techDetailTable tbody tr[data-filters]').forEach(function(row){var show=filter==='all'||row.dataset.filters.split(' ').indexOf(filter)!==-1;row.style.display=show?'':'none';if(show)visible++});
var emptyRow=document.getElementById('techDetailEmpty');if(emptyRow){emptyRow.style.display=visible?'none':'';emptyRow.querySelector('td').textContent='Tidak ada barang dengan status '+labels[filter]+'.';}
if(reportButton){reportButton.dataset.url=reportButton.dataset.baseUrl+'?status='+encodeURIComponent(filter);reportButton.innerHTML='<i class="bi bi-file-pdf"></i> PDF '+labels[filter];}
}
document.querySelectorAll('#techDetailTabs button').forEach(function(button){button.addEventListener('click',function(){applyFilter(button.dataset.filter)})});
document.querySelectorAll('.tech-detail-pdf').forEach(function(button){button.addEventListener('click',function(){var el=document.getElementById('techDetailPdfModal');el.querySelector('iframe').src=button.dataset.url;bootstrap.Modal.getOrCreateInstance(el,{backdrop:'static'}).show()})});
var pdfModal=document.getElementById('techDetailPdfModal');if(pdfModal)pdfModal.addEventListener('hidden.bs.modal',function(){pdfModal.querySelector('iframe').src='about:blank'});
applyFilter('carried');
})();
</script>
@@ -0,0 +1,11 @@
<!doctype html><html><head><meta charset="utf-8"><style>
@page{margin:24px 28px}body{font-family:DejaVu Sans,Arial,sans-serif;font-size:10px;color:#172033}.header{border-bottom:2px solid #172845;padding-bottom:10px;margin-bottom:14px}.header h2{margin:0 0 4px}.muted{color:#68748a}.meta{width:100%;border-collapse:collapse;margin-bottom:14px}.meta td{width:33.33%;border:1px solid #dfe5ee;padding:8px;vertical-align:top}.meta span{display:block;color:#68748a;font-size:8px;text-transform:uppercase}.lines{width:100%;border-collapse:collapse}.lines th{background:#172845;color:#fff;text-align:left;padding:7px;font-size:8px}.lines td{border:1px solid #dfe5ee;padding:7px;vertical-align:top}.right{text-align:right}.note{margin-top:12px;border:1px solid #dfe5ee;padding:9px}.sign{margin-top:35px;width:100%;text-align:center}.sign td{width:33.33%;height:70px;vertical-align:bottom}.footer{position:fixed;bottom:-12px;left:0;right:0;color:#788397;font-size:8px;text-align:center}
</style></head><body>
<?php $documentLabels=array('opening'=>'SALDO AWAL MIGRASI','handover'=>'SERAH TERIMA BARANG TEKNISI','install'=>'PEMASANGAN PERALATAN','remove'=>'PELEPASAN PERALATAN','transfer'=>'TRANSFER ANTAR TEKNISI','return'=>'PENGEMBALIAN KE GUDANG','return_customer'=>'PENGEMBALIAN KE CUSTOMER','replacement'=>'PENGGANTIAN PERALATAN','inspection'=>'HASIL PEMERIKSAAN','correction'=>'KOREKSI / REVERSAL');?>
<div class="header"><h2><?=html_escape($company->name??$company->nama??'Perusahaan')?></h2><div class="muted"><?=html_escape($company->address??$company->alamat??'')?> </div><h3>DOKUMEN <?=html_escape($documentLabels[$document['document_type']]??strtoupper($document['document_type']))?></h3></div>
<table class="meta"><tr><td><span>Nomor Dokumen</span><strong><?=html_escape($document['document_no'])?></strong><br><?=date('d/m/Y',strtotime($document['document_date']))?></td><td><span>Teknisi / Customer</span><?=html_escape($document['technician_name']?:'-')?><br><?=html_escape($document['customer_name']?:'-')?></td><td><span>Gudang / Work Order / Lokasi</span><?=html_escape($document['warehouse_name']?:'-')?><br><?=html_escape($document['work_order_no']?:'-')?><br><?=html_escape($document['installation_location']?:'-')?></td></tr></table>
<table class="lines"><thead><tr><th>BARANG</th><th>BARCODE / SERIAL</th><th>KEPEMILIKAN</th><th>POSISI ASAL → TUJUAN</th><th class="right">QTY</th><th>KONDISI</th></tr></thead><tbody><?php foreach($document['lines'] as$l):?><tr><td><?=html_escape($l['display_name'])?></td><td><?=html_escape($l['barcode_snapshot'])?><br><span class="muted"><?=html_escape($l['serial_number']?:'-')?></span></td><td><?=html_escape($l['ownership_type'])?></td><td><?=html_escape(($l['from_custodian_type']?:'-').' → '.($l['to_custodian_type']?:'-'))?></td><td class="right"><?=number_format((float)$l['qty'],abs((float)$l['qty']-round((float)$l['qty']))<.0001?0:4,',','.')?></td><td><?=html_escape(($l['from_condition_status']?:'-').' → '.($l['to_condition_status']?:'-'))?></td></tr><?php endforeach?></tbody></table>
<div class="note"><strong>Alasan / Keterangan</strong><br><?=nl2br(html_escape(trim(($document['reason']?:'')."\n".($document['notes']?:''))))?></div>
<table class="sign"><tr><td>Dibuat / Diproses<br><br><br><strong><?=html_escape($document['creator_name']?:'-')?></strong></td><td>Teknisi<br><br><br><strong><?=html_escape($document['technician_name']?:'-')?></strong></td><td>Customer / Gudang<br><br><br><strong><?=html_escape($document['customer_name']?:$document['warehouse_name']?:'-')?></strong></td></tr></table>
<div class="footer">Dokumen ini merupakan riwayat operasional non-accounting. Dicetak <?=date('d/m/Y H:i')?>.</div>
</body></html>
@@ -1,5 +1,4 @@
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.4.1/css/responsive.bootstrap5.min.css">
<?php $inventory_title='Peralatan Teknisi';$inventory_subtitle='Serah terima, pengembalian, dan perpindahan barang teknisi dengan riwayat barcode yang dapat ditelusuri.';$this->load->view('inventory_professional/_module_nav',get_defined_vars());?>
<style>
:root {
@@ -496,6 +495,7 @@ body {
<th>Serial Number</th>
<th>Jumlah</th>
<th class="text-center" style="width:100px">Status</th>
<th class="text-center" style="width:130px">Aksi</th>
</tr>
</thead>
</table>
@@ -756,7 +756,8 @@ $(function(){
{ data: 3 },
{ data: 4 },
{ data: 5 },
{ data: 6, className: 'text-center' }
{ data: 6, className: 'text-center' },
{ data: 7, className: 'text-center', orderable: false, searchable: false }
],
order: [[1, 'desc']],
// language: { url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/id.json" },
@@ -767,6 +768,9 @@ $(function(){
$('#modalDetailTeknisi').modal('show');
}
window.afterTechnicianStatusChange=function(){if(dtDetailItems)dtDetailItems.ajax.reload(null,false);dtTeknisi.ajax.reload(null,false);};
<?php $this->load->view('peralatan_teknisi/_status_change_js',get_defined_vars());?>
// =============================================================
// FORM BARANG BAWAAN TEKNISI
// =============================================================
@@ -1,155 +1,35 @@
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.4.1/css/responsive.bootstrap5.min.css">
<style>
:root {
--tech-primary: #ff9800;
--tech-primary-light: #fff3e0;
--tech-primary-dark: #ff9800;
}
body {
background: #f5f7fb;
}
.tech-card {
border: none;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
background: #fff;
}
.tech-card .card-header-custom {
background: linear-gradient(135deg, var(--tech-primary), var(--tech-primary-dark));
color: #fff;
padding: 14px 20px;
font-weight: 600;
font-size: 15px;
}
.tech-card .card-body {
padding: 18px;
}
.table-tech thead th {
background: #f8f9fa;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: .4px;
color: #495057;
border-bottom: 2px solid #ffcc80;
padding: 10px 8px;
white-space: nowrap;
}
.table-tech tbody td {
padding: 8px;
vertical-align: middle;
font-size: 13px;
}
.table-tech td .badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 65px;
padding: 0.35em 0.65em;
margin: 0 auto;
}
.table-tech tbody tr:hover {
background: #fff3e0;
}
</style>
<div class="container-fluid px-3 mt-3">
<div class="d-flex align-items-center gap-2 mb-3">
<div class="bg-warning text-white p-2 rounded-3 d-flex align-items-center justify-content-center" style="width:38px;height:38px;">
<i class="bi bi-journal-text fs-5"></i>
</div>
<div class="flex-grow-1">
<h5 class="mb-0 fw-bold" style="color:#1a1a2e;">Log Barang Teknisi</h5>
<small class="text-muted">Riwayat barang yang pernah dibawa oleh teknisi</small>
</div>
<div>
<a href="<?= base_url('teknisi'); ?>" class="btn btn-secondary">
<i class="bi bi-arrow-left me-1"></i> Kembali
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card tech-card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-tech table-striped w-100" id="tblLogBarang">
<thead>
<tr>
<th>No</th>
<th>Nama Teknisi</th>
<th>Kode Barang</th>
<th>Barcode</th>
<th>Nama Barang</th>
<th>Tanggal Dibawa</th>
<th>Tanggal Kembali</th>
<th>Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php
$inventory_title='Log Peralatan Teknisi';
$inventory_subtitle='Telusuri barang yang dibawa, dipasang pada customer, atau dikembalikan oleh teknisi.';
$perm=$equipment_permissions??array();
$active_panel='history';
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
?>
<link rel="stylesheet" href="<?=base_url('assets/css/technician-equipment.css')?>?v=<?=filemtime(FCPATH.'assets/css/technician-equipment.css')?>">
<?php $this->load->view('peralatan_teknisi/_module_nav',get_defined_vars()); ?>
<section class="inventory-card">
<div class="inventory-card-head"><div><h5>Filter Riwayat Peralatan</h5><p>Tanggal mencakup tanggal dibawa, dipasang, atau dikembalikan.</p></div><a href="<?=base_url('teknisi')?>" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Kembali</a></div>
<form id="technicianLogFilter" class="row g-3 align-items-end p-3 border-bottom" data-ajax="true">
<div class="col-sm-6 col-xl-2"><label class="form-label">Dari Tanggal</label><input type="date" class="form-control" name="date_from"></div>
<div class="col-sm-6 col-xl-2"><label class="form-label">Sampai Tanggal</label><input type="date" class="form-control" name="date_to"></div>
<div class="col-md-6 col-xl-2"><label class="form-label">Customer</label><select class="form-select select-search" name="customer_id"><option value="">Semua customer</option><?php foreach($customers as$customer):?><option value="<?=$customer->id?>"><?=html_escape($customer->nama)?></option><?php endforeach?></select></div>
<div class="col-md-6 col-xl-2"><label class="form-label">Karyawan / Teknisi</label><select class="form-select select-search" name="user_id"><option value="">Semua teknisi</option><?php foreach($teknisi_list as$employee):?><option value="<?=$employee->id?>"><?=html_escape($employee->employee_code.' - '.$employee->full_name)?></option><?php endforeach?></select></div>
<div class="col-md-4 col-xl-2"><label class="form-label">Status</label><select class="form-select" name="status"><option value="">Semua</option><option value="carried">Dibawa</option><option value="installed">Dipasang</option><option value="removed">Dilepas</option><option value="returned">Dikembalikan</option></select></div>
<div class="col-md-4 col-xl-2"><label class="form-label">Kondisi</label><select class="form-select" name="condition"><option value="">Semua</option><option value="good">Baik</option><option value="inspection_required">Perlu Diperiksa</option><option value="repair_required">Perlu Perbaikan</option><option value="under_repair">Dalam Perbaikan</option><option value="damaged">Rusak</option><option value="lost">Hilang</option><option value="scrapped">Dihapuskan</option></select></div>
<div class="col-md-4 col-xl-2"><label class="form-label">Kepemilikan</label><select class="form-select" name="ownership"><option value="">Semua</option><option value="company">Perusahaan</option><option value="customer">Customer</option><option value="transferred_non_inventory">Non-Persediaan Diserahkan</option><option value="unknown">Belum Diketahui</option></select></div>
<div class="col-md-8 col-xl-4"><label class="form-label">Barang / Barcode / Serial</label><input class="form-control" name="query" placeholder="Cari identitas barang"></div>
<div class="col-md-4 col-xl-2 d-flex gap-2"><button type="submit" class="btn btn-primary flex-grow-1"><i class="bi bi-funnel me-1"></i>Tampilkan</button><?php if($can_export):?><button type="button" id="technicianLogPdf" class="btn btn-outline-danger" title="Export PDF"><i class="bi bi-file-earmark-pdf"></i></button><?php endif?></div>
</form>
<div class="table-responsive"><table class="table align-middle mb-0 w-100" id="tblLogBarang"><thead><tr><th>No</th><th>Dokumen</th><th>Teknisi</th><th>Barang</th><th>Barcode / SN</th><th>Pemilik</th><th>Qty</th><th>Customer</th><th>Posisi</th><th>Status</th><th>Kondisi</th><th>Keterangan</th><th>Aksi</th></tr></thead><tbody></tbody></table></div>
</section>
<div class="modal fade" id="technicianLogDetailModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Detail Dokumen Peralatan</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<?php if($can_export):?><div class="modal fade" id="technicianLogPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content" style="height:min(90vh,900px)"><div class="modal-header"><h5 class="modal-title">PDF Log Peralatan Teknisi</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="PDF Log Peralatan Teknisi" style="width:100%;height:100%;border:0"></iframe></div></div></div></div><?php endif?>
<script>
$(function(){
$('#tblLogBarang').DataTable({
processing: true,
serverSide: false,
ajax: {
url: "<?= base_url('teknisi/get_log_data'); ?>",
dataSrc: 'data'
},
columns: [
{ data: 0 },
{ data: 1 },
{ data: 2 },
{ data: 3 },
{ data: 4 },
{ data: 5 },
{ data: 6 },
{ data: 7 }
],
pageLength: 25,
lengthMenu: [
[25, 50, 100, -1],
[25, 50, 100, 'All']
],
responsive: true,
searching: true,
paging: true,
ordering: true,
info: true,
language: {
search: "Cari:",
lengthMenu: "Tampilkan _MENU_ data",
info: "Menampilkan _START_ - _END_ dari _TOTAL_ data",
zeroRecords: "Tidak ada data",
paginate: {
first: "Awal",
last: "Akhir",
next: "&raquo;",
previous: "&laquo;"
}
}
});
const form=$('#technicianLogFilter'),table=$('#tblLogBarang').DataTable({processing:true,serverSide:false,ajax:{url:'<?=base_url('teknisi/get_log_data')?>',data:function(request){form.serializeArray().forEach(field=>request[field.name]=field.value)},dataSrc:'data',error:function(xhr){if(window.Swal)Swal.fire({toast:true,position:'top-end',icon:'error',title:(xhr.responseJSON&&xhr.responseJSON.message)||'Riwayat gagal dimuat.',showConfirmButton:false,timer:4000})}},columns:Array.from({length:13},(_,index)=>({data:index})),order:[[1,'desc']],pageLength:25,responsive:true,language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_ data',info:'Menampilkan _START_ - _END_ dari _TOTAL_ data',zeroRecords:'Tidak ada riwayat sesuai filter',paginate:{next:'&raquo;',previous:'&laquo;'}}});
form.on('submit',function(event){event.preventDefault();table.ajax.reload()});
$('#technicianLogPdf').on('click',function(){const modal=document.getElementById('technicianLogPdfModal'),query=form.serialize();modal.querySelector('iframe').src='<?=base_url('teknisi/log_pdf')?>?'+query;bootstrap.Modal.getOrCreateInstance(modal).show()});
$('#technicianLogPdfModal').on('hidden.bs.modal',function(){$(this).find('iframe').attr('src','about:blank')});
$(document).on('click','.tech-log-detail',function(){const id=this.dataset.id,$modal=$('#technicianLogDetailModal'),esc=v=>$('<div>').text(v==null?'':v).html();$modal.find('.modal-body').html('<div class="text-center p-5"><span class="spinner-border text-primary"></span></div>');bootstrap.Modal.getOrCreateInstance($modal[0],{backdrop:'static'}).show();$.getJSON('<?=base_url('teknisi/equipment_document/')?>'+id).done(function(r){const d=r.result,lines=(d.lines||[]).map(l=>'<tr><td>'+esc(l.display_name)+'</td><td>'+esc(l.barcode_snapshot)+'</td><td>'+Number(l.qty).toLocaleString('id-ID',{maximumFractionDigits:4})+'</td><td>'+esc(l.from_custodian_type)+' &rarr; '+esc(l.to_custodian_type)+'</td><td>'+esc(l.from_condition_status)+' &rarr; '+esc(l.to_condition_status)+'</td></tr>').join('');$modal.find('.modal-body').html('<div class="row g-3 mb-3"><div class="col-md-4"><small>Nomor Dokumen</small><div class="fw-bold">'+esc(d.document_no)+'</div></div><div class="col-md-4"><small>Teknisi / Customer</small><div>'+esc(d.technician_name||'-')+' / '+esc(d.customer_name||'-')+'</div></div><div class="col-md-4"><small>Tanggal / Proses</small><div>'+esc(d.document_date)+' / '+esc(d.document_type)+'</div></div></div><div class="table-responsive"><table class="table"><thead><tr><th>Barang</th><th>Barcode</th><th>Qty</th><th>Posisi</th><th>Kondisi</th></tr></thead><tbody>'+lines+'</tbody></table></div><p><strong>Keterangan:</strong> '+esc(d.notes||'-')+'</p>')}).fail(function(){$modal.find('.modal-body').html('<div class="alert alert-danger">Detail gagal dimuat.</div>')})});
});
</script>
</script>
@@ -0,0 +1,10 @@
<!doctype html><html lang="id"><head><meta charset="utf-8"><title>Log Peralatan Teknisi</title><style>
@page{margin:24px 28px}body{font-family:DejaVu Sans,Arial,sans-serif;color:#172033;font-size:8px}.head{display:table;width:100%;border-bottom:2px solid #ef7d00;padding-bottom:10px}.head>div{display:table-cell;vertical-align:bottom}.right{text-align:right}.company{font-size:15px;font-weight:700}h1{font-size:17px;margin:14px 0 3px}.filter{color:#667085;margin-bottom:12px}table{width:100%;border-collapse:collapse}th{background:#f1f4f8;text-transform:uppercase;font-size:7px;color:#475467}th,td{border:1px solid #dfe5ed;padding:6px;vertical-align:top}tbody tr:nth-child(even){background:#fafbfc}.empty{text-align:center;padding:25px;color:#667085}.status{font-weight:700}.footer{margin-top:10px;color:#667085}
</style></head><body>
<div class="head"><div><div class="company"><?=html_escape($company?($company->name??$company->company_name??'Perusahaan'):'Perusahaan')?></div><div>Dokumen Persediaan</div></div><div class="right">Dicetak <?=date('d/m/Y H:i')?></div></div>
<?php $statusLabels=array('carried'=>'Dibawa','installed'=>'Dipasang','removed'=>'Dilepas','returned'=>'Dikembalikan');?>
<h1>Log Peralatan Teknisi</h1><div class="filter">Periode: <?=html_escape($filters['date_from']?:'Semua')?> s.d. <?=html_escape($filters['date_to']?:'Semua')?> &middot; Customer: <?=$filters['customer_id']?'Sesuai filter':'Semua'?> &middot; Teknisi: <?=$filters['technician_id']?'Sesuai filter':'Semua'?> &middot; Status: <?=html_escape($statusLabels[$filters['status']??'']??'Semua')?> &middot; Kondisi: <?=html_escape($filters['condition']?:'Semua')?> &middot; Kepemilikan: <?=html_escape($filters['ownership']?:'Semua')?> &middot; Barang: <?=html_escape($filters['query']?:'Semua')?></div>
<table><thead><tr><th>No</th><th>Dokumen</th><th>Teknisi</th><th>Barang</th><th>Barcode / Serial</th><th>Pemilik</th><th>Qty</th><th>Customer</th><th>Posisi</th><th>Status / Kondisi</th><th>Keterangan</th></tr></thead><tbody>
<?php foreach($rows as$index=>$row):?><tr><td><?=$index+1?></td><td><?=html_escape($row['document_no'])?><br><?=html_escape(date('d/m/Y',strtotime($row['document_date'])))?></td><td><?=html_escape($row['technician_name']?:'-')?></td><td><?=html_escape($row['display_name'])?></td><td><?=html_escape($row['barcode_snapshot'])?><br><?=html_escape($row['serial_number']?:'-')?></td><td><?=html_escape($row['ownership_type'])?></td><td><?=number_format((float)$row['qty'],abs((float)$row['qty']-round((float)$row['qty']))<.0001?0:4,',','.')?></td><td><?=html_escape($row['customer_name']?:'-')?></td><td><?=html_escape(($row['from_custodian_type']?:'-').' → '.($row['to_custodian_type']?:'-'))?></td><td class="status"><?=html_escape(($row['to_usage_status']?:'-').' / '.($row['to_condition_status']?:'-'))?></td><td><?=html_escape($row['document_notes']?:$row['notes']?:'-')?></td></tr><?php endforeach?>
<?php if(!$rows):?><tr><td colspan="11" class="empty">Tidak ada riwayat sesuai filter yang dipilih.</td></tr><?php endif?></tbody></table><div class="footer">Log ini merupakan riwayat operasional immutable; tidak mengubah nilai persediaan atau jurnal accounting.</div>
</body></html>
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="id"><head><meta charset="utf-8"><style>
@page{margin:24px 26px}body{font-family:DejaVu Sans,sans-serif;color:#202733;font-size:10px}h1{font-size:18px;margin:0 0 3px;color:#172845}.muted{color:#68748a}.header{border-bottom:2px solid #f47c00;padding-bottom:10px;margin-bottom:12px}.meta{width:100%;margin:8px 0 14px}.meta td{padding:3px 8px 3px 0}table.data{width:100%;border-collapse:collapse}.data th{background:#172845;color:#fff;text-align:left;padding:7px 6px;font-size:9px}.data td{border:1px solid #dfe5ee;padding:6px;vertical-align:top}.data tr:nth-child(even){background:#f7f9fc}.right{text-align:right}.badge{display:inline-block;padding:2px 6px;border-radius:8px;background:#eaf1ff;color:#2456a6}.footer{margin-top:16px;border-top:1px solid #dfe5ee;padding-top:7px;color:#68748a}.sign{margin-top:24px;width:100%}.sign td{text-align:center;width:33%;padding-top:28px}
</style></head><body>
<div class="header"><div class="muted"><?=html_escape($company->name??$company->company_name??'Perusahaan')?></div><h1>Daftar Peralatan per Teknisi</h1><div class="muted">Status: <?=html_escape($status_label??'Sedang Dibawa')?> · Snapshot posisi operasional saat laporan dibuat</div></div>
<table class="meta"><tr><td><strong>Teknisi</strong><br><?=html_escape($technician->employee_code.' - '.$technician->full_name)?></td><td><strong>Departemen / Posisi</strong><br><?=html_escape(($technician->department_name??'-').' / '.($technician->position_name??'-'))?></td><td><strong>Tanggal Cetak</strong><br><?=date('d/m/Y H:i')?></td><td><strong>Dicetak oleh</strong><br><?=html_escape($printed_by?:'-')?></td></tr></table>
<table class="data"><thead><tr><th>No</th><th>Barang</th><th>Barcode / Serial</th><th>Pemilik</th><th>Qty</th><th>Posisi</th><th>Customer</th><th>Status</th><th>Kondisi</th><th>Terakhir Diperbarui</th></tr></thead><tbody>
<?php foreach($rows as$index=>$row):$qty=(float)$row['qty'];?><tr><td><?=$index+1?></td><td><?=html_escape($row['display_name'])?></td><td><?=html_escape($row['barcode_snapshot'])?><br><span class="muted">SN: <?=html_escape($row['serial_number']?:$row['external_serial']?:'-')?></span></td><td><?=html_escape($row['ownership_type']==='company'?'Perusahaan':($row['ownership_type']==='customer'?'Customer':'Non-Persediaan'))?></td><td class="right"><?=number_format($qty,abs($qty-round($qty))<.0001?0:4,',','.')?></td><td><?=html_escape($row['custodian_type'])?></td><td><?=html_escape($row['customer_name']?:'-')?></td><td><span class="badge"><?=html_escape($row['usage_status'])?></span></td><td><?=html_escape($row['condition_status'])?></td><td><?=html_escape($row['updated_at']?:'-')?></td></tr><?php endforeach?>
<?php if(!$rows):?><tr><td colspan="10" style="text-align:center;padding:20px">Tidak ada peralatan dengan status <?=html_escape($status_label??'yang dipilih')?>.</td></tr><?php endif?></tbody></table>
<table class="sign"><tr><td>Diserahkan / Gudang<br><br><br>(______________________)</td><td>Teknisi<br><br><br>(<?=html_escape($technician->full_name)?>)</td><td>Mengetahui<br><br><br>(______________________)</td></tr></table>
<div class="footer">Dokumen ini tidak mengubah persediaan maupun jurnal accounting. Sumber data: saldo penguasaan Peralatan Teknisi.</div>
</body></html>
@@ -0,0 +1,59 @@
<?php
$inventory_title='Peralatan Teknisi';$inventory_subtitle='Perpindahan, penguasaan, pemasangan, penggantian, dan pemeriksaan tanpa jurnal accounting.';
$panel=$active_panel??'overview';$perm=$equipment_permissions??array();$dash=$equipment_dashboard??array('summary'=>array(),'technicians'=>array(),'inspection_count'=>0);
function tech_qty($value){$n=(float)$value;return abs($n-round($n))<.0001?number_format($n,0,',','.'):number_format($n,4,',','.');}
?>
<?php $this->load->view('inventory_professional/_module_nav',get_defined_vars()); ?>
<link rel="stylesheet" href="<?=base_url('assets/css/technician-equipment.css')?>?v=<?=filemtime(FCPATH.'assets/css/technician-equipment.css')?>">
<div class="tech-shell" id="techWorkbench" data-base="<?=base_url()?>" data-panel="<?=html_escape($panel)?>" data-permissions='<?=html_escape(json_encode($perm))?>'>
<?php $this->load->view('peralatan_teknisi/_module_nav',get_defined_vars()); ?>
<?php if($panel==='overview'):?>
<div class="tech-summary">
<?php foreach(array('carried'=>array('Dibawa Teknisi','bi-person-bag'),'installed'=>array('Terpasang','bi-house-check'),'inspection'=>array('Menunggu Periksa','bi-clipboard-pulse'),'repair'=>array('Dalam Perbaikan','bi-wrench-adjustable'),'damaged'=>array('Rusak','bi-exclamation-octagon'),'lost'=>array('Hilang','bi-question-diamond')) as$key=>$meta):?>
<div class="tech-stat"><small><i class="bi <?=$meta[1]?>"></i> <?=$meta[0]?></small><strong data-summary="<?=$key?>"><?=tech_qty($dash['summary'][$key]??0)?></strong></div>
<?php endforeach?>
</div>
<section class="tech-card" id="scanner">
<div class="tech-card-head"><div><span class="tech-step-label">Scan terlebih dahulu</span><h5>Temukan Posisi dan Tindakan Barang</h5><p>Scan barcode/serial perusahaan maupun barang customer. Sistem hanya menampilkan tindakan yang valid.</p></div></div>
<form class="tech-scan" id="techScanForm" data-ajax="true"><input class="form-control" id="techScanCode" autocomplete="off" placeholder="Scan barcode/SN atau cari nama barang" autofocus><button class="btn btn-primary px-4"><i class="bi bi-search"></i> Cari / Scan</button></form>
<div class="tech-scan-result tech-empty" id="techScanResult"><i class="bi bi-upc-scan fs-2 d-block mb-2"></i>Belum ada barcode yang diperiksa.</div>
</section>
<section class="tech-card" id="technicians">
<div class="tech-card-head"><div><h5>Penguasaan per Teknisi</h5><p>Buka halaman detail untuk melihat barang dibawa, terpasang, hasil pelepasan, dan riwayat.</p></div></div>
<div class="tech-table-wrap"><table class="table tech-table align-middle" id="techTechniciansTable"><thead><tr><th>Teknisi</th><th>Departemen</th><th>Jenis Barang</th><th>Dibawa</th><th>Terpasang</th><th>Aksi</th></tr></thead><tbody>
<?php foreach($dash['technicians']??array() as$t):?><tr><td><strong><?=html_escape($t->full_name)?></strong><br><small><?=html_escape($t->employee_code)?></small></td><td><?=html_escape($t->department_name?:'-')?></td><td><?=tech_qty($t->subject_count)?></td><td><?=tech_qty($t->carried_qty)?></td><td><?=tech_qty($t->installed_qty)?></td><td><a class="btn btn-outline-primary btn-sm" href="<?=base_url('teknisi/detail/'.(int)$t->id)?>"><i class="bi bi-eye"></i> Detail</a></td></tr><?php endforeach?>
<?php if(empty($dash['technicians'])):?><tr><td colspan="6" class="tech-empty">Belum ada penguasaan barang teknisi.</td></tr><?php endif?></tbody></table></div>
</section>
<section class="tech-card"><div class="tech-card-head"><div><h5>Dokumen Operasional Terakhir</h5><p>Histori immutable; koreksi tidak dilakukan dengan menghapus dokumen.</p></div></div><div class="tech-table-wrap"><table class="table tech-table align-middle" id="techDocumentsTable"><thead><tr><th>Dokumen</th><th>Tanggal</th><th>Proses</th><th>Teknisi / Customer</th><th>Item</th><th>Aksi</th></tr></thead><tbody><tr><td colspan="6" class="tech-empty">Memuat riwayat...</td></tr></tbody></table></div></section>
<?php elseif($panel==='replacement'):?>
<form id="techReplacementForm" data-ajax="true" enctype="multipart/form-data">
<section class="tech-card"><div class="tech-card-head"><div><span class="tech-step-label">Satu dokumen atomik</span><h5>Pemasangan Sekaligus Penggantian</h5><p>Barang baru dipasang dan barang lama dilepas dalam satu posting. Jika salah satu gagal, seluruh proses dibatalkan.</p></div></div>
<div class="tech-form-grid"><label>Customer *<select class="form-select" name="customer_id" required><option value="">Pilih customer</option><?php foreach($customers as$c):?><option value="<?=$c->id?>"><?=html_escape($c->nama)?></option><?php endforeach?></select></label><label>Teknisi *<select class="form-select" name="technician_id" required><option value="">Pilih teknisi</option><?php foreach($teknisi_list as$t):?><option value="<?=$t->id?>"><?=html_escape($t->employee_code.' - '.$t->full_name)?></option><?php endforeach?></select></label><label>Tanggal *<input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></label><label>Work Order / Tiket<input class="form-control" name="work_order_no"></label><label>Lokasi Pemasangan *<input class="form-control" name="installation_location" required></label><label>Alasan Penggantian *<input class="form-control" name="reason" required></label></div>
</section>
<div class="tech-two">
<section class="tech-card"><span class="tech-step-label">Barang dipasang</span><h5 class="mb-3">Pengganti dari Teknisi</h5><label>Scan atau cari barang *<div class="tech-inline-search"><input class="form-control" id="techOutgoingSearch" autocomplete="off" placeholder="Scan barcode/SN atau ketik nama barang"><button type="button" class="btn btn-outline-primary tech-replacement-search" data-kind="carried"><i class="bi bi-search"></i></button></div></label><small class="text-muted d-block mb-2" id="techOutgoingHint">Pilih teknisi terlebih dahulu.</small><label>Barang yang dipilih *<select class="form-select tech-allocation-select" name="outgoing_allocation_id" data-kind="carried" required><option value="">Pilih teknisi terlebih dahulu</option></select></label><div class="tech-selection-info mt-2" id="techOutgoingInfo">Belum ada barang dipilih.</div><label class="mt-3">Jumlah <input class="form-control" name="outgoing_qty" inputmode="decimal" placeholder="Otomatis 1 untuk UNIT"></label><small class="text-muted">Barang harus berada dalam penguasaan teknisi.</small></section>
<section class="tech-card"><span class="tech-step-label">Barang dilepas</span><h5 class="mb-3">Barang Lama di Customer</h5><label>Scan atau cari barang<div class="tech-inline-search"><input class="form-control" id="techIncomingSearch" autocomplete="off" placeholder="Scan barcode/SN atau ketik nama barang"><button type="button" class="btn btn-outline-primary tech-replacement-search" data-kind="installed"><i class="bi bi-search"></i></button></div></label><small class="text-muted d-block mb-2" id="techIncomingHint">Pilih customer terlebih dahulu.</small><label>Barang terpasang yang dipilih<select class="form-select tech-allocation-select" name="incoming_allocation_id" data-kind="installed"><option value="">Pilih customer terlebih dahulu</option></select></label><div class="tech-selection-info mt-2" id="techIncomingInfo">Bisa milik perusahaan maupun milik customer.</div><label class="mt-3">Jumlah <input class="form-control" name="incoming_qty" inputmode="decimal" value="1"></label><div class="form-check mt-3"><input class="form-check-input" type="checkbox" id="techNewOldItem"><label class="form-check-label" for="techNewOldItem">Barang lama belum terdaftar</label></div><div id="techOldItemFields" class="tech-form-grid mt-3 d-none"><label class="full">Nama barang *<input class="form-control" name="old_equipment_name"></label><label>Barcode eksternal *<input class="form-control" name="old_barcode"></label><label>Serial number<input class="form-control" name="old_serial_number"></label><label>Kepemilikan<select class="form-select" name="old_ownership_type"><option value="customer">Milik Customer</option><option value="transferred_non_inventory">Milik Perusahaan (Non-Persediaan)</option><option value="unknown">Belum Diketahui</option></select></label><label>Kondisi<select class="form-select" name="old_condition"><option value="inspection_required">Perlu Diperiksa</option><option value="good">Baik</option><option value="repair_required">Perlu Perbaikan</option><option value="damaged">Rusak</option></select></label></div></section>
</div>
<section class="tech-card"><div class="tech-form-grid"><label class="full">Keterangan lengkap *<textarea class="form-control" name="notes" rows="3" required></textarea></label><div class="full"><label class="form-label">Foto sebelum/sesudah atau dokumen</label><div class="tech-file-box" data-filebox><label class="tech-add-file"><i class="bi bi-plus-lg fs-4"></i><span class="small">Tambah</span><input class="d-none" type="file" name="attachments[]" accept="image/*,application/pdf" multiple></label></div></div></div><?php if(!empty($perm['install'])):?><div class="text-end mt-3"><button class="btn btn-primary"><i class="bi bi-check2-circle"></i> Posting Penggantian</button></div><?php endif?></section>
</form>
<?php elseif($panel==='inspection'):?>
<section class="tech-card">
<div class="tech-card-head"><div><span class="tech-step-label">Pemeriksaan terarah</span><h5>Antrean Pemeriksaan Barang Kembali</h5><p>Barang yang masih dibawa teknisi ditampilkan sebagai pengingat. Barang yang telah kembali dapat diperiksa dari gudang perusahaan maupun penyimpanan customer.</p></div></div>
<form id="techInspectionFilter" data-ajax="true" class="tech-form-grid border rounded p-3 mb-3"><label>Dari Tanggal<input class="form-control" type="date" name="date_from"></label><label>Sampai Tanggal<input class="form-control" type="date" name="date_to"></label><label>Teknisi<select class="form-select" name="technician_id"><option value="">Semua teknisi</option><?php foreach($teknisi_list as$t):?><option value="<?=$t->id?>"><?=html_escape($t->full_name)?></option><?php endforeach?></select></label><label>Customer<select class="form-select" name="customer_id"><option value="">Semua customer</option><?php foreach($customers as$c):?><option value="<?=$c->id?>"><?=html_escape($c->nama)?></option><?php endforeach?></select></label><label>Kepemilikan<select class="form-select" name="ownership"><option value="">Semua</option><option value="company">Perusahaan</option><option value="customer">Customer</option><option value="transferred_non_inventory">Non-Persediaan Diserahkan</option><option value="unknown">Belum Diketahui</option></select></label><label>Kondisi<select class="form-select" name="condition"><option value="">Semua</option><option value="inspection_required">Perlu Diperiksa</option><option value="repair_required">Perlu Perbaikan</option><option value="under_repair">Dalam Perbaikan</option><option value="damaged">Rusak</option></select></label><label>Status<select class="form-select" name="status"><option value="">Semua</option><option value="returned">Dikembalikan</option><option value="removed">Dilepas</option></select></label><div class="d-flex align-items-end"><button class="btn btn-primary w-100"><i class="bi bi-funnel"></i> Terapkan Filter</button></div></form>
<div class="tech-table-wrap"><table class="table tech-table align-middle" id="techInspectionTable"><thead><tr><th>Barang</th><th>Barcode / Serial</th><th>Pemilik</th><th>Posisi Sekarang</th><th>Teknisi / Customer Asal</th><th>Qty</th><th>Langkah Berikutnya</th></tr></thead><tbody><tr><td colspan="7" class="tech-empty">Memuat antrean...</td></tr></tbody></table></div>
</section>
<?php elseif($panel==='customer'):?>
<section class="tech-card mt-0"><div class="tech-card-head"><div><span class="tech-step-label">Cek terlebih dahulu</span><h5>Scan Barcode / Serial Barang Customer</h5><p>Jika barang pernah dicatat, gunakan data lama dan lanjutkan pergerakan tanpa mengisi identitas lagi.</p></div></div><form class="tech-scan" id="techCustomerLookupForm" data-ajax="true"><input class="form-control" id="techCustomerLookup" autocomplete="off" placeholder="Scan barcode/SN atau cari nama barang customer" autofocus><button class="btn btn-primary"><i class="bi bi-search"></i> Cari</button></form><div id="techCustomerLookupResult" class="mt-3 d-none"></div></section>
<section class="tech-card"><div class="tech-card-head"><div><span class="tech-step-label">Ringkasan per customer</span><h5>Daftar Customer yang Memiliki Barang</h5><p>Klik detail untuk melihat barang perusahaan maupun barang customer, posisi terakhir, dan statusnya.</p></div></div><div class="tech-table-wrap"><table class="table tech-table align-middle" id="techCustomerTable"><thead><tr><th>Customer</th><th>Jenis Barang</th><th>Total Qty</th><th>Milik Perusahaan</th><th>Milik Customer</th><th>Terpasang</th><th>Kondisi Baik</th><th>Aksi</th></tr></thead><tbody><tr><td colspan="8" class="tech-empty">Memuat ringkasan...</td></tr></tbody></table></div></section>
<form class="tech-card" id="techCustomerItemForm" data-ajax="true" enctype="multipart/form-data"><div class="tech-card-head"><div><span class="tech-step-label">Non-accounting</span><h5>Catat Barang Milik Customer</h5><p>Gunakan form ini hanya jika barcode/SN belum pernah tercatat. Data tidak masuk persediaan, costing, atau jurnal.</p></div></div><div class="tech-form-grid"><label>Customer Pemilik *<select class="form-select" name="owner_customer_id" required><option value="">Pilih customer</option><?php foreach($customers as$c):?><option value="<?=$c->id?>"><?=html_escape($c->nama)?></option><?php endforeach?></select></label><label>Nama Barang *<input class="form-control" name="equipment_name" required></label><label>Kategori<input class="form-control" name="category"></label><label>Merek<input class="form-control" name="brand"></label><label>Model<input class="form-control" name="model"></label><label>Serial Number<input class="form-control" name="serial_number"></label><label>Barcode Eksternal *<input class="form-control" name="external_barcode" required></label><label>Kepemilikan<select class="form-select" name="ownership_type"><option value="customer">Milik Customer</option><option value="transferred_non_inventory">Milik Perusahaan (Non-Persediaan)</option><option value="unknown">Belum Diketahui</option></select></label><label>Jenis Tracking<select class="form-select" name="tracking_type"><option value="UNIT">UNIT — satu barcode satu unit</option><option value="QTY">QTY — satu barcode beberapa unit</option></select></label><label>Jumlah<input class="form-control" name="qty" inputmode="decimal" value="1"></label><label>Tanggal Diterima / Dicatat *<input class="form-control" type="date" name="source_date" value="<?=date('Y-m-d')?>" required></label><label>Dokumen Sumber<input class="form-control" name="source_document_no" placeholder="BAST / tiket / work order"></label><label class="full">Alasan / Asal Barang<input class="form-control" name="source_reason"></label><label class="full tech-check"><input type="checkbox" name="deployment_allowed" value="1"><span>Diizinkan dipasang kembali pada customer lain</span></label><label class="full">Referensi otorisasi<input class="form-control" name="authorization_reference"></label><label class="full">Deskripsi<input class="form-control" name="description"></label><div class="full"><label class="form-label">Foto / Bukti Penyerahan</label><div class="tech-file-box" data-filebox><label class="tech-add-file"><i class="bi bi-plus-lg fs-4"></i><span class="small">Tambah</span><input class="d-none" type="file" name="attachments[]" accept="image/*,application/pdf" multiple></label></div></div></div><?php if(!empty($perm['register'])):?><button class="btn btn-primary mt-3">Simpan Register</button><?php endif?></form>
<?php endif?>
</div>
<div class="modal fade tech-modal" id="techActionModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"><form class="modal-content" id="techActionForm" data-ajax="true" enctype="multipart/form-data"><div class="modal-header"><div><small class="text-muted">PERGERAKAN OPERASIONAL</small><h5 class="modal-title">Catat Pergerakan</h5></div><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><input type="hidden" name="action"><input type="hidden" name="subject_id"><input type="hidden" name="allocation_id"><input type="hidden" name="idempotency_key"><div id="techActionSubject" class="tech-warning mb-3"></div><div class="tech-form-grid"><label>Tanggal *<input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></label><label data-field="qty">Jumlah *<input class="form-control" name="qty" inputmode="decimal"></label><label data-field="technician">Teknisi *<select class="form-select" name="technician_id"><option value="">Pilih teknisi</option><?php foreach($teknisi_list as$t):?><option value="<?=$t->id?>"><?=html_escape($t->employee_code.' - '.$t->full_name)?></option><?php endforeach?></select></label><label data-field="target_technician">Teknisi Tujuan *<select class="form-select" name="target_technician_id"><option value="">Pilih teknisi</option><?php foreach($teknisi_list as$t):?><option value="<?=$t->id?>"><?=html_escape($t->employee_code.' - '.$t->full_name)?></option><?php endforeach?></select></label><label data-field="customer">Customer *<select class="form-select" name="customer_id"><option value="">Pilih customer</option><?php foreach($customers as$c):?><option value="<?=$c->id?>"><?=html_escape($c->nama)?></option><?php endforeach?></select></label><label data-field="warehouse">Gudang Penerima *<select class="form-select" name="warehouse_id"><option value="">Pilih gudang</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></label><label data-field="condition">Kondisi Saat Dilepas<select class="form-select" name="condition_status"><option value="inspection_required">Perlu Diperiksa</option><option value="good">Baik</option><option value="repair_required">Perlu Perbaikan</option><option value="damaged">Rusak</option></select></label><label data-field="inspection">Hasil Pemeriksaan *<select class="form-select" name="result_status"><option value="">Pilih hasil</option><option value="good">Layak Digunakan</option><option value="repair_required">Perlu Perbaikan</option><option value="under_repair">Dalam Perbaikan</option><option value="damaged">Rusak</option><option value="lost">Hilang</option><option value="return_customer">Dikembalikan ke Customer</option><option value="scrapped">Dihapuskan Operasional</option></select></label><label data-field="location" class="full">Lokasi Pemasangan *<input class="form-control" name="installation_location"></label><label data-field="reason" class="full">Alasan<input class="form-control" name="reason"></label><label class="full">Work Order / Tiket<input class="form-control" name="work_order_no"></label><label class="full">Keterangan *<textarea class="form-control" name="notes" rows="3" required></textarea></label><div class="full"><label class="form-label">Foto / Dokumen (maks. 10 file, 5 MB/file)</label><div class="tech-file-box" data-filebox><label class="tech-add-file"><i class="bi bi-plus-lg fs-4"></i><span class="small">Tambah</span><input class="d-none" type="file" name="attachments[]" accept="image/*,application/pdf" multiple></label></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" type="submit"><i class="bi bi-check2-circle"></i> Posting</button></div></form></div></div>
<div class="modal fade tech-modal" id="techDocumentModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Detail Dokumen Peralatan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="techDocumentBody"></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<div class="modal fade tech-modal" id="techPreviewModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Preview Dokumen</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-2" style="height:min(82vh,850px)" id="techPreviewBody"></div></div></div></div>
<div class="modal fade tech-modal" id="techCustomerGroupModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">DETAIL BARANG CUSTOMER</small><h5 class="modal-title" id="techCustomerGroupTitle">Barang Customer</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-2 align-items-end mb-3"><div class="col-md-8"><label class="form-label">Filter Status</label><select class="form-select" id="techCustomerStatus"><option value="all">Semua Status</option><option value="installed">Terpasang</option><option value="stored">Tersimpan di Customer</option><option value="technician">Dibawa Teknisi</option><option value="inspection">Perlu Pemeriksaan/Perbaikan</option><option value="good">Kondisi Baik</option><option value="damaged">Rusak/Hilang</option></select></div><div class="col-md-4 d-flex gap-2"><button type="button" class="btn btn-primary flex-grow-1" id="techCustomerApplyStatus"><i class="bi bi-funnel"></i> Tampilkan</button><?php if(!empty($perm['export'])):?><button type="button" class="btn btn-outline-danger" id="techCustomerPdf"><i class="bi bi-file-pdf"></i> PDF</button><?php endif?></div></div><div class="tech-table-wrap"><table class="table tech-table align-middle"><thead><tr><th>Barang</th><th>Barcode / Serial</th><th>Pemilik</th><th>Jumlah</th><th>Posisi</th><th>Status</th><th>Kondisi</th></tr></thead><tbody id="techCustomerGroupBody"><tr><td colspan="7" class="tech-empty">Memuat detail...</td></tr></tbody></table></div></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<div class="modal fade tech-modal" id="techCustomerNotesModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">HISTORI KETERANGAN BARANG</small><h5 class="modal-title" id="techCustomerNotesTitle">Log Keterangan Customer</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex justify-content-between align-items-center gap-2 mb-3"><p class="text-muted mb-0">Menampilkan seluruh keterangan per barang dari dokumen operasional.</p><?php if(!empty($perm['export'])):?><a class="btn btn-outline-danger" id="techCustomerNotesPdf" href="#" target="_blank"><i class="bi bi-download"></i> Download PDF</a><?php endif?></div><div class="tech-table-wrap"><table class="table tech-table align-middle" id="techCustomerNotesTable"><thead><tr><th>Tanggal / Dokumen</th><th>Barang</th><th>Barcode / Serial</th><th>Proses</th><th>Qty</th><th>Karyawan</th><th>Keterangan</th></tr></thead><tbody><tr><td colspan="7" class="tech-empty">Memuat log keterangan...</td></tr></tbody></table></div></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<script>document.querySelectorAll('select[name="technician_id"],select[name="target_technician_id"],select[name="user_id"]').forEach(function(select){select.classList.add('select-search')});window.TECH_EQUIPMENT=<?=json_encode(array('base'=>base_url(),'permissions'=>$perm,'panel'=>$panel,'targetTechnicians'=>array_map(function($row){return array('id'=>(int)$row->id,'label'=>$row->employee_code.' - '.$row->full_name);},$target_teknisi_list??$teknisi_list)),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/technician-equipment.js')?>?v=<?=filemtime(FCPATH.'assets/js/technician-equipment.js')?>"></script>
@@ -0,0 +1,41 @@
<?php
$reportNames=array('trial_balance'=>'Neraca Saldo','general_ledger'=>'Buku Besar','profit_loss'=>'Laporan Laba Rugi','balance_sheet'=>'Laporan Neraca','cash_flow'=>'Laporan Arus Kas','changes_equity'=>'Perubahan Ekuitas','journal_register'=>'Register Jurnal','account_detail'=>'Detail Transaksi Akun','actual_budget'=>'Aktual vs Budget','sales_summary'=>'Rekap Penjualan','sales_by_customer'=>'Penjualan per Customer','sales_by_item'=>'Penjualan per Barang','sales_by_barcode'=>'Penjualan per Barcode','running_invoices'=>'Invoice Berjalan Belum Final','sales_returns'=>'Retur Penjualan','revenue_recognition'=>'Pendapatan Diakui vs Ditangguhkan','unbilled_deliveries'=>'Surat Jalan Belum Ditagihkan','receivable_reconciliation'=>'Rekonsiliasi Piutang','purchase_returns'=>'Laporan Retur Pembelian','supplier_refunds'=>'Laporan Refund Supplier','outstanding_supplier_refunds'=>'Outstanding Refund Supplier','supplier_debit_notes'=>'Laporan Debit Note Supplier');
$money=function($value){return number_format((float)$value,2,',','.');};
$accountCode=function($value){return preg_replace('/(?:[\.,]0+)$/','',(string)$value);};
?>
<div class="container-fluid py-3 professional-report-page">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
<div><h4><?=html_escape(isset($reportNames[$report])?$reportNames[$report]:'Laporan Profesional')?></h4><small><?=html_escape($company->name)?> · <?=$report==='balance_sheet'?'posisi per '.$filters['to']:'periode '.$filters['from'].' s.d. '.$filters['to']?></small></div>
<div><a class="btn btn-success" href="<?=base_url('professionalreports/export/'.$report.'?'.http_build_query($filters))?>">Export Excel/CSV</a> <button class="btn btn-outline-dark" onclick="print()">PDF/Print</button></div>
</div>
<form class="card card-body my-3"><div class="row g-2">
<div class="col-md-3"><label class="form-label">Tipe Laporan</label><select class="form-select" name="report"><?php foreach($reportNames as$k=>$v):?><option value="<?=$k?>" <?=$report===$k?'selected':''?>><?=$v?></option><?php endforeach?></select></div>
<div class="col-md-2"><label class="form-label">Dari tanggal</label><input class="form-control" type="date" name="from" value="<?=$filters['from']?>"></div>
<div class="col-md-2"><label class="form-label">Sampai tanggal</label><input class="form-control" type="date" name="to" value="<?=$filters['to']?>"></div>
<div class="col-md-3"><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div>
<div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Tampilkan</button></div>
</div></form>
<?php if($report==='balance_sheet'):
$assets=array();$liabilities=array();$equities=array();$totalAssets=0;$totalLiabilities=0;$totalEquity=0;
foreach($rows as$r){if($r['tipe']==='asset'){$assets[]=$r;$totalAssets+=(float)$r['amount'];}elseif($r['tipe']==='liability'){$liabilities[]=$r;$totalLiabilities+=(float)$r['amount'];}elseif($r['tipe']==='equity'){$equities[]=$r;$totalEquity+=(float)$r['amount'];}}
$revenue=0;$expense=0;foreach($profit_loss_rows as$r){if($r['tipe']==='revenue')$revenue+=(float)$r['amount'];elseif($r['tipe']==='expense')$expense+=(float)$r['amount'];}$netProfit=$revenue-$expense;$totalPasiva=$totalLiabilities+$totalEquity+$netProfit;$difference=$totalAssets-$totalPasiva;$balanced=abs($difference)<0.01;
?>
<div class="alert <?=$balanced?'alert-success':'alert-danger'?> d-flex justify-content-between align-items-center"><b><?=$balanced?'NERACA BALANCE':'NERACA TIDAK BALANCE'?></b><span>Selisih: Rp <?=$money($difference)?></span></div>
<div class="row g-3 report-paired-cards">
<div class="col-lg-6"><div class="card h-100"><div class="card-header bg-primary text-white"><b>AKTIVA</b></div><div class="card-body p-0"><table class="table mb-0"><tbody><?php foreach($assets as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?></tbody><tfoot><tr class="table-primary"><th>TOTAL AKTIVA</th><th class="text-end">Rp <?=$money($totalAssets)?></th></tr></tfoot></table></div></div></div>
<div class="col-lg-6"><div class="card h-100"><div class="card-header bg-dark text-white"><b>PASIVA</b></div><div class="card-body p-0"><table class="table mb-0"><tbody><tr class="table-light"><th colspan="2">Liabilitas</th></tr><?php foreach($liabilities as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?><tr class="table-light"><th colspan="2">Ekuitas</th></tr><?php foreach($equities as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?><tr class="table-warning"><th>Laba Bersih Berjalan</th><th class="text-end">Rp <?=$money($netProfit)?></th></tr></tbody><tfoot><tr class="table-dark"><th>TOTAL PASIVA</th><th class="text-end">Rp <?=$money($totalPasiva)?></th></tr></tfoot></table></div></div></div>
</div>
<?php elseif($report==='profit_loss'):
$revenues=array();$expenses=array();$totalRevenue=0;$totalExpense=0;foreach($rows as$r){if($r['tipe']==='revenue'){$revenues[]=$r;$totalRevenue+=(float)$r['amount'];}elseif($r['tipe']==='expense'){$expenses[]=$r;$totalExpense+=(float)$r['amount'];}}$netProfit=$totalRevenue-$totalExpense;
?>
<div class="row g-3 report-paired-cards">
<div class="col-lg-6"><div class="card h-100"><div class="card-header bg-success text-white"><b>PENDAPATAN</b></div><div class="card-body p-0"><table class="table mb-0"><tbody><?php foreach($revenues as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?></tbody><tfoot><tr class="table-success"><th>TOTAL PENDAPATAN</th><th class="text-end">Rp <?=$money($totalRevenue)?></th></tr></tfoot></table></div></div></div>
<div class="col-lg-6"><div class="card h-100"><div class="card-header bg-danger text-white"><b>BEBAN</b></div><div class="card-body p-0"><table class="table mb-0"><tbody><?php foreach($expenses as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?></tbody><tfoot><tr class="table-danger"><th>TOTAL BEBAN</th><th class="text-end">Rp <?=$money($totalExpense)?></th></tr></tfoot></table></div></div></div>
</div>
<div class="card mt-3"><div class="card-body d-flex justify-content-between align-items-center"><h5 class="mb-0"><?=$netProfit>=0?'LABA BERSIH':'RUGI BERSIH'?></h5><h4 class="mb-0 <?=$netProfit>=0?'text-success':'text-danger'?>">Rp <?=$money(abs($netProfit))?></h4></div></div>
<?php else: ?>
<?php $visibleColumns=$rows?array_values(array_filter(array_keys($rows[0]),function($key){return$key!=='record_id';})):array();$documentTypes=array('return_no'=>'purchase_return','refund_no'=>'supplier_refund','debit_note_no'=>'supplier_debit_note'); ?>
<div class="card"><div class="table-responsive"><table class="table table-sm mb-0"><thead><?php if($rows):?><tr><?php foreach($visibleColumns as$h):?><th><?=html_escape(ucwords(str_replace('_',' ',$h)))?></th><?php endforeach?></tr><?php endif?></thead><tbody><?php foreach($rows as$r):?><tr><?php foreach($visibleColumns as$k):$v=$r[$k];?><td class="<?=is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)?'text-end':''?>"><?php if($k==='journal_id'&&$v):?><a href="<?=base_url('jurnal/detail/'.$v)?>">#<?=$v?></a><?php elseif(isset($documentTypes[$k])&&!empty($r['record_id'])):?><a href="<?=base_url('purchases/returns?action=history&focus_type='.$documentTypes[$k].'&focus='.$r['record_id'])?>"><?=html_escape($v)?></a><?php elseif($k==='kode_akun'):?><?=html_escape($accountCode($v))?><?php elseif(is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)):?><?=$money($v)?><?php else:?><?=html_escape($v===null?'-':$v)?><?php endif?></td><?php endforeach?></tr><?php endforeach?></tbody></table></div></div>
<?php endif; ?>
<div class="mt-3"><b>Subledger:</b> <a href="<?=base_url('receivables')?>">Aging Piutang</a> · <a href="<?=base_url('payables')?>">Aging Hutang</a> · <a href="<?=base_url('inventoryprofessional')?>">Persediaan</a> · <a href="<?=base_url('fixedassets')?>">Aset</a> · <a href="<?=base_url('hrpayroll')?>">Payroll</a> · <a href="<?=base_url('purchases/returns')?>">Retur &amp; Refund</a></div>
</div>
File diff suppressed because one or more lines are too long
+60
View File
@@ -0,0 +1,60 @@
<?php
function purchase_action_form($route,$id,$label,$ci,$step){return'<form class="d-inline" method="post" action="'.base_url('purchases/'.$route.'/'.$id).'"><input type="hidden" name="'.$ci->security->get_csrf_token_name().'" value="'.$ci->security->get_csrf_hash().'"><input type="hidden" name="return_step" value="'.$step.'"><button class="btn btn-sm btn-outline-primary">'.html_escape($label).'</button></form>';}
$step=$this->input->get('step')?:'request';$allowedSteps=array('request','order','payment','receipt','invoice');if(!in_array($step,$allowedSteps,true))$step='request';$departments=$this->db->where('is_active',1)->order_by('department_name')->get('k_departments')->result();$paymentAccounts=$this->db->where(array('is_active'=>1,'allow_posting'=>1))->group_start()->like('nama_akun','kas')->or_like('nama_akun','bank')->group_end()->order_by('kode_akun')->get('accounts')->result();$returnBarcodes=array();$isMasterAdmin=$this->session->userdata('role')==='Admin';$canPrView=$isMasterAdmin||check_permission('purchases','can_view');$canPrCreate=$isMasterAdmin||check_permission('purchases','can_create');$canPrApprove=$isMasterAdmin||check_permission('purchases','can_approve');$canPrExport=$isMasterAdmin||check_permission('purchases','can_export');
?>
<div class="container-fluid py-3 purchase-workflow">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-3"><div><h4 class="mb-1">Purchase Workflow</h4><p class="text-muted mb-0">Kelola proses normal permintaan, pemesanan, pembayaran awal, penerimaan, dan Invoice Supplier.</p></div><a class="btn btn-outline-primary" href="<?=base_url('purchases/returns')?>"><i class="bi bi-arrow-return-left me-1"></i>Retur & Refund</a></div>
<?php foreach(array('success'=>'success','error'=>'error')as$key=>$icon)if($this->session->flashdata($key)):?><div class="d-none" data-app-toast data-icon="<?=$icon?>" data-message="<?=html_escape($this->session->flashdata($key))?>"></div><?php endif?>
<?php $workflowSteps=array('request'=>array('bi-file-earmark-plus','Permintaan','Kebutuhan barang'),'order'=>array('bi-cart-check','Purchase Order','Pesanan disetujui'),'payment'=>array('bi-wallet2','Pembayaran Awal/DP','Jika disyaratkan'),'receipt'=>array('bi-box-arrow-in-down','Penerimaan','Pemeriksaan gudang'),'invoice'=>array('bi-receipt','Invoice Supplier','Final matching'));?>
<nav class="purchase-workflow-stepper mb-4" aria-label="Tahapan Purchase Workflow"><?php $stepNo=0;foreach($workflowSteps as$key=>$meta):$stepNo++;?><a class="purchase-workflow-step <?=$step===$key?'active':''?>" href="<?=base_url('purchases?step='.$key)?>"><span class="workflow-number"><?=$stepNo?></span><i class="bi <?=$meta[0]?>"></i><span><b><?=$meta[1]?></b><small><?=$meta[2]?></small></span></a><?php endforeach?></nav>
<?php if($step==='request'):?>
<?php if($canPrCreate):?>
<div class="card mb-3"><div class="card-header"><b>Buat Purchase Request</b></div><div class="card-body"><form class="row g-2" method="post" action="<?=base_url('purchases/create_request')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="request">
<input type="hidden" name="destination_type" value="warehouse">
<div class="col-md-2"><label class="form-label">Tanggal</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Dibutuhkan</label><input class="form-control" type="date" name="needed_date"></div><div class="col-md-3"><label class="form-label">Departemen</label><select class="form-select select-search" name="department_id" required><option value="">Pilih departemen</option><?php foreach($departments as$d):?><option value="<?=$d->id?>"><?=html_escape($d->code.' - '.$d->department_name)?></option><?php endforeach?></select></div><div class="col-md-5"><label class="form-label">Tujuan/Kebutuhan</label><input class="form-control" name="purpose" required></div>
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" name="applicant_is_creator" value="1" id="prApplicantSelf"><label class="form-check-label" for="prApplicantSelf">Saya yang membuat dan saya yang mengajukan PR ini</label></div></div>
<div class="col-md-6" id="prApplicantNameGroup"><label class="form-label">Nama yang Mengajukan</label><input class="form-control" name="applicant_name" id="prApplicantName" maxlength="150" placeholder="Tulis nama pihak yang mengajukan" required><small class="text-muted">Diisi manual dan akan ditampilkan pada detail serta tanda tangan PDF.</small></div>
<?php if(false): // Form satu barang lama dipertahankan sementara untuk kompatibilitas riwayat. ?>
<div class="col-md-4"><label class="form-label">Master Barang</label><select class="form-select select-search" name="kode_barang_id" id="requestCatalog" required><option value="">Pilih kode - nama</option><?php foreach($catalogs as$i):?><option value="<?=$i->id?>" data-tracking="<?=strtoupper($i->tracking_type?:'UNIT')?>" data-serial="<?=strtolower($i->need_serial_number)==='true'?'1':'0'?>"><?=html_escape($i->kode_barang.' - '.$i->nama)?> · <?=strtoupper($i->tracking_type?:'UNIT')?><?=$i->need_serial_number==='true'?' · Serial':''?></option><?php endforeach?></select><small class="text-muted">UNIT: satu barang satu barcode. QTY: satu barcode dapat memuat beberapa barang.</small></div><div class="col-md-4"><label class="form-label">Deskripsi / Merek / Spesifikasi</label><input class="form-control" name="description" placeholder="Contoh: Epson L3210 warna hitam" required></div><div class="col-md-2"><label class="form-label">Qty Barang</label><input class="form-control" type="number" name="qty" min=".01" step=".01" required></div><div class="col-md-2"><label class="form-label">Estimasi Harga</label><input class="form-control format-rupiah" name="estimated_price" required></div><div class="col-md-3 d-none" id="qtyPerBarcodeGroup"><label class="form-label">Isi per Barcode</label><input class="form-control" type="number" name="qty_per_barcode" min=".01" step=".01"><small class="text-muted">Wajib untuk tracking QTY.</small></div><div class="col-12 text-end"><button class="btn btn-primary">Simpan Draft PR</button></div>
<?php endif?>
<div class="col-12"><div class="d-flex justify-content-between align-items-center mb-2"><div><b>Daftar Barang</b><small class="d-block text-muted">Satu PR dapat berisi beberapa barang.</small></div><button class="btn btn-sm btn-outline-primary" type="button" id="addPrLine"><i class="bi bi-plus-lg"></i> Tambah Barang</button></div><div id="prLines"></div></div>
<template id="prLineTemplate"><div class="pr-line border rounded p-3 mb-2"><div class="d-flex justify-content-between align-items-center mb-2"><b class="pr-line-title">Barang</b><button type="button" class="btn btn-sm btn-outline-danger remove-pr-line" aria-label="Hapus barang"><i class="bi bi-trash"></i></button></div><div class="row g-2"><div class="col-lg-4"><label class="form-label">Master Barang</label><select class="form-select pr-catalog" name="kode_barang_id[]" required><option value="">Pilih kode - nama</option><?php foreach($catalogs as$i):?><option value="<?=$i->id?>" data-tracking="<?=strtoupper($i->tracking_type?:'UNIT')?>" data-serial="<?=strtolower($i->need_serial_number)==='true'?'1':'0'?>"><?=html_escape($i->kode_barang.' - '.$i->nama)?> · <?=strtoupper($i->tracking_type?:'UNIT')?><?=$i->need_serial_number==='true'?' · Serial':''?></option><?php endforeach?></select><small class="text-muted">UNIT: satu barang satu barcode. QTY: satu barcode dapat berisi beberapa barang.</small></div><div class="col-lg-4"><label class="form-label">Deskripsi / Merek / Spesifikasi</label><input class="form-control" name="description[]" placeholder="Contoh: Epson L3210 warna hitam" maxlength="255" required></div><div class="col-lg-2 col-6"><label class="form-label">Qty Barang</label><input class="form-control pr-qty" type="number" name="qty[]" min=".01" step=".01" required></div><div class="col-lg-2 col-6"><label class="form-label">Estimasi Harga/Satuan</label><input class="form-control format-rupiah" name="estimated_price[]" inputmode="decimal" required></div><div class="col-lg-3 d-none qty-per-barcode-group"><label class="form-label">Isi per Barcode</label><input class="form-control" type="number" name="qty_per_barcode[]" min=".01" step=".01"><small class="text-muted">Wajib untuk tracking QTY.</small></div></div></div></template>
<div class="col-12 text-end"><button class="btn btn-primary">Simpan Draft PR</button></div>
</form></div></div>
<?php endif?>
<div class="card"><div class="card-header">Daftar Purchase Request</div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Nomor/Tanggal</th><th>Departemen</th><th>Kebutuhan</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($requests as$r):$isOwner=(int)$r->requested_by===(int)$this->session->userdata('user_id');if($r->status==='draft'&&!$isOwner)continue;?><tr><td><?=$r->request_no?><small class="d-block text-muted"><?=$r->request_date?></small></td><td><?=html_escape($r->department?:'-')?></td><td><?=html_escape($r->purpose)?></td><td><?=$r->status?></td><td><?php if($canPrView):?><button type="button" class="btn btn-sm btn-outline-info btn-pr-detail" data-id="<?=$r->id?>">Detail</button><?php endif?> <?php if($canPrExport):?><a class="btn btn-sm btn-outline-danger" target="_blank" href="<?=base_url('purchases/request_pdf/'.$r->id)?>"><i class="bi bi-file-earmark-pdf"></i> PDF</a><?php endif?> <?php if($r->status==='draft'&&$isOwner)echo purchase_action_form('submit_request',$r->id,'Ajukan',$this,'request');elseif($canPrApprove&&$r->status==='submitted')echo purchase_action_form('approve_request',$r->id,'Approve',$this,'request');?> <?php if($r->status==='draft'&&$isOwner):?><form class="d-inline js-confirm" method="post" action="<?=base_url('purchases/delete_request/'.$r->id)?>" data-confirm-title="Hapus draft PR?" data-confirm-text="Draft <?=html_escape($r->request_no)?> akan dihapus permanen." data-confirm-button="Ya, hapus"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="request"><button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i> Hapus</button></form><?php endif?></td></tr><?php endforeach?></tbody></table></div></div>
<?php elseif($step==='order'):?>
<div class="card mb-3"><div class="card-header"><b>Buat Purchase Order dari PR Disetujui</b></div><div class="card-body"><form class="row g-2" method="post" action="<?=base_url('purchases/create_order')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="order"><input type="hidden" name="destination_type" id="poDestination" value="warehouse">
<div class="col-md-3"><label class="form-label">Purchase Request</label><select class="form-select" name="request_id" id="poRequest" required><option value="">Pilih PR</option><?php foreach($requests as$r)if($r->status==='approved'):?><option value="<?=$r->id?>" data-destination="<?=$r->destination_type?>"><?=$r->request_no?> · <?=$r->destination_type==='asset'?'Aset':'Gudang'?></option><?php endif?></select></div><div class="col-md-3"><label class="form-label">Supplier</label><select class="form-select select-search" name="supplier_id" required><?php foreach($suppliers as$s):?><option value="<?=$s->id?>"><?=html_escape($s->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Tanggal PO</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Estimasi Datang</label><input class="form-control" type="date" name="expected_date"></div><div class="col-md-2"><label class="form-label">Pajak %</label><input class="form-control" type="number" name="tax_rate" step=".01" value="0"></div>
<div class="col-md-4"><label class="form-label">Deskripsi / Merek / Spesifikasi</label><input class="form-control" name="description" required></div><div class="col-md-2"><label class="form-label">Qty</label><input class="form-control" type="number" name="qty" step=".01" required></div><div class="col-md-2"><label class="form-label">Harga Satuan</label><input class="form-control format-rupiah" name="unit_price" required></div><div class="col-md-4"><label class="form-label">Rencana Pembayaran</label><select class="form-select" name="payment_plan" required><option value="cash">Bayar Langsung</option><option value="installment">Cicilan</option><option value="credit" selected>Kredit / Jatuh Tempo</option></select><small class="text-muted">Pembayaran dicatat setelah tagihan supplier diposting.</small></div>
<div class="col-12 po-warehouse-fields"><div class="row"><div class="col-md-4"><label class="form-label">Gudang Tujuan</label><select class="form-select" name="warehouse_id"><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div></div></div>
<div class="col-12 po-asset-fields d-none"><div class="row g-2"><div class="col-md-4"><label class="form-label">Akun Aset</label><select class="form-select" name="asset_account_id"><?php foreach($asset_accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Lokasi Aset</label><select class="form-select" name="asset_location_id"><?php foreach($asset_locations as$l):?><option value="<?=$l->id?>"><?=html_escape($l->nama)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Masa Manfaat (bulan)</label><input class="form-control" type="number" name="useful_life_months" value="48"></div><div class="col-md-2"><label class="form-label">Nilai Residu</label><input class="form-control format-rupiah" name="residual_value" value="0"></div></div></div>
<div class="col-12 text-end"><button class="btn btn-primary">Buat Draft PO</button></div></form></div></div>
<div class="card"><div class="card-header">Daftar Purchase Order</div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>PO</th><th>Supplier</th><th>Tujuan</th><th>Total</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($orders as$o):$line=$this->db->where('purchase_order_id',$o->id)->get('purchase_order_lines')->row();?><tr><td><?=$o->po_no?></td><td><?=html_escape($o->name)?></td><td><?=$line&&$line->destination_type==='asset'?'Aset Tetap':'Gudang'?></td><td><?=number_format($o->total,2,',','.')?></td><td><?=$o->status?></td><td><?php if($o->status==='draft')echo purchase_action_form('submit_order',$o->id,'Ajukan',$this,'order');elseif($o->status==='submitted'&&$this->session->userdata('role')==='Admin')echo purchase_action_form('approve_order',$o->id,'Approve',$this,'order');?></td></tr><?php endforeach?></tbody></table></div></div>
<?php elseif($step==='receipt'):?>
<div class="card"><div class="card-header"><b>Penerimaan Barang/Aset</b><small class="d-block text-muted">Posting penerimaan adalah satu-satunya proses yang mengaktifkan stok atau register aset.</small></div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>PO/Supplier</th><th>Tujuan</th><th>Item dan Sisa</th><th>Penerimaan</th></tr></thead><tbody><?php foreach($orders as$o):if(!in_array($o->status,array('approved','partially_received')))continue;$lines=$this->db->where('purchase_order_id',$o->id)->get('purchase_order_lines')->result();?><tr><td><?=$o->po_no?><small class="d-block"><?=html_escape($o->name)?></small></td><td><?=$lines&&$lines[0]->destination_type==='asset'?'<span class="badge bg-dark">Aset Tetap</span>':'<span class="badge bg-primary">Gudang</span>'?></td><td><?php foreach($lines as$l):?><?=html_escape($l->description)?> · sisa <?=number_format($l->qty-$l->received_qty,2,',','.')?><br><?php endforeach?></td><td><form method="post" action="<?=base_url('purchases/receive/'.$o->id)?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="receipt"><div class="d-flex flex-wrap gap-1"><input class="form-control form-control-sm" type="date" name="date" value="<?=date('Y-m-d')?>"><input class="form-control form-control-sm" name="delivery_no" placeholder="Surat jalan"><?php foreach($lines as$l):?><input class="form-control form-control-sm" type="number" name="qty[<?=$l->id?>]" min="0" max="<?=$l->qty-$l->received_qty?>" step=".01" placeholder="Qty <?=html_escape($l->description)?>"><?php endforeach?><button class="btn btn-sm btn-warning">Post Penerimaan</button></div></form></td></tr><?php endforeach?></tbody></table></div></div>
<div class="card mt-3"><div class="card-header">Riwayat Penerimaan</div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Nomor</th><th>PO</th><th>Supplier</th><th>Tanggal</th><th>Surat Jalan</th></tr></thead><tbody><?php foreach($receipts as$r):?><tr><td><?=$r->receipt_no?></td><td><?=$r->po_no?></td><td><?=html_escape($r->name)?></td><td><?=$r->receipt_date?></td><td><?=html_escape($r->supplier_delivery_no?:'-')?></td></tr><?php endforeach?></tbody></table></div></div>
<?php elseif($step==='invoice'):?>
<div class="d-flex justify-content-end mb-2"><a class="btn btn-success" href="<?=base_url('payables')?>">Bayar Tagihan Langsung / Cicilan</a></div>
<?php foreach($bills as$b):if($b->status!=='approved'||!in_array($b->payment_plan,array('cash','installment')))continue;?><div class="card mb-3 border-warning"><div class="card-header"><b><?=$b->payment_plan==='cash'?'Pembayaran Langsung':'Cicilan Pertama'?> — <?=html_escape($b->internal_no)?></b></div><div class="card-body"><form class="row g-2" method="post" action="<?=base_url('purchases/post_bill/'.$b->id)?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="invoice"><div class="col-md-3"><label class="form-label">Kas/Bank</label><select class="form-select" name="bank_account_id" required><?php foreach($paymentAccounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><?php if($b->payment_plan==='installment'):?><div class="col-md-3"><label class="form-label">Cicilan Pertama</label><input class="form-control format-rupiah" name="initial_amount" required></div><?php endif?><div class="col-md-2"><label class="form-label">Tanggal Bayar</label><input class="form-control" type="date" name="payment_date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Referensi</label><input class="form-control" name="reference_no"></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-warning w-100">Post & Bayar</button></div></form></div></div><?php endforeach?>
<div class="card mb-3"><div class="card-header"><b>Tagihan Supplier / Three-way Matching</b></div><div class="card-body"><form class="row g-2" method="post" action="<?=base_url('purchases/create_bill')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="invoice"><div class="col-md-3"><label class="form-label">PO</label><select class="form-select" name="po_id"><?php foreach($orders as$o)if(in_array($o->status,array('partially_received','received'))):?><option value="<?=$o->id?>"><?=$o->po_no?></option><?php endif?></select></div><div class="col-md-3"><label class="form-label">Penerimaan</label><select class="form-select" name="receipt_id"><?php foreach($receipts as$r):?><option value="<?=$r->id?>"><?=$r->receipt_no?> · <?=$r->po_no?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">No. Invoice Supplier</label><input class="form-control" name="supplier_invoice_no" required></div><div class="col-md-2"><label class="form-label">Tanggal</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>"></div><div class="col-md-1 d-flex align-items-end"><button class="btn btn-primary">Match</button></div></form></div></div>
<div class="card"><div class="card-header">Daftar Tagihan</div><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Tagihan</th><th>Supplier</th><th>Rencana Bayar</th><th>Jatuh Tempo</th><th>Saldo</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($bills as$b):?><tr><td><?=$b->internal_no?><small class="d-block"><?=html_escape($b->supplier_invoice_no)?></small></td><td><?=html_escape($b->name)?></td><td><?=array('cash'=>'Langsung','installment'=>'Cicilan','credit'=>'Kredit')[$b->payment_plan]??'Kredit'?></td><td><?=$b->due_date?></td><td><?=number_format($b->balance,0,',','.')?></td><td><?=html_escape(array('verified'=>'Perlu Finalisasi','partial'=>'Parsial','paid'=>'Lunas')[$b->status]??$b->status)?></td><td><?php if($b->status==='verified'):?><a class="btn btn-sm btn-warning" href="<?=base_url('purchases?step=invoice')?>">Finalisasi</a><?php endif?></td></tr><?php endforeach?></tbody></table></div></div>
<?php else:?>
<div class="row g-3"><div class="col-lg-6"><div class="card h-100"><div class="card-header">Retur Pembelian</div><div class="card-body"><form method="post" action="<?=base_url('purchases/purchase_return')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="adjustment"><select class="form-select mb-2" name="receipt_line_id"><?php foreach($this->db->select('goods_receipt_lines.id,goods_receipt_lines.qty,goods_receipts.receipt_no,items.nama_barang')->from('goods_receipt_lines')->join('goods_receipts','goods_receipts.id=goods_receipt_lines.goods_receipt_id')->join('items','items.id=goods_receipt_lines.item_id')->where('goods_receipt_lines.stock_log_id IS NOT NULL',null,false)->get()->result()as$l):?><option value="<?=$l->id?>"><?=html_escape($l->receipt_no.' · '.$l->nama_barang.' · '.$l->qty)?></option><?php endforeach?></select><input class="form-control mb-2" type="date" name="date" value="<?=date('Y-m-d')?>"><input class="form-control mb-2" type="number" name="qty" min=".01" step=".01" placeholder="Qty retur"><input class="form-control mb-2" name="reason" placeholder="Alasan" required><button class="btn btn-warning">Post Retur</button></form></div></div></div><div class="col-lg-6"><div class="card h-100"><div class="card-header">Debit Note Supplier</div><div class="card-body"><form method="post" action="<?=base_url('purchases/debit_note')?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="adjustment"><select class="form-select mb-2" name="bill_id"><?php foreach($bills as$b)if($b->status==='partial'&&$b->balance>0):?><option value="<?=$b->id?>"><?=html_escape($b->internal_no.' · '.$b->name)?></option><?php endif?></select><input class="form-control mb-2" type="date" name="date" value="<?=date('Y-m-d')?>"><input class="form-control mb-2 format-rupiah" name="amount" placeholder="Nilai"><input class="form-control mb-2" name="reason" placeholder="Alasan" required><button class="btn btn-danger">Post Debit Note</button></form></div></div></div></div>
<?php endif?>
</div>
<style>
.purchase-workflow-stepper{display:flex;gap:.65rem;overflow-x:auto;padding:.2rem .1rem .65rem;scrollbar-width:thin}.purchase-workflow-step{position:relative;display:flex;align-items:center;gap:.6rem;min-width:190px;padding:.85rem .9rem;border:1px solid var(--bs-border-color);border-radius:14px;background:var(--bs-body-bg);color:var(--bs-body-color);text-decoration:none;box-shadow:0 .15rem .55rem rgba(30,42,70,.05)}.purchase-workflow-step:hover{border-color:var(--bs-primary);color:var(--bs-primary)}.purchase-workflow-step.active{background:linear-gradient(135deg,var(--bs-primary),#135ed2);border-color:var(--bs-primary);color:#fff;box-shadow:0 .4rem 1rem rgba(13,110,253,.2)}.purchase-workflow-step .workflow-number{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;background:rgba(13,110,253,.1);font-weight:700;flex:none}.purchase-workflow-step.active .workflow-number{background:rgba(255,255,255,.2)}.purchase-workflow-step>span:last-child{display:flex;flex-direction:column;white-space:nowrap}.purchase-workflow-step small{font-size:.72rem;opacity:.72}@media(max-width:575.98px){.purchase-workflow-step{min-width:150px;padding:.7rem}.purchase-workflow-step small,.purchase-workflow-step>i{display:none}}
</style>
<div class="modal fade" id="prDetailModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Detail Permintaan Pembelian</h5><small class="text-muted" id="prDetailNumber"></small></div><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button></div><div class="modal-body" id="prDetailBody"><div class="text-center py-4">Memuat data...</div></div><div class="modal-footer"><div class="me-auto small"><div><span class="text-muted">Dibuat oleh:</span> <b id="prCreatorName">-</b></div><div><span class="text-muted">Diajukan oleh:</span> <b id="prRequesterName">-</b></div><div><span class="text-muted">Disetujui oleh:</span> <b id="prApproverName">-</b></div></div><a class="btn btn-outline-danger" id="prDetailPdf" target="_blank"><i class="bi bi-file-earmark-pdf"></i> Buka PDF</a><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
<script>$(function(){function destination(){var x=$('#poRequest option:selected').data('destination')||'warehouse';$('#poDestination').val(x);$('.po-warehouse-fields').toggleClass('d-none',x!=='warehouse');$('.po-asset-fields').toggleClass('d-none',x!=='asset');}function tracking(){var q=$('#requestCatalog option:selected').data('tracking')==='QTY';$('#qtyPerBarcodeGroup').toggleClass('d-none',!q).find('input').prop('required',q);}var returnBarcodes=<?=json_encode($returnBarcodes)?>,returnForm=$('form[action*="purchases/purchase_return"]'),receiptSelect=returnForm.find('[name="receipt_line_id"]'),barcodeSelect=$('<select class="form-select mb-2" name="barcode_id" required><option value="">Pilih barcode / serial yang diretur</option></select>');receiptSelect.after(barcodeSelect);function returnOptions(){var receipt=String(receiptSelect.val()||'');barcodeSelect.html('<option value="">Pilih barcode / serial yang diretur</option>'+returnBarcodes.filter(x=>String(x.source_receipt_line_id)===receipt).map(x=>'<option value="'+x.id+'" data-max="'+x.qty_sisa+'" data-tracking="'+x.tracking_type+'">'+$('<div>').text(x.barcode+(x.serial_number?' · SN '+x.serial_number:'')+' · saldo '+Number(x.qty_sisa).toLocaleString('id-ID')).html()+'</option>').join(''));}receiptSelect.on('change',returnOptions);barcodeSelect.on('change',function(){var o=$(this).find(':selected'),unit=o.data('tracking')==='UNIT';returnForm.find('[name="qty"]').attr('max',o.data('max')||'').val(unit&&o.val()?1:'').prop('readonly',unit&&!!o.val());});$('input[name="destination_type"]').on('change',destination);$('#poRequest').on('change',destination);$('#requestCatalog').on('change',tracking);destination();tracking();returnOptions();});</script>
<script>$(document).on('click','.btn-pr-detail',async function(){var id=this.dataset.id,body=document.getElementById('prDetailBody'),modalElement=document.getElementById('prDetailModal');body.innerHTML='<div class="text-center py-4">Memuat data...</div>';document.getElementById('prDetailPdf').href='<?=base_url('purchases/request_pdf/')?>'+id;var modal=window.bootstrap?bootstrap.Modal.getOrCreateInstance(modalElement):null;if(modal)modal.show();else $('#prDetailModal').modal('show');try{var response=await fetch('<?=base_url('purchases/request_detail/')?>'+id,{headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();if(!response.ok||!json.status)throw new Error(json.message||'Data tidak dapat dimuat.');var r=json.request,lines=Array.isArray(json.lines)?json.lines:[];document.getElementById('prDetailNumber').textContent=r.request_no;var esc=v=>$('<div>').text(v??'-').html();body.innerHTML='<div class="row g-2 mb-3"><div class="col-md-4"><small class="text-muted">Tanggal</small><div>'+esc(r.request_date)+'</div></div><div class="col-md-4"><small class="text-muted">Dibutuhkan</small><div>'+esc(r.needed_date||'-')+'</div></div><div class="col-md-4"><small class="text-muted">Status</small><div>'+esc(r.status)+'</div></div><div class="col-md-4"><small class="text-muted">Departemen</small><div>'+esc((r.department_code?r.department_code+' - ':'')+(r.department_name||r.department||'-'))+'</div></div><div class="col-md-8"><small class="text-muted">Kebutuhan</small><div>'+esc(r.purpose)+'</div></div></div><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Master Barang</th><th>Deskripsi</th><th class="text-end">Qty</th><th>Tracking</th><th class="text-end">Estimasi</th></tr></thead><tbody>'+lines.map(x=>'<tr><td>'+esc((x.kode_barang||'-')+' - '+(x.master_name||'-'))+'</td><td>'+esc(x.description)+'</td><td class="text-end">'+Number(x.qty).toLocaleString('id-ID')+'</td><td>'+esc(x.tracking_type)+(x.tracking_type==='QTY'?' / '+Number(x.qty_per_barcode).toLocaleString('id-ID')+' per barcode':'')+'</td><td class="text-end">'+(Number(x.qty)*Number(x.estimated_price)).toLocaleString('id-ID')+'</td></tr>').join('')+'</tbody></table></div>';}catch(error){body.innerHTML='<div class="alert alert-danger mb-0">'+$('<div>').text(error.message).html()+'</div>';}});</script>
<script>$(function(){var self=$('#prApplicantSelf'),group=$('#prApplicantNameGroup'),name=$('#prApplicantName');function toggleApplicant(){var checked=self.is(':checked');group.toggleClass('d-none',checked);name.prop('required',!checked).prop('disabled',checked);if(checked)name.val('');}self.on('change',toggleApplicant);toggleApplicant();});</script>
<script>$(function(){var container=$('#prLines'),template=document.getElementById('prLineTemplate');function renumber(){container.find('.pr-line').each(function(i){$(this).find('.pr-line-title').text('Barang '+(i+1));$(this).find('.remove-pr-line').toggle(container.children().length>1);});}function syncTracking(row){var option=row.find('.pr-catalog option:selected'),tracking=option.data('tracking')||'UNIT',group=row.find('.qty-per-barcode-group'),per=group.find('input'),qty=row.find('.pr-qty');group.toggleClass('d-none',tracking!=='QTY');per.prop('required',tracking==='QTY').prop('disabled',false);if(tracking==='UNIT'){per.val('1');qty.attr({step:'1',min:'1'});}else{if(per.val()==='1')per.val('');qty.attr({step:'.01',min:'.01'});}}function addLine(){var node=$(template.content.cloneNode(true));container.append(node);var row=container.children().last();syncTracking(row);renumber();row.find('.pr-catalog').focus();}$('#addPrLine').on('click',addLine);container.on('change','.pr-catalog',function(){syncTracking($(this).closest('.pr-line'));});container.on('click','.remove-pr-line',function(){if(container.children().length<=1)return;$(this).closest('.pr-line').remove();renumber();});addLine();});</script>
<script>$(document).on('click','.btn-pr-detail',async function(){var creator=document.getElementById('prCreatorName'),requester=document.getElementById('prRequesterName'),approver=document.getElementById('prApproverName');creator.textContent=requester.textContent=approver.textContent='-';try{var response=await fetch('<?=base_url('purchases/request_detail/')?>'+this.dataset.id,{headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json(),r=json.request||{};creator.textContent=r.creator_name||r.creator_username||('Pengguna #'+(r.requested_by||'-'));requester.textContent=r.applicant_display||creator.textContent;approver.textContent=r.approver_name||r.approver_username||'-';}catch(error){creator.textContent=requester.textContent=approver.textContent='Data tidak tersedia';}});</script>
<script>$(function(){var pdfModal=$('<div class="modal fade" id="prPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content" style="height:min(92vh,900px)"><div class="modal-header"><h5 class="modal-title">PDF Permintaan Pembelian</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="PDF Permintaan Pembelian" style="width:100%;height:100%;border:0"></iframe></div></div></div></div>').appendTo(document.body);$(document).on('click','a[href*="purchases/request_pdf/"]',function(e){e.preventDefault();pdfModal.find('iframe').attr('src',this.href);bootstrap.Modal.getOrCreateInstance(pdfModal[0]).show();});pdfModal.on('hidden.bs.modal',function(){pdfModal.find('iframe').attr('src','about:blank')});$('form[action*="purchases/approve_request/"]').each(function(){var approve=$(this),id=approve.attr('action').split('/').pop(),button=$('<button type="button" class="btn btn-sm btn-outline-danger ms-1">Reject</button>');approve.after(button);button.on('click',function(){Swal.fire({icon:'warning',title:'Tolak Purchase Request?',input:'textarea',inputLabel:'Alasan penolakan',inputPlaceholder:'Jelaskan alasan agar pembuat dapat memperbaiki PR',inputValidator:v=>!String(v||'').trim()?'Alasan penolakan wajib diisi.':undefined,showCancelButton:true,confirmButtonText:'Tolak PR',cancelButtonText:'Batal'}).then(function(r){if(!r.isConfirmed)return;var form=$('<form method="post" action="<?=base_url('purchases/reject_request/')?>'+id+'"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="return_step" value="request"><input type="hidden" name="reason"></form>');form.find('[name=reason]').val(r.value);form.appendTo(document.body).trigger('submit');});});});});</script>
File diff suppressed because one or more lines are too long
+32
View File
@@ -0,0 +1,32 @@
<?php $csrfName=$this->security->get_csrf_token_name();$csrfHash=$this->security->get_csrf_hash();function arMoney($v){return'Rp '.number_format((float)$v,2,',','.');} ?>
<link rel="stylesheet" href="<?=base_url('assets/css/invoices-professional.css').'?v='.filemtime(FCPATH.'assets/css/invoices-professional.css')?>">
<link rel="stylesheet" href="<?=base_url('assets/css/receivables-professional.css').'?v='.filemtime(FCPATH.'assets/css/receivables-professional.css')?>">
<section class="receivable-workspace">
<header class="invoice-hero"><div><span>SIKLUS PENJUALAN</span><h2>Piutang & Pembayaran</h2><p>Alokasikan pelunasan melalui akun Aktiva atau Passiva, termasuk pemindahbukuan simpanan customer.</p></div><a class="btn btn-light" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i> Daftar Invoice</a></header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan"><a href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a><a href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span></a><a href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span></a><a class="active" href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span></a><a href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span></a><a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a></nav>
<div class="receivable-grid">
<div class="card receivable-card">
<div class="card-header"><div><b>1. Pilih Customer & Invoice</b><small>Hanya invoice posted yang masih memiliki saldo.</small></div></div>
<div class="card-body"><label class="form-label">Customer</label><select id="payCustomer" class="form-select"></select><div class="allocation-toolbar"><span id="invoiceSelectionHint">Pilih customer terlebih dahulu.</span><div><button type="button" id="allocateAll" class="btn btn-sm btn-outline-primary">Alokasikan Semua</button><button type="button" id="clearAllocations" class="btn btn-sm btn-light">Kosongkan</button></div></div><div id="invoiceAllocations" class="invoice-allocation-list"><div class="ar-empty"><i class="bi bi-receipt"></i><span>Invoice terbuka akan tampil di sini.</span></div></div></div>
</div>
<div class="card receivable-card payment-panel">
<div class="card-header"><div><b>2. Detail Penerimaan</b><small>Bukti pembayaran wajib dan jurnal dibuat otomatis.</small></div></div>
<div class="card-body"><form id="paymentForm" action="<?=base_url('receivables/payment')?>" enctype="multipart/form-data"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><input type="hidden" name="customer_id" id="paymentCustomerId">
<div class="row g-3"><div class="col-md-6"><label class="form-label">Tanggal Pelunasan</label><input class="form-control" type="date" name="date" value="<?=date('Y-m-d')?>" required></div><div class="col-md-6"><label class="form-label">Referensi Pembayaran</label><input class="form-control" name="reference_no" placeholder="Nomor transfer atau referensi pemindahbukuan"></div></div>
<div class="source-title"><div><b>Akun Penerimaan / Pelunasan</b><small>Pilih Aktiva untuk dana masuk, atau Passiva untuk pemindahbukuan simpanan customer.</small></div><button class="btn btn-sm btn-outline-primary" type="button" id="addPaymentSource"><i class="bi bi-plus-lg"></i> Tambah Akun</button></div><div id="paymentSources"></div>
<div class="payment-balance"><div><span>Total dialokasikan</span><b id="allocatedTotal">Rp 0,00</b></div><div><span>Total sumber penerimaan</span><b id="sourceTotal">Rp 0,00</b></div><div class="difference"><span>Selisih</span><strong id="paymentDifference">Rp 0,00</strong></div></div>
<label class="form-check mt-3"><input class="form-check-input" type="checkbox" name="allow_advance" value="1" id="allowAdvance"><span class="form-check-label">Catat kelebihan sebagai uang muka customer</span></label>
<div class="mt-3"><label class="form-label">Bukti Pembayaran <span class="text-danger">*</span></label><small class="d-block text-muted mb-2">JPG, PNG, WEBP, GIF, atau PDF. Maksimal 10 file, masing-masing 5 MB.</small><input type="file" id="paymentAttachments" name="attachments[]" accept="image/jpeg,image/png,image/webp,image/gif,application/pdf,.pdf" multiple hidden><div id="paymentAttachmentGrid" class="payment-attachment-grid"><button type="button" class="attachment-add" id="addPaymentAttachment"><i class="bi bi-plus-lg"></i><span>Tambah</span></button></div></div>
<div class="mt-3"><label class="form-label">Catatan</label><textarea class="form-control" name="notes" rows="2" placeholder="Keterangan penerimaan atau informasi tambahan"></textarea></div>
<?php if($can_post):?><button class="btn btn-success w-100 mt-3" type="submit" id="postPayment"><i class="bi bi-check-circle"></i> Post Pembayaran</button><?php else:?><div class="alert alert-warning mt-3 mb-0">Anda tidak memiliki permission untuk memposting pembayaran.</div><?php endif?>
</form></div>
</div>
</div>
<div class="card receivable-card mt-3"><div class="card-header"><div><b>Riwayat Pembayaran Customer</b><small>Bukti penerimaan dapat dipratinjau sebagai PDF.</small></div></div><div class="card-body" id="paymentHistory"><div class="ar-empty"><i class="bi bi-clock-history"></i><span>Pilih customer untuk melihat riwayat.</span></div></div></div>
<div class="card receivable-card mt-3"><div class="card-header"><div><b>Ringkasan Aging Piutang</b><small>Saldo terbuka berdasarkan umur jatuh tempo.</small></div></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Customer</th><th class="text-end">Belum jatuh tempo</th><th class="text-end">1–30 hari</th><th class="text-end">31–60 hari</th><th class="text-end">61–90 hari</th><th class="text-end">&gt;90 hari</th><th class="text-end">Total</th><th></th></tr></thead><tbody><?php foreach($aging as$r):?><tr><td><b><?=html_escape($r->nama)?></b></td><?php foreach(array('current_amount','days_1_30','days_31_60','days_61_90','days_over_90','total')as$f):?><td class="text-end"><?=arMoney($r->$f)?></td><?php endforeach?><td><a class="btn btn-sm btn-outline-secondary" href="<?=base_url('receivables/statement/'.$r->customer_id)?>">Statement</a></td></tr><?php endforeach?><?php if(!$aging):?><tr><td colspan="8" class="text-center text-muted py-4">Tidak ada saldo piutang terbuka.</td></tr><?php endif?></tbody></table></div></div>
</section>
<div class="modal fade" id="paymentPreviewModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Pratinjau Dokumen</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF"></iframe><img alt="Pratinjau gambar"></div></div></div></div>
<script>window.RECEIVABLE_PAGE=<?=json_encode(array('base'=>base_url(),'csrfName'=>$csrfName,'csrfHash'=>$csrfHash,'focus'=>$focus_invoice),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/receivables-professional.js').'?v='.filemtime(FCPATH.'assets/js/receivables-professional.js')?>"></script>
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><title>Receipt <?=html_escape($payment->payment_no)?></title><style>body{font:14px Arial;max-width:760px;margin:30px auto}.right{text-align:right}table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #ddd}@media print{button{display:none}}</style></head><body><button onclick="print()">Cetak</button><h2>BUKTI PENERIMAAN</h2><p><b><?=html_escape($payment->payment_no)?></b><br>Tanggal: <?=$payment->tanggal?><br>Diterima dari: <?=html_escape($payment->nama)?></p><table><thead><tr><th>Invoice</th><th class="right">Alokasi</th></tr></thead><tbody><?php foreach($allocations as$a):?><tr><td><?=html_escape($a->no_invoice)?></td><td class="right"><?=number_format($a->amount,0,',','.')?></td></tr><?php endforeach?></tbody><tfoot><tr><th>Total diterima</th><th class="right"><?=number_format($payment->jumlah,0,',','.')?></th></tr></tfoot></table><p>Referensi: <?=html_escape($payment->reference_no?:'-')?></p></body></html>
@@ -0,0 +1 @@
<div class="container py-3"><div class="d-flex justify-content-between"><div><h4>Customer Statement</h4><p><?=html_escape($customer->nama)?> · <?=html_escape($from)?> s/d <?=html_escape($to)?></p></div><button onclick="print()" class="btn btn-outline-secondary h-25">Cetak</button></div><div class="table-responsive"><table class="table"><thead><tr><th>Tanggal</th><th>Dokumen</th><th>Debit</th><th>Kredit</th></tr></thead><tbody><?php foreach($invoices as$i):?><tr><td><?=$i->tanggal?></td><td><?=html_escape($i->no_invoice)?></td><td><?=number_format($i->total,0,',','.')?></td><td></td></tr><?php endforeach?><?php foreach($payments as$p):?><tr><td><?=$p->tanggal?></td><td><?=html_escape($p->payment_no)?></td><td></td><td><?=number_format($p->jumlah,0,',','.')?></td></tr><?php endforeach?></tbody></table></div></div>
+23
View File
@@ -0,0 +1,23 @@
<div class="container-fluid py-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3"><div><h4 class="mb-1">Role & Permission</h4><p class="text-muted mb-0">Role bawaan memiliki tanggung jawab standar. Role custom dapat dibuat sesuai kebutuhan perusahaan.</p></div><button class="btn btn-primary" id="addRole"><i class="bi bi-plus-circle me-1"></i>Role Custom</button></div>
<div class="alert alert-warning"><b>Admin = Master Admin.</b> Role ini selalu mempunyai akses penuh untuk melihat, membuat, mengubah, menghapus, posting, dan approve seluruh modul. Permission-nya tidak dapat dikurangi.</div>
<div class="row g-3">
<div class="col-lg-4"><div class="card h-100"><div class="card-header">Daftar dan Tanggung Jawab Role</div><div class="list-group list-group-flush" id="roleList">
<?php foreach($roles as$r):?><button type="button" class="list-group-item list-group-item-action role-item" data-id="<?=$r->id?>"><div class="d-flex justify-content-between"><b><?=html_escape($r->nama_role)?></b><span><?php if($r->is_super_admin):?><span class="badge bg-danger">Master Admin</span><?php elseif($r->is_system):?><span class="badge bg-secondary">Bawaan</span><?php else:?><span class="badge bg-info text-dark">Custom</span><?php endif?></span></div><small class="text-muted"><?=html_escape($r->description?:'Belum ada uraian tanggung jawab')?></small></button><?php endforeach?>
</div></div></div>
<div class="col-lg-8"><div class="card"><div class="card-header" id="formTitle">Pilih role untuk melihat permission</div><div class="card-body"><form id="roleForm"><input type="hidden" name="id" id="roleId"><div class="row g-2 mb-3"><div class="col-md-4"><label class="form-label">Nama Role</label><input class="form-control" name="name" id="roleName" required></div><div class="col-md-6"><label class="form-label">Tanggung Jawab Role</label><input class="form-control" name="description" id="roleDescription" required placeholder="Jelaskan pekerjaan yang ditangani"></div><div class="col-md-2 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" id="roleActive" value="1"> Aktif</label></div></div>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Modul</th><?php foreach($actions as$label):?><th class="text-center"><?=$label?></th><?php endforeach?></tr></thead><tbody><?php foreach($features as$key=>$label):?><tr><td><b><?=$label?></b></td><?php foreach($actions as$action=>$actionLabel):?><td class="text-center"><input class="form-check-input permission-box" type="checkbox" name="permissions[<?=$key?>][]" value="<?=$action?>" data-feature="<?=$key?>" data-action="<?=$action?>"></td><?php endforeach?></tr><?php endforeach?></tbody></table></div>
<div class="d-flex justify-content-between mt-3"><button type="button" class="btn btn-outline-danger" id="deleteRole" hidden>Hapus Role Custom</button><button class="btn btn-success ms-auto" id="saveRole">Simpan Permission</button></div>
</form></div></div></div>
</div>
</div>
<script>
$(function(){var isSuper=false,isSystem=false;
function reset(){isSuper=false;isSystem=false;$('#roleForm')[0].reset();$('#roleId').val('');$('.permission-box,#roleName,#roleDescription,#roleActive,#saveRole').prop('disabled',false);$('#deleteRole').prop('hidden',true);$('#formTitle').text('Role Custom Baru');}
$('#addRole').on('click',reset);
$('.role-item').on('click',function(){$('.role-item').removeClass('active');$(this).addClass('active');$.get('<?=base_url('roles/detail/')?>'+$(this).data('id'),function(r){if(!r.status)return;var x=r.result;reset();isSuper=Number(x.is_super_admin)===1;isSystem=Number(x.is_system)===1;$('#roleId').val(x.id);$('#roleName').val(x.nama_role);$('#roleDescription').val(x.description);$('#roleActive').prop('checked',Number(x.is_active)===1);$('#formTitle').text(x.nama_role+(isSuper?' · Master Admin':(isSystem?' · Role Bawaan':' · Role Custom')));Object.keys(x.permissions||{}).forEach(function(f){(x.permissions[f]||[]).forEach(function(a){$('.permission-box[data-feature="'+f+'"][data-action="'+a+'"]').prop('checked',true);});});if(isSuper){$('.permission-box').prop('checked',true).prop('disabled',true);$('#roleName,#roleDescription,#roleActive,#saveRole').prop('disabled',true);}$('#deleteRole').prop('hidden',isSystem||isSuper);},'json');});
$(document).on('change','.permission-box[data-action="can_view"]',function(){if(!this.checked)$('.permission-box[data-feature="'+$(this).data('feature')+'"]').prop('checked',false);});$(document).on('change','.permission-box:not([data-action="can_view"])',function(){if(this.checked)$('.permission-box[data-feature="'+$(this).data('feature')+'"][data-action="can_view"]').prop('checked',true);});
$('#roleForm').on('submit',function(e){e.preventDefault();$.post('<?=base_url('roles/save')?>',$(this).serialize(),function(r){if(r.status){Swal.fire('Sukses',r.message,'success').then(function(){location.reload();});}else Swal.fire('Gagal',r.message,'error');},'json');});
$('#deleteRole').on('click',function(){var id=$('#roleId').val();if(!id)return;Swal.fire({title:'Hapus role custom?',icon:'warning',showCancelButton:true,confirmButtonText:'Hapus'}).then(function(x){if(x.isConfirmed)$.post('<?=base_url('roles/delete/')?>'+id,function(r){if(r.status)location.reload();else Swal.fire('Gagal',r.message,'error');},'json');});});
});
</script>
+2
View File
@@ -0,0 +1,2 @@
<?php function dpQty($v){return floor($v)==$v?number_format($v,0,',','.'):number_format($v,4,',','.');}?>
<!doctype html><html><head><meta charset="utf-8"><style>@page{margin:30px}body{font-family:DejaVu Sans,sans-serif;font-size:10px;color:#20242c}h1{margin:0}.head{width:100%;margin-bottom:20px}.right{text-align:right}.muted{color:#667085}.grid{width:100%;border-collapse:collapse}.grid th,.grid td{padding:8px;border:1px solid #d8dee8}.grid th{background:#eef2f6;font-size:8px}.sign{width:100%;text-align:center;margin-top:45px}.sign td{width:25%;height:90px;vertical-align:top}</style></head><body><table class="head"><tr><td><h1><?=html_escape($pdf_company->name??'Perusahaan')?></h1><div class="muted"><?=html_escape($pdf_company->address??'')?></div></td><td class="right"><h1>SURAT JALAN</h1><b><?=html_escape($delivery->delivery_no)?></b><br><?=$delivery->delivery_date?></td></tr></table><table width="100%"><tr><td><b>Customer</b><br><?=html_escape($delivery->customer_name)?><br><?=nl2br(html_escape($delivery->address?:$delivery->customer_address))?></td><td class="right"><b>Invoice:</b> <?=html_escape($delivery->no_invoice?:'-')?><br><b>Gudang:</b> <?=html_escape($delivery->warehouse_name?:'-')?><br><b>Kurir/Resi:</b> <?=html_escape(($delivery->courier?:'-').' / '.($delivery->tracking_no?:'-'))?></td></tr></table><br><table class="grid"><thead><tr><th>NO</th><th>BARANG / SPESIFIKASI</th><th>QTY</th><th>CATATAN KONDISI</th></tr></thead><tbody><?php foreach($lines as$n=>$l):?><tr><td><?=$n+1?></td><td><b><?=html_escape($l->description)?></b><br><?=html_escape($l->specification?:'-')?></td><td><?=dpQty($l->qty).' '.html_escape($l->unit)?></td><td><?=html_escape($l->condition_notes?:'-')?></td></tr><?php endforeach?></tbody></table><h3>Barcode / Serial Number</h3><table class="grid"><thead><tr><th>BARCODE</th><th>SERIAL NUMBER</th><th>QTY</th><th>STATUS</th></tr></thead><tbody><?php foreach($barcodes as$b):?><tr><td><?=html_escape($b->barcode)?></td><td><?=html_escape($b->serial_number?:'-')?></td><td><?=dpQty($b->qty)?></td><td><?=html_escape($b->status)?></td></tr><?php endforeach?></tbody></table><p><b>Catatan:</b> <?=html_escape($delivery->notes?:'-')?></p><table class="sign"><tr><td>Pembuat<br><br><br><b><?=html_escape($delivery->creator_name?:'-')?></b></td><td>Pengirim<br><br><br>(........................)</td><td>Penerima<br><br><br>(........................)</td><td>Tanggal diterima<br><br><br>........................</td></tr></table></body></html>
+25
View File
@@ -0,0 +1,25 @@
<?php
$csrfName=$this->security->get_csrf_token_name();$csrfHash=$this->security->get_csrf_hash();$p=$permissions;
$section=isset($section)?$section:'summary';
$pageTitle=$section==='delivery'?'Pengiriman / Surat Jalan':($section==='return'?'Retur Penjualan & Refund':'Ringkasan Penjualan');
$pageIntro=$section==='delivery'?'Kelola bukti pengiriman, posting pengeluaran stok, dan Surat Jalan dari invoice yang telah disetujui.':($section==='return'?'Kelola pengajuan retur, pemeriksaan barang kembali, pengembalian stok, credit note, dan refund secara tertelusur.':'Pusat pemantauan invoice, pengiriman, piutang, dan retur tanpa form transaksi yang tumpang tindih.');
function saleMoney($v){return'Rp '.number_format((float)$v,2,',','.');}
function saleStatus($v){$m=array('draft'=>'Draft','submitted'=>'Menunggu Approval','approved'=>'Disetujui','posted'=>'Posted','unpaid'=>'Belum Lunas','partial'=>'Sebagian','paid'=>'Lunas','received'=>'Diterima','completed'=>'Selesai','partially_returned'=>'Retur Sebagian','returned'=>'Diretur','credit_note'=>'Credit Note','restock'=>'Kembali ke Stok','quarantine'=>'Karantina','damaged'=>'Rusak','refund'=>'Refund','replacement'=>'Penggantian');return$m[$v]??ucwords(str_replace('_',' ',$v));}
?>
<link rel="stylesheet" href="<?=base_url('assets/css/sales-professional.css').'?v='.filemtime(FCPATH.'assets/css/sales-professional.css')?>">
<link rel="stylesheet" href="<?=base_url('assets/css/invoices-professional.css').'?v='.filemtime(FCPATH.'assets/css/invoices-professional.css')?>">
<style>.sales-page[data-section="summary"] #salesDelivery,.sales-page[data-section="delivery"] .sales-metrics,.sales-page[data-section="delivery"] #salesReturn,.sales-page[data-section="return"] .sales-metrics,.sales-page[data-section="return"] #salesDelivery{display:none!important}</style>
<section class="sales-page" data-section="<?=html_escape($section)?>">
<header class="sales-hero"><div><span>SIKLUS PENJUALAN</span><h2><?=html_escape($pageTitle)?></h2><p><?=html_escape($pageIntro)?></p></div><?php if($section==='return'&&$p['create']):?><a class="btn btn-primary" href="<?=base_url('sales/deliveries')?>"><i class="bi bi-arrow-counterclockwise"></i> Ajukan dari Surat Jalan</a><?php elseif($section==='delivery'):?><a class="btn btn-outline-primary" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i> Lihat Invoice</a><?php elseif($p['create']):?><a class="btn btn-primary" href="<?=base_url('invoices/create')?>"><i class="bi bi-plus-lg"></i> Buat Invoice</a><?php endif?></header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan"><a class="<?=$section==='summary'?'active':''?>" href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a><a href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span><?php if($summary['draft']):?><b><?=$summary['draft']?></b><?php endif?></a><a class="<?=$section==='delivery'?'active':''?>" href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span><?php if($summary['delivery']):?><b><?=$summary['delivery']?></b><?php endif?></a><a href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span><?php if($summary['open_invoice']):?><b><?=$summary['open_invoice']?></b><?php endif?></a><a class="<?=$section==='return'?'active':''?>" href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span><?php if($summary['return']):?><b><?=$summary['return']?></b><?php endif?></a><a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a></nav>
<div class="sales-metrics"><article><small>Penjualan bulan ini</small><strong><?=saleMoney($summary['month_sales'])?></strong></article><article><small>Invoice belum lunas</small><strong><?=$summary['open_invoice']?></strong><span><?=$summary['overdue']?> jatuh tempo</span></article><article><small>Draft / approval</small><strong><?=$summary['draft']?></strong><span><a href="<?=base_url('invoices')?>">Buka daftar invoice</a></span></article><article><small>Operasional</small><strong><?=$summary['delivery']+$summary['return']?></strong><span><?=$summary['delivery']?> pengiriman · <?=$summary['return']?> retur</span></article></div>
<div class="row g-3">
<div class="col-12" id="salesDelivery"><div class="card"><div class="card-header"><div><b>Pengiriman / Surat Jalan</b><small>Posting Surat Jalan adalah satu-satunya proses pengurangan stok fisik.</small></div><a class="btn btn-sm btn-outline-primary" href="<?=base_url('invoices')?>">Lihat Invoice</a></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Surat Jalan</th><th>Customer / Invoice</th><th>Tanggal</th><th>Gudang</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($deliveries as$d):?><tr><td><b><?=html_escape($d->delivery_no)?></b></td><td><?=html_escape($d->customer_name)?><small><?=html_escape($d->no_invoice?:'-')?></small></td><td><?=html_escape($d->delivery_date)?></td><td><?=html_escape($d->warehouse_name?:'-')?></td><td><span class="status status-<?=html_escape($d->status)?>"><?=saleStatus($d->status)?></span></td><td class="actions"><button class="btn btn-sm btn-outline-primary detail-delivery" data-id="<?=$d->id?>">Detail</button><?php if($p['export']):?><button class="btn btn-sm btn-outline-danger pdf-open" data-url="<?=base_url('sales/delivery_pdf/'.$d->id)?>">PDF</button><?php endif?><?php if($d->status==='draft'&&$p['post']):?><button class="btn btn-sm btn-success sale-action" data-url="<?=base_url('sales/post_delivery/'.$d->id)?>" data-confirm="Posting Surat Jalan dan keluarkan stok?">Post Pengiriman</button><?php endif?><?php if(in_array($d->status,array('posted','partially_returned'),true)&&$p['create']):?><button class="btn btn-sm btn-warning new-return" data-id="<?=$d->id?>">Ajukan Retur</button><?php endif?></td></tr><?php endforeach?><?php if(!$deliveries):?><tr><td colspan="6" class="empty">Belum ada dokumen pengiriman.</td></tr><?php endif?></tbody></table></div></div></div>
<div class="col-12" id="salesReturn"><div class="card"><div class="card-header"><div><b>Retur Penjualan & Refund</b><small>Retur ditelusuri dari Surat Jalan dan barcode yang benar-benar dikirim.</small></div></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Dokumen</th><th>Customer / Sumber</th><th>Tanggal</th><th>Penyelesaian</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($returns as$r):?><tr><td><b><?=html_escape($r->return_no)?></b></td><td><?=html_escape($r->customer_name)?><small><?=html_escape(($r->no_invoice?:'-').' / '.($r->delivery_no?:'-'))?></small></td><td><?=html_escape($r->return_date)?></td><td><?=saleStatus($r->resolution)?></td><td><span class="status status-<?=html_escape($r->status)?>"><?=saleStatus($r->status)?></span></td><td class="actions"><button class="btn btn-sm btn-outline-primary detail-return" data-id="<?=$r->id?>">Detail</button><?php if($p['export']):?><button class="btn btn-sm btn-outline-danger pdf-open" data-url="<?=base_url('sales/return_pdf/'.$r->id)?>">PDF</button><?php endif?><?php if($r->status==='draft'&&$p['create']):?><button class="btn btn-sm btn-primary sale-action" data-url="<?=base_url('sales/submit_return/'.$r->id)?>" data-confirm="Ajukan retur ini?">Ajukan</button><?php endif?><?php if($r->status==='submitted'&&$p['approve']):?><button class="btn btn-sm btn-success sale-action" data-url="<?=base_url('sales/approve_return/'.$r->id)?>" data-confirm="Setujui retur ini?">Setujui</button><?php endif?><?php if($r->status==='approved'&&$p['post']):?><button class="btn btn-sm btn-info sale-action" data-url="<?=base_url('sales/receive_return/'.$r->id)?>" data-confirm="Konfirmasi barang diterima dan diperiksa?">Terima</button><?php endif?><?php if($r->status==='received'&&$p['post']):?><button class="btn btn-sm btn-success sale-action" data-url="<?=base_url('sales/post_return/'.$r->id)?>" data-confirm="Posting stok dan penyelesaian retur?">Post Retur</button><?php endif?></td></tr><?php endforeach?><?php if(!$returns):?><tr><td colspan="6" class="empty">Belum ada retur penjualan.</td></tr><?php endif?></tbody></table></div></div></div>
</div>
</section>
<div class="modal fade" id="detailModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Detail Dokumen</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="detailBody"></div></div></div></div>
<div class="modal fade" id="pdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5>Pratinjau PDF</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF"></iframe></div></div></div></div>
<div class="modal fade" id="returnModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><form id="returnForm" enctype="multipart/form-data"><div class="modal-header"><h5>Pengajuan Retur Penjualan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><input type="hidden" name="sales_delivery_id"><div class="row g-3"><div class="col-md-3"><label>Tanggal retur</label><input type="date" name="return_date" value="<?=date('Y-m-d')?>" class="form-control" required></div><div class="col-md-4"><label>Penyelesaian</label><select name="resolution" class="form-select"><option value="credit_note">Credit Note</option><option value="restock">Kembali ke stok</option><option value="quarantine">Karantina</option><option value="damaged">Rusak</option><option value="refund">Refund</option><option value="replacement">Penggantian</option></select></div><div class="col-md-5"><label>Kategori masalah</label><input name="problem_category" class="form-control"></div><div class="col-12"><label>Alasan</label><textarea name="reason" class="form-control" required></textarea></div></div><div id="returnLines" class="mt-3"></div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan Draft Retur</button></div></form></div></div></div>
<script>window.SALES_PAGE=<?=json_encode(array('base'=>base_url(),'csrfName'=>$csrfName,'csrfHash'=>$csrfHash,'sourceOrder'=>0,'focus'=>$focus,'accounts'=>array()),JSON_UNESCAPED_SLASHES)?>;</script>
<script src="<?=base_url('assets/js/sales-professional.js').'?v='.filemtime(FCPATH.'assets/js/sales-professional.js')?>"></script>
+23
View File
@@ -0,0 +1,23 @@
<?php
$company=$pdf_company;$groups=array();
foreach($lines as$l){
$detail=trim((string)($l->keterangan?:$l->specification));
$key=$l->service_date.'|'.$l->items_id.'|'.$l->harga.'|'.$l->tax_rate.'|'.$detail;
$label=trim(($l->item_code?$l->item_code.' - ':'').$l->nama_item);
if(!isset($groups[$key]))$groups[$key]=array('date'=>$l->service_date,'item'=>$label,'detail'=>$detail,'qty'=>0,'unit'=>$l->unit,'price'=>$l->harga,'discount'=>0,'tax'=>0,'total'=>0);
$groups[$key]['qty']+=(float)$l->qty;$groups[$key]['discount']+=(float)$l->discount_amount;$groups[$key]['tax']+=(float)$l->tax_amount;$groups[$key]['total']+=(float)$l->subtotal;
}
function ipMoney($v){return number_format((float)$v,2,',','.');}
function ipQty($v){return floor($v)==$v?number_format($v,0,',','.'):number_format($v,4,',','.');}
?>
<!doctype html><html><head><meta charset="utf-8"><style>
@page{margin:28px 34px}body{font-family:DejaVu Sans,sans-serif;font-size:10px;color:#20242c}h1{font-size:20px;margin:0}.muted{color:#667085}.head{width:100%;margin-bottom:18px}.head td{vertical-align:top}.right{text-align:right}table.grid{width:100%;border-collapse:collapse}table.grid th{background:#eef2f6;text-align:left;font-size:8px;letter-spacing:.04em}table.grid th,table.grid td{border:1px solid #d8dee8;padding:7px}.num{text-align:right}.summary{margin:14px 0 0 auto;width:44%}.summary td{padding:4px}.page{page-break-before:always}.sign{margin-top:40px;width:100%;text-align:center}.sign td{width:33%;height:80px;vertical-align:top}.repeat{display:table-header-group}
</style></head><body>
<table class="head"><tr><td><h1><?=html_escape($company->name??'Perusahaan')?></h1><div class="muted"><?=html_escape($company->address??'')?></div></td><td class="right"><h1>INVOICE</h1><b><?=html_escape($invoice->no_invoice)?></b><br><?=html_escape($invoice->tanggal)?></td></tr></table>
<table width="100%"><tr><td><b>Kepada</b><br><?=html_escape($invoice->customer_name)?><br><?=nl2br(html_escape($invoice->customer_address?:'-'))?></td><td class="right"><b>Periode</b><br><?=html_escape(($invoice->period_start?:$invoice->tanggal).' s.d. '.($invoice->period_end?:$invoice->tanggal))?><br><b>Jatuh tempo:</b> <?=html_escape($invoice->jatuh_tempo)?><br><b>Pengakuan:</b> <?=$invoice->recognition_policy==='accrual'?'Akrual':'Setelah pembayaran'?></td></tr></table><br>
<table class="grid"><thead><tr><th>TANGGAL</th><th>BARANG / JASA & KETERANGAN</th><th>QTY</th><th class="num">HARGA</th><th class="num">DISKON</th><th class="num">PAJAK</th><th class="num">SUBTOTAL</th></tr></thead><tbody><?php foreach($groups as$g):?><tr><td><?=html_escape($g['date'])?></td><td><b><?=html_escape($g['item'])?></b><br><span class="muted"><?=html_escape($g['detail']?:'-')?></span></td><td><?=ipQty($g['qty']).' '.html_escape($g['unit'])?></td><td class="num"><?=ipMoney($g['price'])?></td><td class="num"><?=ipMoney($g['discount'])?></td><td class="num"><?=ipMoney($g['tax'])?></td><td class="num"><b><?=ipMoney($g['total'])?></b></td></tr><?php endforeach?></tbody></table>
<table class="summary"><tr><td>Subtotal bersih</td><td class="num"><?=ipMoney($invoice->subtotal_before_tax)?></td></tr><tr><td>Diskon</td><td class="num"><?=ipMoney($invoice->discount_total)?></td></tr><tr><td>Pajak</td><td class="num"><?=ipMoney($invoice->tax_total)?></td></tr><tr><td><b>Total</b></td><td class="num"><b>Rp <?=ipMoney($invoice->total)?></b></td></tr><tr><td>Sudah dibayar</td><td class="num"><?=ipMoney($invoice->total_bayar)?></td></tr><tr><td><b>Sisa piutang</b></td><td class="num"><b><?=ipMoney($invoice->sisa_piutang)?></b></td></tr></table>
<p><b>Surat Jalan:</b> <?=html_escape(implode(', ',array_map(function($d){return$d->delivery_no;},$deliveries))?:'-')?><br><b>Catatan:</b> <?=html_escape($invoice->keterangan?:'-')?></p>
<table class="sign"><tr><td>Dibuat oleh<br><br><br><b><?=html_escape($invoice->creator_name?:'-')?></b></td><td>Disetujui<br><br><br><b><?=html_escape($invoice->approver_name?:'-')?></b></td><td>Diterima Customer</td></tr></table>
<div class="page"><h2>Lampiran Detail Barcode / Serial Number</h2><p class="muted">Lampiran invoice <?=html_escape($invoice->no_invoice)?> · <?=count($barcodes)?> baris barcode</p><table class="grid"><thead class="repeat"><tr><th>TANGGAL</th><th>SURAT JALAN</th><th>BARANG</th><th>BARCODE / SERIAL</th><th>QTY</th><th>GUDANG</th><th>STATUS</th></tr></thead><tbody><?php foreach($barcodes as$b):?><tr><td><?=html_escape($b->service_date)?></td><td><?=html_escape($b->delivery_no?:'-')?></td><td><?=html_escape(trim(($b->item_code?$b->item_code.' - ':'').$b->nama_barang))?></td><td><b><?=html_escape($b->barcode)?></b><br><?=html_escape($b->serial_number?:'-')?></td><td><?=ipQty($b->qty)?></td><td><?=html_escape($b->warehouse_name?:'-')?></td><td><?=in_array($invoice->workflow_status,array('posted','approved'),true)?'Terkirim':'Dialokasikan'?></td></tr><?php endforeach?><?php if(!$barcodes):?><tr><td colspan="7" style="text-align:center">Invoice ini tidak berisi barang gudang sehingga tidak memiliki detail barcode atau Surat Jalan.</td></tr><?php endif?></tbody></table></div>
</body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><style>@page{margin:35px}body{font-family:DejaVu Sans,sans-serif;font-size:11px;color:#20242c}.head{width:100%;margin-bottom:24px}.right{text-align:right}.box{border:1px solid #d8dee8;border-radius:8px;padding:14px}.grid{width:100%;border-collapse:collapse;margin-top:15px}.grid th,.grid td{border:1px solid #d8dee8;padding:8px}.grid th{background:#eef2f6}.total{font-size:18px;text-align:right;margin-top:16px}</style></head><body><table class="head"><tr><td><h2><?=html_escape($pdf_company->name??'Perusahaan')?></h2></td><td class="right"><h2>BUKTI PENERIMAAN</h2><b><?=html_escape($payment->payment_no)?></b></td></tr></table><div class="box"><b>Diterima dari:</b> <?=html_escape($payment->customer_name)?><br><b>Tanggal:</b> <?=$payment->tanggal?><br><b>Referensi:</b> <?=html_escape($payment->reference_no?:'-')?><br><b>Catatan:</b> <?=html_escape($payment->notes?:'-')?></div><h3>Alokasi Invoice</h3><table class="grid"><thead><tr><th>Invoice</th><th class="right">Nilai</th></tr></thead><tbody><?php foreach($allocations as$a):?><tr><td><?=html_escape($a->no_invoice)?></td><td class="right">Rp <?=number_format($a->amount,2,',','.')?></td></tr><?php endforeach?></tbody></table><h3>Sumber Kas/Bank</h3><table class="grid"><thead><tr><th>Akun</th><th class="right">Nilai</th></tr></thead><tbody><?php foreach($sources as$s):?><tr><td><?=html_escape($s->kode_akun.' - '.$s->nama_akun)?></td><td class="right">Rp <?=number_format($s->amount,2,',','.')?></td></tr><?php endforeach?></tbody></table><div class="total"><b>Total diterima: Rp <?=number_format($payment->jumlah,2,',','.')?></b></div></body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><style>@page{margin:30px}body{font-family:DejaVu Sans,sans-serif;font-size:10px;color:#20242c}h1{margin:0}.head{width:100%;margin-bottom:18px}.right{text-align:right}.grid{width:100%;border-collapse:collapse}.grid th,.grid td{padding:8px;border:1px solid #d8dee8}.grid th{background:#eef2f6;font-size:8px}.sign{width:100%;text-align:center;margin-top:45px}.sign td{height:85px;width:25%;vertical-align:top}</style></head><body><table class="head"><tr><td><h1><?=html_escape($pdf_company->name??'Perusahaan')?></h1></td><td class="right"><h1>RETUR PENJUALAN</h1><b><?=html_escape($return->return_no)?></b><br><?=$return->return_date?></td></tr></table><p><b>Customer:</b> <?=html_escape($return->customer_name)?><br><b>Invoice / Surat Jalan:</b> <?=html_escape($return->no_invoice.' / '.$return->delivery_no)?><br><b>Alasan:</b> <?=html_escape($return->reason)?><br><b>Tindakan:</b> <?=html_escape($return->resolution)?> · <b>Status:</b> <?=html_escape($return->status)?></p><table class="grid"><thead><tr><th>BARANG</th><th>QTY</th><th>KONDISI</th><th>NILAI BERSIH</th><th>PAJAK</th></tr></thead><tbody><?php foreach($lines as$l):?><tr><td><?=html_escape($l->kode_barang.' - '.$l->nama_barang)?></td><td><?=number_format($l->qty,floor($l->qty)==$l->qty?0:4,',','.')?></td><td><?=html_escape($l->physical_condition)?></td><td><?=number_format($l->net_amount,2,',','.')?></td><td><?=number_format($l->tax_amount,2,',','.')?></td></tr><?php endforeach?></tbody></table><h3>Barcode / Serial</h3><table class="grid"><thead><tr><th>BARCODE</th><th>SERIAL NUMBER</th><th>QTY</th><th>STATUS</th></tr></thead><tbody><?php foreach($barcodes as$b):?><tr><td><?=html_escape($b->barcode)?></td><td><?=html_escape($b->serial_number?:'-')?></td><td><?=$b->qty?></td><td><?=html_escape($b->status)?></td></tr><?php endforeach?></tbody></table><table class="sign"><tr><td>Pembuat</td><td>Approver</td><td>Pemeriksa Gudang</td><td>Customer</td></tr></table></body></html>
@@ -0,0 +1,5 @@
<?php $csrf=$this->security->get_csrf_token_name();$hash=$this->security->get_csrf_hash();$p=$permissions;?>
<section class="container-fluid py-3"><div class="d-flex justify-content-between align-items-start mb-3"><div><small class="text-muted">DOKUMEN PRA-PENJUALAN</small><h3>Penawaran & Sales Order</h3><p class="text-muted">Dokumen komersial multi-item. Belum mengurangi stok dan belum membuat jurnal.</p></div><?php if($p['create']):?><button class="btn btn-primary" id="newSalesDocument">+ Dokumen Baru</button><?php endif?></div><div class="card"><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Dokumen</th><th>Customer</th><th>Jenis</th><th>Tanggal</th><th class="text-end">Total</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><b><?=html_escape($r->document_no)?></b></td><td><?=html_escape($r->nama)?></td><td><?=$r->document_type==='quotation'?'Penawaran':'Sales Order'?></td><td><?=$r->document_date?></td><td class="text-end">Rp <?=number_format($r->total,2,',','.')?></td><td><?=html_escape(ucwords($r->status))?></td><td><button class="btn btn-sm btn-outline-primary doc-detail" data-id="<?=$r->id?>">Detail</button><?php if($r->status==='draft'&&$p['create']):?><button class="btn btn-sm btn-primary doc-action" data-url="<?=base_url('salesdocuments/workflow/'.$r->id.'/sent')?>">Kirim</button><?php endif?><?php if($r->status==='sent'&&$p['approve']):?><button class="btn btn-sm btn-success doc-action" data-url="<?=base_url('salesdocuments/workflow/'.$r->id.'/accepted')?>">Terima</button><button class="btn btn-sm btn-outline-danger doc-action" data-url="<?=base_url('salesdocuments/workflow/'.$r->id.'/rejected')?>">Tolak</button><?php endif?><?php if($r->document_type==='quotation'&&$r->status==='accepted'&&$p['create']):?><button class="btn btn-sm btn-success doc-action" data-url="<?=base_url('salesdocuments/convert/'.$r->id)?>">Jadikan Sales Order</button><?php endif?><?php if($r->document_type==='sales_order'&&$r->status==='accepted'&&$p['create']):?><a class="btn btn-sm btn-warning" href="<?=base_url('sales?source_order='.$r->id)?>">Proses Pengiriman/Invoice</a><?php endif?></td></tr><?php endforeach?></tbody></table></div></div></section>
<div class="modal fade" id="docFormModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><form id="docForm"><div class="modal-header"><h5>Dokumen Penjualan Baru</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><input type="hidden" name="<?=$csrf?>" value="<?=$hash?>"><div class="row g-3"><div class="col-md-3"><label>Jenis</label><select name="document_type" class="form-select"><option value="quotation">Penawaran</option><option value="sales_order">Sales Order</option></select></div><div class="col-md-4"><label>Customer</label><select name="customer_id" id="docCustomer" class="form-select" required></select></div><div class="col-md-2"><label>Tanggal</label><input type="date" name="document_date" value="<?=date('Y-m-d')?>" class="form-control"></div><div class="col-md-3"><label>Berlaku sampai</label><input type="date" name="valid_until" class="form-control"></div></div><hr><div class="row g-2"><div class="col-md-4"><select id="docItem" class="form-select"></select></div><div class="col-md-2"><input id="docQty" class="form-control" placeholder="Qty" inputmode="decimal"></div><div class="col-md-2"><input id="docPrice" class="form-control" placeholder="Harga" inputmode="decimal"></div><div class="col-md-2"><input id="docTax" class="form-control" placeholder="Pajak %" inputmode="decimal"></div><div class="col-md-2"><button type="button" id="docAdd" class="btn btn-outline-primary w-100">Tambah</button></div></div><div class="table-responsive mt-3"><table class="table" id="docCart"><thead><tr><th>Barang</th><th>Spesifikasi</th><th>Qty</th><th>Harga</th><th>Diskon</th><th>Pajak</th><th>Total</th><th></th></tr></thead><tbody></tbody></table></div><div class="text-end fs-5">Total: <b id="docTotal">Rp 0,00</b></div><label>Catatan</label><textarea name="notes" class="form-control"></textarea></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan Draft</button></div></form></div></div></div>
<div class="modal fade" id="docDetailModal"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5>Detail Dokumen</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="docDetail"></div></div></div></div>
<script>(function($){var base=<?=json_encode(base_url())?>,csrf=<?=json_encode($csrf)?>,hash=<?=json_encode($hash)?>,cart=[],item=null;function e(v){return $('<div>').text(v||'').html()}function n(v){v=String(v||'').replace(/\./g,'').replace(',','.');return Number(v)||0}function m(v){return'Rp '+new Intl.NumberFormat('id-ID',{minimumFractionDigits:2}).format(v||0)}function load(){return Promise.all([$.getJSON(base+'sales/lookup_customers',{q:''}).then(function(r){$('#docCustomer').html(r.rows.map(function(x){return'<option value="'+x.id+'">'+e(x.nama)+'</option>'}).join(''))}),$.getJSON(base+'sales/lookup_items',{q:''}).then(function(r){$('#docItem').html('<option value="">Pilih barang</option>'+r.rows.map(function(x){return'<option data-x="'+encodeURIComponent(JSON.stringify(x))+'" value="'+x.id+'">'+e(x.kode_barang+' - '+x.nama_barang)+'</option>'}).join(''))})])}$('#newSalesDocument').on('click',function(){cart=[];load().then(function(){draw();bootstrap.Modal.getOrCreateInstance(document.getElementById('docFormModal')).show()})});$('#docItem').on('change',function(){item=this.selectedOptions[0].dataset.x?JSON.parse(decodeURIComponent(this.selectedOptions[0].dataset.x)):null;if(item)$('#docPrice').val(new Intl.NumberFormat('id-ID').format(item.harga_jual))});$('#docAdd').on('click',function(){if(!item||n($('#docQty').val())<=0)return;cart.push({item_id:item.id,name:item.kode_barang+' - '+item.nama_barang,qty:n($('#docQty').val()),unit_price:n($('#docPrice').val()),tax_rate:n($('#docTax').val()),discount_amount:0,specification:''});draw()});function draw(){var total=0;$('#docCart tbody').html(cart.map(function(x,i){var t=(x.qty*x.unit_price-x.discount_amount)*(1+x.tax_rate/100);total+=t;return'<tr data-i="'+i+'"><td>'+e(x.name)+'</td><td><input class="form-control form-control-sm spec" value="'+e(x.specification)+'"></td><td>'+x.qty+'</td><td>'+m(x.unit_price)+'</td><td><input class="form-control form-control-sm discount" value="'+x.discount_amount+'"></td><td>'+x.tax_rate+'%</td><td>'+m(t)+'</td><td><button type="button" class="btn btn-sm btn-outline-danger del">×</button></td></tr>'}).join(''));$('#docTotal').text(m(total))}$(document).on('input','.spec,.discount',function(){var tr=$(this).closest('tr'),x=cart[tr.data('i')];x.specification=tr.find('.spec').val();x.discount_amount=n(tr.find('.discount').val());draw()});$(document).on('click','.del',function(){cart.splice($(this).closest('tr').data('i'),1);draw()});$('#docForm').on('submit',function(ev){ev.preventDefault();var f=new FormData(this);f.append('rows',JSON.stringify(cart));$.ajax({url:base+'salesdocuments/save',type:'POST',data:f,processData:false,contentType:false,dataType:'json'}).done(function(r){if(r.status)location.reload();else Swal.fire({toast:true,position:'top-end',icon:'error',title:r.message,showConfirmButton:false,timer:3000})})});$(document).on('click','.doc-action',function(){var u=$(this).data('url');Swal.fire({title:'Lanjutkan proses dokumen?',icon:'question',showCancelButton:true}).then(function(r){if(r.isConfirmed){var d={};d[csrf]=hash;$.post(u,d,function(x){if(x.status)location.reload();else Swal.fire('Gagal',x.message,'error')},'json')}})});$(document).on('click','.doc-detail',function(){$.getJSON(base+'salesdocuments/detail/'+$(this).data('id')).done(function(r){var d=r.document;$('#docDetail').html('<h5>'+e(d.document_no)+'</h5><p>'+e(d.customer_name)+' · '+e(d.document_type)+' · '+e(d.status)+'</p><div class="table-responsive"><table class="table"><thead><tr><th>Barang</th><th>Spesifikasi</th><th>Qty</th><th>Harga</th><th>Total</th></tr></thead><tbody>'+r.lines.map(function(x){return'<tr><td>'+e((x.kode_barang||'')+' - '+(x.nama_barang||x.description))+'</td><td>'+e(x.specification||'-')+'</td><td>'+x.qty+'</td><td>'+m(x.unit_price)+'</td><td>'+m(x.line_total)+'</td></tr>'}).join('')+'</tbody></table></div>');bootstrap.Modal.getOrCreateInstance(document.getElementById('docDetailModal')).show()})})})(jQuery);</script>
+12
View File
@@ -0,0 +1,12 @@
<?php $admin=$this->session->userdata('role')==='Admin';$canCreate=$admin||check_permission('purchases','can_create');$canUpdate=$admin||check_permission('purchases','can_update');$active=count(array_filter($rows,function($r){return(int)$r->is_active===1;}));?>
<div class="container-fluid py-3">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><h4 class="mb-1">Master Supplier</h4><p class="text-muted mb-0">Identitas, pajak, kontak, termin, dan rekening supplier.</p></div><?php if($canCreate):?><button class="btn btn-primary" id="newSupplier"><i class="bi bi-plus-lg me-1"></i>Tambah Supplier</button><?php endif?></div>
<?php if($this->session->flashdata('success')):?><div class="alert alert-success"><?=html_escape($this->session->flashdata('success'))?></div><?php endif?>
<div class="row g-3 mb-3"><div class="col-6 col-md-3"><div class="card"><div class="card-body"><small class="text-muted">Total Supplier</small><h3 class="mb-0"><?=count($rows)?></h3></div></div></div><div class="col-6 col-md-3"><div class="card"><div class="card-body"><small class="text-muted">Supplier Aktif</small><h3 class="mb-0 text-success"><?=$active?></h3></div></div></div></div>
<div class="card"><div class="card-header bg-body d-flex justify-content-between align-items-center"><b>Daftar Supplier</b><select class="form-select form-select-sm w-auto" id="statusFilter"><option value="">Semua status</option><option>Aktif</option><option>Nonaktif</option></select></div><div class="table-responsive"><table class="table align-middle mb-0 responsive-detail-table"><thead><tr><th>Kode</th><th>Supplier</th><th>Kontak</th><th>Termin</th><th>Rekening</th><th>Status</th><th class="text-end">Aksi</th></tr></thead><tbody>
<?php foreach($rows as$r):$j=html_escape(json_encode($r,JSON_HEX_APOS|JSON_HEX_QUOT));?><tr data-status="<?=$r->is_active?'Aktif':'Nonaktif'?>"><td data-label="Kode"><b><?=html_escape($r->supplier_code)?></b></td><td data-label="Supplier"><b><?=html_escape($r->name)?></b><small class="d-block text-muted"><?=html_escape($r->tax_number?:'Tanpa NPWP')?></small></td><td data-label="Kontak"><?=html_escape($r->contact_person?:'-')?><small class="d-block text-muted"><?=html_escape($r->phone?:'-')?></small></td><td data-label="Termin"><?=(int)$r->payment_term_days?> hari</td><td data-label="Rekening"><?=html_escape($r->bank_name?:'-')?><small class="d-block text-muted"><?=html_escape($r->bank_account_no?:'-')?></small></td><td data-label="Status"><span class="badge bg-<?=$r->is_active?'success':'secondary'?>"><?=$r->is_active?'Aktif':'Nonaktif'?></span></td><td data-label="Aksi" class="text-end text-nowrap"><button class="btn btn-sm btn-outline-info detail-supplier" data-row="<?=$j?>"><i class="bi bi-eye"></i> Detail</button><?php if($canUpdate):?> <button class="btn btn-sm btn-outline-primary edit-supplier" data-row="<?=$j?>"><i class="bi bi-pencil"></i> Edit</button> <form method="post" class="d-inline status-form" action="<?=base_url('suppliers/update_status/'.$r->id)?>"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><button class="btn btn-sm btn-outline-<?=$r->is_active?'warning':'success'?>"><?=$r->is_active?'Nonaktifkan':'Aktifkan'?></button></form><?php endif?></td></tr><?php endforeach?>
</tbody></table></div></div>
</div>
<div class="modal fade" id="supplierFormModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-lg"><div class="modal-content"><form method="post" id="supplierForm"><div class="modal-header"><h5 class="modal-title">Tambah Supplier</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><div class="row g-3"><div class="col-md-4"><label class="form-label">Kode Supplier *</label><input class="form-control text-uppercase" name="supplier_code" maxlength="40" required></div><div class="col-md-8"><label class="form-label">Nama Supplier/Toko *</label><input class="form-control" name="name" maxlength="150" required></div><div class="col-md-6"><label class="form-label">NPWP/Nomor Pajak</label><input class="form-control" name="tax_number"></div><div class="col-md-6"><label class="form-label">Termin Pembayaran</label><div class="input-group"><input class="form-control" type="number" name="payment_term_days" min="0" value="30"><span class="input-group-text">hari</span></div></div><div class="col-12"><label class="form-label">Alamat</label><textarea class="form-control" name="address" rows="2"></textarea></div><div class="col-md-4"><label class="form-label">Nama Kontak</label><input class="form-control" name="contact_person"></div><div class="col-md-4"><label class="form-label">Telepon/WhatsApp</label><input class="form-control" name="phone" inputmode="tel"></div><div class="col-md-4"><label class="form-label">Email</label><input class="form-control" type="email" name="email"></div><div class="col-12"><hr><h6>Rekening Pembayaran</h6></div><div class="col-md-4"><label class="form-label">Nama Bank</label><input class="form-control" name="bank_name"></div><div class="col-md-4"><label class="form-label">Nomor Rekening</label><input class="form-control" name="bank_account_no"></div><div class="col-md-4"><label class="form-label">Nama Pemilik</label><input class="form-control" name="bank_account_name"></div><div class="col-12 opening"><hr><h6>Saldo Awal Hutang</h6><div class="alert alert-warning small">Khusus migrasi awal. Setelah tersimpan, koreksi harus melalui transaksi accounting/reversal.</div></div><div class="col-md-6 opening"><label class="form-label">Saldo Awal</label><input class="form-control format-rupiah" name="opening_balance" value="0"></div><div class="col-md-6 opening"><label class="form-label">Tanggal Saldo Awal</label><input class="form-control" type="date" name="opening_balance_date"></div><div class="col-12"><div class="form-check form-switch"><input class="form-check-input" type="checkbox" name="is_active" value="1" id="supplierActive" checked><label class="form-check-label" for="supplierActive">Aktif dan dapat dipilih pada transaksi baru</label></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary">Simpan Supplier</button></div></form></div></div></div>
<div class="modal fade" id="supplierDetailModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Detail Supplier</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="supplierDetail"></div></div></div></div>
<script>document.addEventListener('DOMContentLoaded',function(){const f=document.getElementById('supplierForm'),fm=bootstrap.Modal.getOrCreateInstance(document.getElementById('supplierFormModal')),esc=v=>$('<div>').text(v||'-').html();function open(s){f.reset();f.action=s?'<?=base_url('suppliers/update/')?>'+s.id:'<?=base_url('suppliers/save')?>';f.querySelector('.modal-title').textContent=s?'Edit Supplier':'Tambah Supplier';f.querySelectorAll('.opening').forEach(x=>x.classList.toggle('d-none',!!s));if(s)Object.keys(s).forEach(k=>{let x=f.elements[k];if(!x)return;x.type==='checkbox'?x.checked=Number(s[k])===1:x.value=s[k]??'';});else f.elements.is_active.checked=true;fm.show()}document.getElementById('newSupplier')?.addEventListener('click',()=>open());document.querySelectorAll('.edit-supplier').forEach(b=>b.onclick=()=>open(JSON.parse(b.dataset.row)));document.querySelectorAll('.detail-supplier').forEach(b=>b.onclick=()=>{let s=JSON.parse(b.dataset.row);document.getElementById('supplierDetail').innerHTML='<h5>'+esc(s.supplier_code+' - '+s.name)+'</h5><span class="badge bg-'+(Number(s.is_active)?'success':'secondary')+'">'+(Number(s.is_active)?'Aktif':'Nonaktif')+'</span><hr><dl class="row mb-0"><dt class="col-5">NPWP/Pajak</dt><dd class="col-7">'+esc(s.tax_number)+'</dd><dt class="col-5">Alamat</dt><dd class="col-7">'+esc(s.address)+'</dd><dt class="col-5">Kontak</dt><dd class="col-7">'+esc(s.contact_person)+'<br>'+esc(s.phone)+'<br>'+esc(s.email)+'</dd><dt class="col-5">Termin</dt><dd class="col-7">'+Number(s.payment_term_days||0)+' hari</dd><dt class="col-5">Rekening</dt><dd class="col-7">'+esc(s.bank_name)+'<br>'+esc(s.bank_account_no)+'<br>a.n. '+esc(s.bank_account_name)+'</dd><dt class="col-5">Saldo awal</dt><dd class="col-7">Rp '+Number(s.opening_balance||0).toLocaleString('id-ID',{minimumFractionDigits:2})+'</dd></dl>';bootstrap.Modal.getOrCreateInstance(document.getElementById('supplierDetailModal')).show()});document.querySelectorAll('.status-form').forEach(x=>x.onsubmit=function(e){e.preventDefault();let on=this.querySelector('button').textContent.trim()==='Aktifkan';Swal.fire({icon:'question',title:on?'Aktifkan supplier?':'Nonaktifkan supplier?',text:on?'Supplier dapat dipilih kembali pada transaksi baru.':'Histori tetap aman, tetapi supplier tidak dapat dipilih pada transaksi baru.',showCancelButton:true,confirmButtonText:on?'Ya, aktifkan':'Ya, nonaktifkan',cancelButtonText:'Batal'}).then(r=>{if(r.isConfirmed)this.submit()})});document.getElementById('statusFilter').onchange=function(){document.querySelectorAll('tbody tr[data-status]').forEach(r=>r.classList.toggle('d-none',this.value&&r.dataset.status!==this.value))}});</script>
+2
View File
@@ -0,0 +1,2 @@
<div class="container-fluid py-3"><h4>Pajak</h4><p class="text-muted">Master tarif configurable, register pajak masukan/keluaran, withholding, periode dan pelaporan.</p><?php foreach(array('success','error')as$f)if($this->session->flashdata($f)):?><div class="alert alert-<?=$f==='success'?'success':'danger'?>"><?=html_escape($this->session->flashdata($f))?></div><?php endif?><div class="row g-3"><div class="col-lg-6"><div class="card h-100"><div class="card-header">Master Kode Pajak</div><div class="card-body"><form method="post" action="<?=base_url('taxes/code')?>"><div class="row g-2"><div class="col-4"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-8"><input class="form-control" name="name" placeholder="Nama" required></div><div class="col-4"><select class="form-select" name="tax_type"><option value="sales">Penjualan</option><option value="purchase">Pembelian</option><option value="withholding">Withholding</option><option value="payroll">Payroll</option></select></div><div class="col-4"><select class="form-select" name="calculation_method"><option value="exclusive">Exclusive</option><option value="inclusive">Inclusive</option></select></div><div class="col-4"><input class="form-control" type="number" step=".000001" name="rate" placeholder="Tarif %"></div><div class="col-6"><input class="form-control" type="date" name="effective_from" value="<?=date('Y-m-01')?>"></div><div class="col-6"><input class="form-control" type="date" name="effective_to"></div><div class="col-12"><input class="form-control" name="formula_expression" placeholder="Formula opsional; kosong = DPP × tarif"></div><div><button class="btn btn-primary">Simpan</button></div></div></form></div></div></div><div class="col-lg-6"><div class="card h-100"><div class="card-header">Periode & Pelaporan</div><div class="card-body"><form method="post" action="<?=base_url('taxes/period')?>"><div class="row g-2"><div class="col-4"><input class="form-control" type="month" name="period" value="<?=$period?>"></div><div class="col-4"><select class="form-select" name="tax_type"><option value="all">Semua</option><option value="sales">Penjualan</option><option value="purchase">Pembelian</option><option value="withholding">Withholding</option><option value="payroll">Payroll</option></select></div><div class="col-4"><select class="form-select" name="status"><option>open</option><option>reviewed</option><option>filed</option><option>closed</option></select></div><div class="col-6"><input class="form-control" name="filing_reference" placeholder="Referensi pelaporan"></div><div class="col-6"><input class="form-control" name="notes" placeholder="Catatan"></div><div><button class="btn btn-warning">Update Periode</button> <a class="btn btn-success" href="<?=base_url('taxes/export/'.$period)?>">Export CSV</a></div></div></form></div></div></div></div>
<div class="card mt-3"><div class="card-header d-flex justify-content-between"><span>Tax Register <?=$period?></span><form><input type="month" name="period" value="<?=$period?>" onchange="this.form.submit()"></form></div><div class="table-responsive"><table class="table table-sm mb-0"><tr><th>Tanggal</th><th>Kode</th><th>Arah</th><th>Sumber</th><th>Faktur</th><th>DPP</th><th>Pajak</th><th>Status</th></tr><?php foreach($register as$r):?><tr><td><?=$r->tax_date?></td><td><?=$r->code?></td><td><?=$r->direction?></td><td><?=$r->source_type?>#<?=$r->source_id?></td><td><?=html_escape($r->tax_invoice_no?:'-')?></td><td class="text-end"><?=number_format($r->tax_base,0,',','.')?></td><td class="text-end"><?=number_format($r->tax_amount,0,',','.')?></td><td><?=$r->status?></td></tr><?php endforeach?></table></div></div></div>
+30 -15
View File
@@ -17,8 +17,9 @@
<th>ID</th>
<th>Username</th>
<th>Nama</th>
<th>Role</th>
<th>Aksi</th>
<th>Role</th>
<th>Karyawan Terkait</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
@@ -53,12 +54,17 @@
<input type="text" class="form-control" id="inputNama">
</div>
<div class="mb-3">
<label class="fw-semibold">Role</label>
<div class="mb-3">
<label class="fw-semibold">Role</label>
<select class="form-control" id="inputRole">
<option value="">-- Pilih Role --</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="fw-semibold">Karyawan Terkait</label>
<select class="form-control" id="inputEmployee"><option value="">-- Tidak ditautkan --</option></select>
<small class="text-muted">Wajib ditautkan untuk role Teknisi agar hanya dapat memproses barang miliknya.</small>
</div>
<div class="password-group">
<div class="mb-3">
@@ -97,8 +103,9 @@ $(document).ready(function () {
{ data: 0 },
{ data: 1 },
{ data: 2 },
{ data: 3 },
{ data: 4, orderable: false }
{ data: 3 },
{ data: 4 },
{ data: 5, orderable: false }
]
});
@@ -128,7 +135,8 @@ $(document).ready(function () {
id: $('#inputId').val(),
username: $('#inputUsername').val(),
nama: $('#inputNama').val(),
role_id: $('#inputRole').val(),
role_id: $('#inputRole').val(),
employee_id: $('#inputEmployee').val(),
password: $('#inputPassword').val()
};
@@ -183,7 +191,8 @@ $(document).ready(function () {
$('#inputUsername').val(res.data.username);
$('#inputNama').val(res.data.nama);
loadRole(res.data.role_id);
loadRole(res.data.role_id);
loadEmployee(res.data.employee_id);
$('.password-group').hide();
$('#inputPassword').val('');
@@ -247,7 +256,7 @@ $(document).ready(function () {
// ======================
// LOAD ROLE (FIXED)
// ======================
function loadRole(selected = '') {
function loadRole(selected = '') {
$.ajax({
url: "<?= base_url('users/get_role'); ?>",
@@ -272,7 +281,11 @@ $(document).ready(function () {
}
});
}
}
function loadEmployee(selected = '') {
$.getJSON("<?=base_url('users/get_employee')?>").done(function(res){const select=$('#inputEmployee').empty().append('<option value="">-- Tidak ditautkan --</option>');(res.data||[]).forEach(function(e){select.append($('<option>',{value:e.id,text:e.employee_code+' - '+e.full_name,selected:String(e.id)===String(selected)}))})});
}
// ======================
// RESET MODAL
@@ -282,10 +295,12 @@ $(document).ready(function () {
$('#inputUsername').val('');
$('#inputNama').val('');
$('#inputPassword').val('');
$('#inputRole').val('');
loadRole();
$('#inputRole').val('');
$('#inputEmployee').val('');
loadRole();
loadEmployee();
}
});
</script>
</script>
+1 -30
View File
@@ -120,34 +120,5 @@ $(function(){
},'json');
});
// DELETE
$(document).on('click','.btn-delete',function(){
let id = $(this).data('id');
Swal.fire({
title:'Hapus gudang?',
text:'Pastikan tidak dipakai item',
icon:'warning',
showCancelButton:true
}).then(r=>{
if(r.isConfirmed){
$.get("<?= base_url('warehouses/delete/'); ?>"+id,function(res){
if(res.status){
table.ajax.reload(null,false);
Swal.fire('Sukses',res.message,'success');
} else {
Swal.fire('Error',res.message,'error');
}
},'json');
}
});
});
});
</script>
</script>