feat: Implement invoice creation functionality and integrate DataTables for invoice listing with dynamic column rendering.
This commit is contained in:
@@ -4,10 +4,13 @@ namespace App\Http\Controllers\admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\InvoiceRequest;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Payment;
|
||||
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
||||
use App\Repositories\Order\OrderRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
@@ -31,6 +34,46 @@ class InvoiceController extends Controller
|
||||
return view('administrator.pages.invoice.index', compact('title', 'invoices'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$title = 'Add new Invoice';
|
||||
$customer = Customer::select('id', 'fullname')->where('status', 1)->get();
|
||||
$payment = Payment::select('id', 'name')->where('status', 1)->get();
|
||||
|
||||
return view('administrator.pages.invoice.add', compact('title', 'customer', 'payment'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'required|numeric',
|
||||
'payment_id' => 'required|numeric',
|
||||
'sub_total' => 'required|numeric',
|
||||
'total' => 'required|numeric',
|
||||
]);
|
||||
|
||||
$invoiceData = [
|
||||
'date' => time(),
|
||||
'due_date' => time() + (7 * 24 * 60 * 60),
|
||||
'sub_total' => $validated['sub_total'],
|
||||
'tax' => $validated['sub_total'] * 0.1,
|
||||
'total' => $validated['total'],
|
||||
'payment_id' => $validated['payment_id'],
|
||||
'customer_id' => $validated['customer_id'],
|
||||
'status' => 0,
|
||||
];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$invoice = $this->invoiceRepository->create($invoiceData);
|
||||
DB::commit();
|
||||
return redirect()->route('administrator.pages.invoice.edit', ['id' => $invoice->id])->with('success', 'Invoice created successfully');
|
||||
} catch (\Exception $error) {
|
||||
DB::rollBack();
|
||||
return back()->withErrors($error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(int $id = 0)
|
||||
{
|
||||
$invoice = $this->invoiceRepository->findWithRelationships($id, ['payment', 'invoiceItem', 'customer']);
|
||||
@@ -96,4 +139,40 @@ class InvoiceController extends Controller
|
||||
|
||||
return redirect()->route('administrator.pages.invoice.index')->with('success', 'Invoice deleted successfully');
|
||||
}
|
||||
|
||||
public function datatable(Request $request)
|
||||
{
|
||||
$query = $this->invoiceRepository->filter(
|
||||
$request->all(),
|
||||
['payment', 'customer'],
|
||||
[],
|
||||
['*'],
|
||||
true
|
||||
);
|
||||
|
||||
return DataTables::eloquent($query)
|
||||
->addColumn('invoice_num', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.invoice_num', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('date', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.date', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('customer', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.customer', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('payment', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.payment', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('total', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.total', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('status', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.status', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('action', function ($d) {
|
||||
return view('administrator.pages.invoice.datatable.action', ['row' => $d])->render();
|
||||
})
|
||||
->rawColumns(['invoice_num', 'date', 'customer', 'payment', 'total', 'status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,12 @@ class InvoiceCachedRepository extends BaseCachedRepository implements InvoiceRep
|
||||
{
|
||||
return $this->invoiceRepository->getModel();
|
||||
}
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'])
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'], bool $returnQuery = false)
|
||||
{
|
||||
if ($returnQuery) {
|
||||
return $this->invoiceRepository->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filterItem' => [
|
||||
'data' => $data,
|
||||
|
||||
@@ -12,10 +12,13 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
|
||||
{
|
||||
return Invoice::class;
|
||||
}
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'])
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'], bool $returnQuery = false)
|
||||
{
|
||||
$model = $this->model->with($relationships)->select($select);
|
||||
|
||||
if (isset($data['client']) && ! empty($data['client'])) {
|
||||
$model = $model->where("customer_id", $data['client']);
|
||||
}
|
||||
if (isset($data['invoice_type'])) {
|
||||
$model = $model->where("invoice_type", $data['invoice_type']);
|
||||
}
|
||||
@@ -86,6 +89,11 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
|
||||
if (count($withCount) > 0) {
|
||||
$model = $model->withCount($withCount);
|
||||
}
|
||||
|
||||
if ($returnQuery) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
if (isset($data['limit']) && $data['limit']) {
|
||||
return $model->latest('id')->take($data['limit'])->get();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Repositories\Invoice;
|
||||
use App\Repositories\BaseRepositoryInterface;
|
||||
|
||||
interface InvoiceRepositoryInterface extends BaseRepositoryInterface{
|
||||
public function filter(array $data = [],array $relationships = [],array $withCount = [],array $select = ['*']);
|
||||
public function filter(array $data = [],array $relationships = [],array $withCount = [],array $select = ['*'], bool $returnQuery = false);
|
||||
public function get_billing_year(int $year = 0);
|
||||
public function get_billing_month(int $month = 0);
|
||||
public function get_billing_day(int|string $day = '');
|
||||
|
||||
+1
-125
@@ -1,125 +1 @@
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-ThinItalic.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-ExtraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-ExtraLightItalic.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-LightItalic.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-SemiBoldItalic.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-BoldItalic.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-ExtraBoldItalic.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Be Vietnam Pro';
|
||||
src: url('/fonts/bevietnampro/BeVietnamPro-BlackItalic.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
@@ -233,7 +233,7 @@ $component-hover-bg: rgba($secondary, .2); // custom variable
|
||||
// Font, line-height, and color for body text, headings, and more.
|
||||
|
||||
// Font family
|
||||
$font-family-sans-serif: "Be Vietnam Pro", Helvetica, sans-serif !default;
|
||||
$font-family-sans-serif: "Inter Tight", Helvetica, sans-serif !default;
|
||||
|
||||
$font-size-base: 0.875rem !default; // 14px
|
||||
$font-size-lg: 1rem !default; // 16px
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
@php
|
||||
$dtColumns = collect($columns)->map(function ($c) {
|
||||
$dtColumns = collect($columns)->map(function ($c, $index) {
|
||||
return [
|
||||
'data' => $c['key'],
|
||||
'name' => $c['key'],
|
||||
'orderable' => $c['orderable'] ?? true,
|
||||
'searchable' => $c['searchable'] ?? true,
|
||||
'visible' => $c['visible'] ?? true,
|
||||
];
|
||||
})->values();
|
||||
@endphp
|
||||
|
||||
<div class="d-flex justify-content-end align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#{{ $id }}-column-offcanvas">
|
||||
<i class="ti ti-columns"></i> Columns
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="{{ $id }}" class="table table-striped w-100">
|
||||
<thead>
|
||||
@@ -21,15 +28,41 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="{{ $id }}-column-offcanvas">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">Column Visibility</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
@foreach($columns as $index => $col)
|
||||
@if((!isset($col['visibility']) || $col['visibility'] !== false) && $col['key'] !== 'checkbox')
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input type="checkbox" class="form-check-input column-toggle"
|
||||
data-column-index="{{ $index }}"
|
||||
id="{{ $id }}-col-{{ $index }}"
|
||||
{{ !isset($col['visible']) || $col['visible'] !== false ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="{{ $id }}-col-{{ $index }}">{!! $col['label'] !!}</label>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('custom-scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
$('#{{ $id }}').DataTable({
|
||||
var table = $('#{{ $id }}').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: '{{ $ajax }}',
|
||||
columns: @json($dtColumns),
|
||||
});
|
||||
|
||||
$('#{{ $id }}-column-offcanvas').on('change', '.column-toggle', function () {
|
||||
var columnIdx = $(this).data('column-index');
|
||||
var column = table.column(columnIdx);
|
||||
column.visible($(this).is(':checked'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
@extends('administrator.layout.master')
|
||||
|
||||
@section('breadcrumb')
|
||||
@php
|
||||
$breadcrumbItems = [
|
||||
['label' => 'Invoice'],
|
||||
['label' => 'Invoice Management', 'url' => route('administrator.pages.invoice.index')],
|
||||
['label' => 'Add New Invoice', 'active' => true],
|
||||
];
|
||||
@endphp
|
||||
<x-admin-breadcrumb :items="$breadcrumbItems" />
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(auth()->guard('admins')->user()->can("add invoice"))
|
||||
<form action="{{ route('administrator.pages.invoice.store') }}" method="post">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card card-outline card-primary rounded-4 shadow-none">
|
||||
<div class="card-header bg-white rounded-top-4">
|
||||
<h5 class="card-title m-0">Invoice Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group mb-3">
|
||||
<label for="customer_id">Client User</label>
|
||||
<select class="form-control select2" name="customer_id" id="customer_id" data-placeholder="Select Customer">
|
||||
<option value=""></option>
|
||||
@if($customer->count())
|
||||
@foreach($customer->all() as $item)
|
||||
<option value="{{ $item->id }}">
|
||||
{{ $item->firstname . ' ' . $item->lastname }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="payment_id">Payment Method</label>
|
||||
<select class="form-control select2" name="payment_id" id="payment_id" data-placeholder="Select Payment">
|
||||
<option value=""></option>
|
||||
@if($payment->count())
|
||||
@foreach($payment->all() as $item)
|
||||
<option value="{{ $item->id }}">{{ $item->name }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="sub_total">Sub Total</label>
|
||||
<input type="number" class="form-control" name="sub_total" id="sub_total" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="total">Total</label>
|
||||
<input type="number" class="form-control" name="total" id="total" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" name="description" id="description" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card card-outline card-primary rounded-4 shadow-none">
|
||||
<div class="card-header bg-white rounded-top-4">
|
||||
<h5 class="card-title m-0">Invoice Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Sub Total</td>
|
||||
<td class="text-end" id="summary_sub_total">0đ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAT (10%)</td>
|
||||
<td class="text-end" id="summary_tax">0đ</td>
|
||||
</tr>
|
||||
<tr class="border-top">
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-end" id="summary_total"><strong>0đ</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-fixed-action-bar>
|
||||
<x-button-crm type="button" variant="secondary" size="sm" icon="arrow-left"
|
||||
route="administrator.pages.invoice.index">Cancel</x-button-crm>
|
||||
<x-button-crm type="submit" variant="primary" size="sm" icon="check-circle">Create Invoice</x-button-crm>
|
||||
</x-fixed-action-bar>
|
||||
</form>
|
||||
@else
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<h5 class="mb-0"><i class="icon fas fa-ban"></i> Access Denied - You are not assigned to any Invoice
|
||||
management department</h5>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('custom-scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const subTotalInput = document.getElementById('sub_total');
|
||||
const totalInput = document.getElementById('total');
|
||||
const summarySubTotal = document.getElementById('summary_sub_total');
|
||||
const summaryTax = document.getElementById('summary_tax');
|
||||
const summaryTotal = document.getElementById('summary_total');
|
||||
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('vi-VN').format(amount) + 'đ';
|
||||
}
|
||||
|
||||
function calculateTotal() {
|
||||
const subTotal = parseFloat(subTotalInput.value) || 0;
|
||||
const tax = subTotal * 0.1;
|
||||
const total = subTotal + tax;
|
||||
|
||||
summarySubTotal.textContent = formatCurrency(subTotal);
|
||||
summaryTax.textContent = formatCurrency(tax);
|
||||
summaryTotal.innerHTML = '<strong>' + formatCurrency(total) + '</strong>';
|
||||
totalInput.value = total;
|
||||
}
|
||||
|
||||
subTotalInput.addEventListener('input', calculateTotal);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="dt-action-dropdown dropdown">
|
||||
<button class="btn btn-sm btn-light d-flex align-items-center gap-1" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i data-lucide="more-vertical" width="14" height="14"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('administrator.pages.invoice.edit', ['id' => $row->id]) }}">
|
||||
<i data-lucide="pencil" width="14" height="14" class="me-2"></i>
|
||||
View
|
||||
</a>
|
||||
</li>
|
||||
@if(auth()->guard('admins')->user()->can("delete invoice"))
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
<form method="post" class="d-inline" action="{{ route('administrator.pages.invoice.destroy', ['id' => $row->id]) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i data-lucide="trash-2" width="14" height="14" class="me-2 text-danger"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{{ $row->customer->firstname . ' ' . $row->customer->lastname }}
|
||||
@@ -0,0 +1 @@
|
||||
@time($row->date)
|
||||
@@ -0,0 +1,3 @@
|
||||
<a href="{{ route('administrator.pages.invoice.edit', ['id' => $row->id]) }}" title="View Invoice">
|
||||
{{ $row->id }}
|
||||
</a>
|
||||
@@ -0,0 +1 @@
|
||||
{{ $row->payment->name ?? 'VN Pay' }}
|
||||
@@ -0,0 +1 @@
|
||||
{!! $row->getStatus() !!}
|
||||
@@ -0,0 +1 @@
|
||||
{{ number_format($row->total, 0, ',', '.') }}đ
|
||||
@@ -1,184 +1,42 @@
|
||||
@extends("administrator.layout.master")
|
||||
@section("content")
|
||||
@extends('administrator.layout.master')
|
||||
@push('plugin-styles')
|
||||
@foreach (config('assets.datatable.css') as $datatable)
|
||||
<link href="{{ asset($datatable) }}" rel="stylesheet" />
|
||||
@endforeach
|
||||
@endpush
|
||||
|
||||
@section('breadcrumb')
|
||||
@php
|
||||
$breadcrumbItems = [
|
||||
['label' => 'Invoice'],
|
||||
['label' => 'Invoice Management'],
|
||||
['label' => 'Invoice List', 'active' => true],
|
||||
];
|
||||
@endphp
|
||||
<x-admin-breadcrumb :items="$breadcrumbItems" />
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(auth()->guard('admins')->user()->can("manager invoice"))
|
||||
<div class="card card-primary card-outline">
|
||||
<div class="card-header">
|
||||
<form action="{{ route('administrator.pages.invoice.index') }}" method="get">
|
||||
<div class="row form-group mb-0">
|
||||
<div class="col-md-3">
|
||||
{{-- Customer --}}
|
||||
<x-form.customer :model="request()->get('client') ?? null" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="invoice_id">
|
||||
Invoice ID </label>
|
||||
<input type="number" name="invoice_id" id="invoice_id" class="form-control"
|
||||
value="{{ request()->invoice_id ?? "" }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{-- Payment --}}
|
||||
<x-form.payment :model="request()->get('payment_method') ?? null" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="inputStatus">
|
||||
Status</label>
|
||||
<select name="status" class="form-control" id="inputStatus">
|
||||
<option value="full"
|
||||
@selected(request()->status === "full")>
|
||||
Any
|
||||
</option>
|
||||
<option value="active"
|
||||
@selected(request()->status === "active")>
|
||||
Active
|
||||
</option>
|
||||
<option value="unpaid"
|
||||
@selected(request()->status === "unpaid")>
|
||||
Pending
|
||||
</option>
|
||||
<option value="cancel"
|
||||
@selected(request()->status === "cancel")>
|
||||
Cancel
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="date">Invoice Date:</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="far fa-calendar-alt"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control reservation float-right"
|
||||
name="date"
|
||||
value="{{ request()->date ?? "" }}"
|
||||
id="date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="due_date">Due Date:</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="far fa-calendar-alt"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control reservation float-right"
|
||||
name="due_date"
|
||||
value="{{ request()->due_date ?? "" }}"
|
||||
id="due_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="date_paid">Date Paid:</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="far fa-calendar-alt"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control reservation float-right"
|
||||
name="date_paid"
|
||||
value="{{ request()->date_paid ?? "" }}"
|
||||
id="date_paid">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="clear-search hidden">
|
||||
|
||||
</label>
|
||||
<button type="submit" class="form-control btn btn-block bg-gradient-primary">
|
||||
<i class="fas fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<span>{{ $invoices->count() }} Records Found</span>
|
||||
<div class="card card-primary card-outline rounded-4 shadow-none">
|
||||
<div class="card-header bg-white d-flex justify-content-end gap-2 rounded-top-4">
|
||||
<x-button-c-r-m route="administrator.pages.invoice.create" variant="primary" size="sm" icon="plus">
|
||||
Add New Invoice
|
||||
</x-button-c-r-m>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-invoice table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10px">
|
||||
<label for=""></label><input type="checkbox" name="check" class="check-all" id="">
|
||||
</th>
|
||||
<th>Invoice #</th>
|
||||
<th>Client Name</th>
|
||||
<th>Date</th>
|
||||
<th>Payment Method</th>
|
||||
<th>Total</th>
|
||||
<th>Status</th>
|
||||
<th style="width: 40px"></th>
|
||||
<th style="width: 40px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($invoices->count())
|
||||
@foreach($invoices->all() as $item)
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" name="id[]" id="" value="{{ $item->id }}">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ route("administrator.pages.invoice.edit",['id' => $item->id]) }}"
|
||||
title="View Order"> {{ $item->id }}</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@isset($item->customer->firstname)
|
||||
{{ $item->customer->firstname . ' ' . $item->customer->lastname }}
|
||||
@endisset
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@time($item->date)
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ $item->payment->name ?? 'VN Pay' }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ number_format($item->total,0,',','.') }}đ
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{!! $item->getStatus() !!}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a class="btn btn-warning btn-sm"
|
||||
href="{{ route('administrator.pages.invoice.edit',['id' => $item->id ]) }}">
|
||||
<i class="fas fa-search"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if(auth()->guard('admins')->user()->can("delete invoice"))
|
||||
<form method="post" class="d-inline"
|
||||
action="{{ route('administrator.pages.invoice.destroy',['id' => $item->id]) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button type="submit" data-toggle="tooltip" data-placement="top" title=""
|
||||
class="btn btn-sm btn-danger" data-original-title="Xoá">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $invoices->withQueryString()->links() }}
|
||||
<div class="card-body p-0">
|
||||
@php
|
||||
$invoiceColumns = [
|
||||
['key' => 'invoice_num', 'label' => 'Invoice #'],
|
||||
['key' => 'date', 'label' => 'Date'],
|
||||
['key' => 'customer', 'label' => 'Client Name'],
|
||||
['key' => 'payment', 'label' => 'Payment Method'],
|
||||
['key' => 'total', 'label' => 'Total'],
|
||||
['key' => 'status', 'label' => 'Status'],
|
||||
['key' => 'action', 'label' => 'Action'],
|
||||
];
|
||||
@endphp
|
||||
<x-datatable id="invoiceTable" ajax="{{ route('administrator.pages.invoice.datatable') }}" :columns="$invoiceColumns" />
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@@ -188,3 +46,8 @@
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@push('plugin-scripts')
|
||||
@foreach (config('assets.datatable.js') as $datatable)
|
||||
<script src="{{ asset($datatable) }}"></script>
|
||||
@endforeach
|
||||
@endpush
|
||||
|
||||
@@ -2,32 +2,34 @@
|
||||
|
||||
@php
|
||||
$dtColumns = collect($columns)
|
||||
->map(function ($c) {
|
||||
->map(function ($c, $index) {
|
||||
return [
|
||||
'data' => $c['key'],
|
||||
'name' => $c['key'],
|
||||
'orderable' => $c['orderable'] ?? true,
|
||||
'searchable' => $c['searchable'] ?? true,
|
||||
'className' => $c['className'] ?? '',
|
||||
'visible' => $c['visible'] ?? true,
|
||||
];
|
||||
})
|
||||
->values();
|
||||
@endphp
|
||||
|
||||
@if ($bulkDeleteUrl)
|
||||
<div class="d-flex justify-content-start align-items-center mb-3" id="{{ $id }}-bulk-actions"
|
||||
style="display: none !important;">
|
||||
<div class="bg-danger-subtle text-danger rounded-3 d-flex align-items-center gap-3 px-4 py-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i data-lucide="alert-circle" width="16" height="16"></i>
|
||||
<span class="fw-medium"><span id="{{ $id }}-selected-count">0</span> items selected</span>
|
||||
</div>
|
||||
<div class="vr bg-danger opacity-25"></div>
|
||||
<x-button-c-r-m type="button" variant="danger" size="sm" icon="trash-2" id="{{ $id }}-bulk-delete-opener-btn"
|
||||
class="shadow-sm">
|
||||
Delete Selected
|
||||
</x-button-c-r-m>
|
||||
<div class="d-flex justify-content-start align-items-center mb-3" id="{{ $id }}-bulk-actions"
|
||||
style="display: none !important;">
|
||||
<div class="bg-danger-subtle text-danger rounded-3 d-flex align-items-center gap-3 px-4 py-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i data-lucide="alert-circle" width="16" height="16"></i>
|
||||
<span class="fw-medium"><span id="{{ $id }}-selected-count">0</span> items selected</span>
|
||||
</div>
|
||||
<div class="vr bg-danger opacity-25"></div>
|
||||
<x-button-c-r-m type="button" variant="danger" size="sm" icon="trash-2" id="{{ $id }}-bulk-delete-opener-btn"
|
||||
class="shadow-sm">
|
||||
Delete Selected
|
||||
</x-button-c-r-m>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="{{ $id }}-bulk-delete-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
@@ -56,6 +58,7 @@ $dtColumns = collect($columns)
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="data-table-wrapper position-relative shadow-none">
|
||||
<div class="loading-process" id="{{ $id }}-loading" style="display: none;">
|
||||
<div class="loading-bar"></div>
|
||||
@@ -110,6 +113,27 @@ $dtColumns = collect($columns)
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="{{ $id }}-column-offcanvas">
|
||||
<div class="offcanvas-header border-bottom">
|
||||
<h5 class="offcanvas-title">Column Visibility</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
@foreach($columns as $index => $col)
|
||||
@if((!isset($col['visibility']) || $col['visibility'] !== false) && $col['key'] !== 'checkbox')
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input type="checkbox" class="form-check-input column-toggle"
|
||||
data-column-index="{{ $index }}"
|
||||
id="{{ $id }}-col-{{ $index }}"
|
||||
{{ !isset($col['visible']) || $col['visible'] !== false ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="{{ $id }}-col-{{ $index }}">{{ $col['label'] }}</label>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('custom-scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
@@ -139,7 +163,7 @@ $dtColumns = collect($columns)
|
||||
zeroRecords: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="text-secondary mb-3"><i data-lucide="inbox" width="28" height="28" stroke-width="1.5"></i></div><h6 class="text-secondary fw-medium">No matching records found</h6></div>',
|
||||
emptyTable: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="text-secondary mb-3"><i data-lucide="inbox" width="28" height="28" stroke-width="1.5"></i></div><h6 class="text-secondary fw-medium">No data available</h6></div>'
|
||||
},
|
||||
dom: "<'row px-3 py-2 align-items-center'<'col-sm-12 col-md-6 d-inline-flex align-items-center gap-2'<'dt-filter-toggle'>f><'col-sm-12 col-md-6 d-inline-flex align-items-center justify-content-end gap-2'l>>" +
|
||||
dom: "<'row px-3 py-2 align-items-center'<'col-sm-12 col-md-6 d-inline-flex align-items-center gap-2'<'dt-filter-toggle'><'dt-column-toggle'>f><'col-sm-12 col-md-6 d-inline-flex align-items-center justify-content-end gap-2'l>>" +
|
||||
"<'row p-0'<'col-sm-12'tr>>" +
|
||||
"<'row px-3 pb-3 pt-3 align-items-center'<'col-sm-12 col-md-5 text-muted small d-inline-flex align-items-center'i><'col-sm-12 col-md-7 d-inline-flex align-items-center justify-content-end gap-2'p>>",
|
||||
initComplete: function () {
|
||||
@@ -152,6 +176,13 @@ $dtColumns = collect($columns)
|
||||
'</button>'
|
||||
);
|
||||
|
||||
wrapper.find('.dt-column-toggle').html(
|
||||
'<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1" data-bs-toggle="offcanvas" data-bs-target="#{{ $id }}-column-offcanvas">' +
|
||||
'<i data-lucide="columns" width="14" height="14"></i>' +
|
||||
'<span>Columns</span>' +
|
||||
'</button>'
|
||||
);
|
||||
|
||||
$('#{{ $id }}-filter-toggle-btn').on('click', function () {
|
||||
var filterRow = $('#{{ $id }}-filter-row');
|
||||
var btn = $(this);
|
||||
@@ -207,6 +238,12 @@ $dtColumns = collect($columns)
|
||||
}
|
||||
});
|
||||
|
||||
$('#{{ $id }}-column-offcanvas').on('change', '.column-toggle', function () {
|
||||
var columnIdx = $(this).data('column-index');
|
||||
var column = table.column(columnIdx);
|
||||
column.visible($(this).is(':checked'));
|
||||
});
|
||||
|
||||
$('#{{ $id }} tbody').on('dblclick', 'tr', function () {
|
||||
const editLink = $(this).find('a.btn-outline-primary, a[href*="edit"]').first();
|
||||
if (editLink.length && editLink.attr('href')) {
|
||||
|
||||
@@ -473,6 +473,9 @@ Route::prefix('')->group(function () {
|
||||
//Invoice
|
||||
Route::controller(InvoiceController::class)->group(function () {
|
||||
Route::get('list-all-invoice', 'index')->name('administrator.pages.invoice.index');
|
||||
Route::get('add-new-invoice', 'create')->name('administrator.pages.invoice.create');
|
||||
Route::post('add-new-invoice', 'store')->name('administrator.pages.invoice.store');
|
||||
Route::get('invoice-datatable', 'datatable')->name('administrator.pages.invoice.datatable');
|
||||
Route::get('view-invoice-item/{id}', 'edit')->name('administrator.pages.invoice.edit');
|
||||
Route::put('view-invoice-item/{id}', 'update')->name('administrator.pages.invoice.update');
|
||||
Route::delete('remove-invoice/{id}', 'destroy')->name('administrator.pages.invoice.destroy');
|
||||
|
||||
Reference in New Issue
Block a user