You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
13 KiB
273 lines
13 KiB
@use(App\Domains\Billing\Enums\Invoicing\InvoiceType)
|
|
@use(App\Domains\System\Support\ValueObjects\Money)
|
|
|
|
<div class="row g-3 g-lg-4">
|
|
<!-- Invoice Header & Title Section -->
|
|
<div class="col-12">
|
|
<!-- Main title input card -->
|
|
<div class="card card-body shadow-sm border-0">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="avatar avatar-md bg-primary-subtle text-primary rounded-3 d-flex align-items-center justify-content-center p-2">
|
|
<x-tabler-file-invoice width="24" height="24" />
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<x-form.input
|
|
:label="__('domains/billing/field.invoice.name')"
|
|
wire:model="form.name"
|
|
placeholder="Contoh: Tagihan UKT & Registrasi Ulang 2026/2027"
|
|
class="form-control-lg fw-semibold"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Left Main Column: Customer Details & Invoice Items -->
|
|
<div class="col-12 col-lg-7 col-xl-8 d-flex flex-column gap-3 gap-lg-4">
|
|
<!-- Customer Details Card -->
|
|
<x-card
|
|
:title="__('domains/billing/section.invoice.customer_detail.title')"
|
|
:subtitle="__('domains/billing/section.invoice.customer_detail.subtitle')"
|
|
class="shadow-sm border-0"
|
|
>
|
|
<div class="row g-3">
|
|
@foreach($this->schema as $schema)
|
|
@php($model = $schema->isLive ? 'wire:model.live' : 'wire:model')
|
|
<div class="{{ count($this->schema) > 5 ? 'col-12 col-md-6' : 'col-12' }}">
|
|
<!-- Dynamic schema input component binding -->
|
|
<x-dynamic-component
|
|
:component="$schema->type->component()"
|
|
:attributes="new Illuminate\View\ComponentAttributeBag($schema->attributes)->merge([
|
|
$model => 'form.client_detail.'.$schema->key
|
|
])"
|
|
/>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<!-- Action button for student search if schema forbids manual items -->
|
|
@if($form->type?->schema()->isCannotAddItems())
|
|
<div class="d-flex pt-3 justify-content-end border-top mt-3 border-subtle">
|
|
<x-button
|
|
type="button"
|
|
theme="primary"
|
|
icon="tabler-file-search"
|
|
wire:click="searchStudentAdmission"
|
|
:icon-property="['width' => 20, 'height' => 20]"
|
|
:label="__('domains/billing/button.check_student_admission')"
|
|
class="px-3"
|
|
/>
|
|
</div>
|
|
@endif
|
|
</x-card>
|
|
|
|
<!-- Invoice Items Table Card -->
|
|
<x-card
|
|
:title="__('domains/billing/section.invoice.items.title')"
|
|
:subtitle="__('domains/billing/section.invoice.items.subtitle')"
|
|
class="shadow-sm border-0"
|
|
>
|
|
<x-slot:actions>
|
|
@if($form->type?->schema()->isCanAddItems())
|
|
<!-- Button to add dynamic line items -->
|
|
<x-button
|
|
type="button"
|
|
theme="outline-primary"
|
|
icon-only
|
|
icon="tabler-plus"
|
|
:icon-property="['width' => 20, 'height' => 20]"
|
|
class="btn-sm rounded-circle"
|
|
/>
|
|
@endif
|
|
</x-slot:actions>
|
|
|
|
<div class="table-responsive rounded-3 border border-subtle">
|
|
<table class="table table-hover align-middle mb-0 text-nowrap small">
|
|
<thead class="bg-body-tertiary">
|
|
<tr class="text-uppercase fs-7 fw-semibold text-body-secondary">
|
|
<th class="ps-3 py-3" style="width: 50px;">#</th>
|
|
<th class="py-3">@lang('domains/billing/field.invoice_item.code')</th>
|
|
<th class="py-3">@lang('domains/billing/field.invoice_item.name')</th>
|
|
<th class="text-center py-3">@lang('domains/billing/field.invoice_item.quantity')</th>
|
|
<th class="text-end py-3">@lang('domains/billing/field.invoice_item.gross_price')</th>
|
|
<th class="text-end py-3">@lang('domains/billing/field.invoice_item.discounted_price')</th>
|
|
<th class="text-end pe-3 py-3">@lang('domains/billing/field.invoice_item.net_price')</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($this->form->items ?? [] as $index => $item)
|
|
<tr>
|
|
<td class="ps-3 py-2 fw-semibold text-body-secondary">{{ $index + 1 }}</td>
|
|
<td class="py-2 font-monospace text-body-secondary">
|
|
{{ $item['code'] ?? '-' }}
|
|
</td>
|
|
<td class="py-2 text-body fw-bold">{{ $item['name'] ?? '-' }}</td>
|
|
<td class="text-center py-2 font-monospace fw-semibold">{{ $item['quantity'] ?? 1 }}</td>
|
|
<td class="text-end py-2 font-monospace text-body-secondary">{{ $item['gross_amount'] ?? new Money(0) }}</td>
|
|
<td class="text-end py-2 font-monospace text-danger">{{ $item['discount_amount'] ?? new Money(0) }}</td>
|
|
<td class="text-end pe-3 py-2 font-monospace fw-bold text-success">
|
|
{{ $item['net_amount'] ?? new Money(0) }}
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5 text-body-secondary">
|
|
<!-- Empty state display -->
|
|
<div class="avatar avatar-lg mx-auto mb-2">
|
|
<span class="avatar-initial rounded-circle bg-body-tertiary text-body-secondary">
|
|
<x-tabler-receipt width="28" height="28" />
|
|
</span>
|
|
</div>
|
|
<p class="small mb-0 text-body-secondary fw-medium">{{ __('ui/common.no_data') }}</p>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</x-card>
|
|
</div>
|
|
|
|
<!-- Right Column: Sticky Invoice Summary Side Card -->
|
|
<div class="col-12 col-lg-5 col-xl-4">
|
|
<div class="sticky-top" style="top: 90px; z-index: 10;">
|
|
<x-card
|
|
:title="__('domains/billing/section.invoice.summary.title')"
|
|
:subtitle="__('domains/billing/section.invoice.summary.subtitle')"
|
|
class="shadow-sm border-0 border-top border-3 border-primary"
|
|
>
|
|
<div class="p-3 rounded-3 bg-body-tertiary border border-subtle">
|
|
<!-- Status Indicator Badge -->
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<span class="text-body-secondary fw-bold text-uppercase fs-7 tracking-wider">
|
|
@lang('domains/billing/field.invoice.status')
|
|
</span>
|
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-2 py-1 fw-semibold">
|
|
@lang('domains/billing/enum.invoice_status.draft')
|
|
</span>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column gap-2">
|
|
<!-- Gross Amount -->
|
|
<div class="d-flex align-items-center justify-content-between p-2 rounded bg-body border border-subtle">
|
|
<small class="text-body-secondary fw-semibold fs-7">
|
|
@lang('domains/billing/field.invoice.gross_amount')
|
|
</small>
|
|
<span class="fw-bold text-body font-monospace">
|
|
{{ $form->gross_amount }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Discount Amount -->
|
|
<div class="d-flex align-items-center justify-content-between p-2 rounded bg-body border border-subtle">
|
|
<small class="text-body-secondary fw-semibold fs-7">
|
|
@lang('domains/billing/field.invoice.discount_amount')
|
|
</small>
|
|
<span class="fw-bold text-danger font-monospace">
|
|
-{{ $form->discount_amount }}
|
|
</span>
|
|
</div>
|
|
|
|
<hr class="my-1 border-subtle opacity-50">
|
|
|
|
<!-- Net Amount -->
|
|
<div class="d-flex align-items-center justify-content-between p-3 rounded bg-primary text-white shadow-xs">
|
|
<small class="fw-bold text-uppercase fs-7 opacity-90">
|
|
@lang('domains/billing/field.invoice.net_amount')
|
|
</small>
|
|
<span class="fw-bold font-monospace fs-5">
|
|
{{ $form->net_amount }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="mt-3 d-flex flex-column gap-2">
|
|
<!-- Issue Invoice Button -->
|
|
<x-button
|
|
type="button"
|
|
theme="primary"
|
|
size="md"
|
|
icon="tabler-file-invoice"
|
|
:label="__('domains/billing/button.issue_invoice')"
|
|
wire:click="createInvoice"
|
|
wire:loading.attr="disabled"
|
|
wire:target="createInvoice"
|
|
class="w-100 fw-bold shadow-xs"
|
|
/>
|
|
|
|
<!-- Save Draft Button -->
|
|
<x-button
|
|
type="button"
|
|
theme="secondary"
|
|
size="md"
|
|
icon="tabler-device-floppy"
|
|
:label="__('domains/billing/button.save_invoice_draft')"
|
|
wire:click="saveDraft"
|
|
wire:loading.attr="disabled"
|
|
wire:target="save"
|
|
class="w-100 fw-bold shadow-xs"
|
|
/>
|
|
|
|
<!-- Cancel / Re-select Type Button -->
|
|
<x-button
|
|
type="button"
|
|
theme="outline-secondary"
|
|
size="sm"
|
|
x-on:click="$js.selectInvoiceType"
|
|
:label="__('ui/button.cancel')"
|
|
class="w-100 mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</x-card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@script
|
|
<script>
|
|
// SweetAlert initialization function for invoice type and client selection
|
|
$js.selectInvoiceType = init = () => {
|
|
Swal.fire({
|
|
title: '{{ __('domains/billing/section.invoice.select_invoice_type') }}',
|
|
html: `
|
|
<div class="text-start mb-3">
|
|
<x-form.select x-select2="{dropdownParent: '.swal2-container'}" id="swal-client-id" :label="__('domains/billing/field.invoice.client_name')" :options="$this->clients" />
|
|
</div>
|
|
<div class="text-start">
|
|
<x-form.select x-select2="{dropdownParent: '.swal2-container'}" id="swal-invoice-type" :label="__('domains/billing/field.invoice.type')" :options="InvoiceType::options()" />
|
|
</div>
|
|
`,
|
|
focusConfirm: false,
|
|
showCancelButton: false,
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
confirmButtonText: 'Lanjutkan',
|
|
customClass: {
|
|
confirmButton: 'btn btn-primary px-4',
|
|
popup: 'rounded-4 shadow-lg border-0 bg-body text-body'
|
|
},
|
|
buttonsStyling: false,
|
|
preConfirm: () => {
|
|
const clientId = document.getElementById('swal-client-id').value;
|
|
const invoiceType = document.getElementById('swal-invoice-type').value;
|
|
|
|
if (!clientId) {
|
|
Swal.showValidationMessage('{{ __('validation.required', ['attribute' => __('domains/billing/field.invoice.client_name')]) }}');
|
|
return false;
|
|
}
|
|
if (!invoiceType) {
|
|
Swal.showValidationMessage('{{ __('validation.required', ['attribute' => __('domains/billing/field.invoice.type')]) }}');
|
|
return false;
|
|
}
|
|
|
|
$wire.set('form.type', invoiceType);
|
|
$wire.set('form.client_id', clientId);
|
|
}
|
|
});
|
|
}
|
|
|
|
init()
|
|
</script>
|
|
@endscript
|
|
|