First invoice implementation.
This commit is contained in:
70
app/Http/Controllers/Api/InvoiceController.php
Normal file
70
app/Http/Controllers/Api/InvoiceController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index($from = null, $end = null): JsonResponse
|
||||
{
|
||||
$from = $from . ' 00:00:00';
|
||||
$end = $end . ' 23:59:59';
|
||||
return response()->json(Invoice::whereBetween('created_at', [$from, $end])->with('address')->orderBy('created_at', 'desc')->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$invoiceData = $request->validate([
|
||||
'customer_id' => 'required|integer|exists:customers,id',
|
||||
'address_id' => 'required|integer|exists:addresses,id',
|
||||
'delivery_id' => 'nullable|integer|exists:addresses,id',
|
||||
'tax' => 'required|numeric',
|
||||
'sum' => 'required|numeric',
|
||||
]);
|
||||
$invoiceData['user_id'] = auth()->id();
|
||||
$invoiceData['type'] = '380';
|
||||
$invoiceData['status'] = 'created';
|
||||
$invoiceData['invoice_number'] = Invoice::whereYear('created_at', now()->year)->count() + 1;
|
||||
|
||||
|
||||
|
||||
$invoice = new Invoice($invoiceData);
|
||||
$invoice->save();
|
||||
|
||||
return response()->json($invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Invoice $invoice)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Invoice $invoice)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Invoice $invoice)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
64
app/Http/Controllers/Api/InvoiceitemController.php
Normal file
64
app/Http/Controllers/Api/InvoiceitemController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Invoiceitem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceitemController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Invoice $invoice): JsonResponse
|
||||
{
|
||||
return response()->json($invoice->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request, Invoice $invoice): JsonResponse
|
||||
{
|
||||
$itemData = $request->validate([
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'discount' => 'numeric|nullable',
|
||||
'tax' => 'required|numeric|min:0',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'total' => 'required|numeric|min:0',
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
|
||||
]);
|
||||
|
||||
$item = $invoice->items()->create($itemData);
|
||||
return response()->json($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Invoiceitem $invoiceitem)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Invoiceitem $invoiceitem)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Invoiceitem $invoiceitem)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
41
app/Http/Controllers/InvoiceController.php
Normal file
41
app/Http/Controllers/InvoiceController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('invoice.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('invoice.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Invoice $invoice)
|
||||
{
|
||||
return view('invoice.show', ['invoice' => $invoice]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Invoice $invoice)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,77 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
//
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'customer_id',
|
||||
'address_id',
|
||||
'delivery_id',
|
||||
'invoice_number',
|
||||
'type',
|
||||
'status',
|
||||
'sum',
|
||||
'tax',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are appended with attribute getters.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $appends = [
|
||||
'created',
|
||||
'number'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the invoice number formatted
|
||||
*/
|
||||
public function getNumberAttribute(): string
|
||||
{
|
||||
return $this->created_at->format('Y') . '-' . str_pad($this->invoice_number, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created_at attribute in local time format.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCreatedAttribute(): string
|
||||
{
|
||||
return $this->created_at->format('d.m.Y');
|
||||
}
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function address(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Address::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function delivery(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Address::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(Invoiceitem::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,20 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Invoiceitem extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'amount',
|
||||
'discount',
|
||||
'tax',
|
||||
'price',
|
||||
'total',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
37
lang/de/invoice.php
Normal file
37
lang/de/invoice.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|------------------------------------------------- -------------------------
|
||||
| Übersetzungen von Daten die Rechnungen verwenden
|
||||
|------------------------------------------------- -------------------------
|
||||
|
|
||||
| Die folgenden Sprachzeilen werden für alles verwendet, das zum Bereich
|
||||
| Rechnungen gehört.
|
||||
|
|
||||
*/
|
||||
|
||||
'Invoices' => 'Rechnungen',
|
||||
'Invoice' => 'Rechnung',
|
||||
'Add new invoice' => 'Neue Rechnung erstellen',
|
||||
'Add new invoice by clicking add' => 'Durch Klick auf "Anlegen" neue Rechnung erstellen',
|
||||
'Existing invoices' => 'Bestehende Rechnungen',
|
||||
'Create new invoice' => 'Neue Rechnung anlegen',
|
||||
'Select customer' => 'Kunde auswählen',
|
||||
'Select your customer and address' => 'Wähle einen Kunden und seine Adresse aus',
|
||||
'Invoice item' => 'Rechnungsposition',
|
||||
'Invoice items' => 'Rechnungspositionen',
|
||||
'Amount' => 'Menge',
|
||||
'Name' => 'Bezeichnung',
|
||||
'Price' => 'Preis',
|
||||
'Tax' => 'MwSt.',
|
||||
'Description' => 'Beschreibung',
|
||||
'State' => 'Status',
|
||||
'Invoice Number' => 'Rechnungsnummer',
|
||||
'Sum' => 'Summe',
|
||||
'Net' => 'Nettobetrag',
|
||||
'From' => 'Von',
|
||||
'End' => 'Bis',
|
||||
'Enter your invoice items. Click add for an additional invoice item.' => 'Gib Deine Rechnungspositionen ein. Klicke auf "Hinzufügen" für weitere Rechnungspositionen.',
|
||||
];
|
||||
10
resources/views/components/delivery-card.blade.php
Normal file
10
resources/views/components/delivery-card.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<div x-show="delivery.is_delivery"
|
||||
class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('customer.Delivery Address') }}</div>
|
||||
<div x-text="delivery.name"></div>
|
||||
<div x-text="delivery.address"></div>
|
||||
<div class="flex flex-row">
|
||||
<div class="mr-2" x-text="delivery.zip"></div>
|
||||
<div x-text="delivery.city"></div>
|
||||
</div>
|
||||
<div x-text="delivery.phone"></div>
|
||||
<div x-text="delivery.email"></div>
|
||||
15
resources/views/components/invoice-item.blade.php
Normal file
15
resources/views/components/invoice-item.blade.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="mt-1 block w-1/12">{{ $item->amount }}</div>
|
||||
<div class="mt-1 block w-2/3">{{ $item->name }}</div>
|
||||
<div class="mt-1 block w-1/12 text-right">{{ \Illuminate\Support\Number::currency($item->price) }}</div>
|
||||
<div class="mt-1 block w-1/12 text-right">{{ \Illuminate\Support\Number::percentage($item->tax) }}</div>
|
||||
<div class="mt-1 block w-1/12 text-right">{{ \Illuminate\Support\Number::currency($item->total) }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<div class="w-2/3">{!! nl2br($item->description) !!}</div>
|
||||
<div class="w-1/12"></div>
|
||||
<div class="w-1/12"></div>
|
||||
<div class="w-1/12"></div>
|
||||
</div>
|
||||
|
||||
11
resources/views/components/php-card.blade.php
Normal file
11
resources/views/components/php-card.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
@props(['address' => []])
|
||||
|
||||
<div>{{ $address->name }}</div>
|
||||
<div>{{ $address->address }}</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="mr-2">{{ $address->zip }}</div>
|
||||
<div>{{ $address->city }}</div>
|
||||
</div>
|
||||
<div>{{ $address->phone }}</div>
|
||||
<div>{{ $address->email }}</div>
|
||||
|
||||
391
resources/views/invoice/create.blade.php
Normal file
391
resources/views/invoice/create.blade.php
Normal file
@@ -0,0 +1,391 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('invoice.Create new invoice') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12" x-data="invoiceForm()">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
<!-- Customer data -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"
|
||||
x-show="customer_id == 0 || step == 1">
|
||||
<div class="max-w">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Select customer') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("invoice.Select your customer and address") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="my-8">
|
||||
<x-input-label for="search" :value="__('common.Search')"/>
|
||||
<x-text-input id="search" name="search" type="search" class="mt-1 block w-full"
|
||||
x-ref="searchInput"
|
||||
autofocus
|
||||
placeholder="{{ __('invoice.Search customer') }}"
|
||||
x-on:keydown.window.prevent.slash="$refs.searchInput.focus()"
|
||||
x-model="search"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<template x-for="(customer, index) in getFilteredCustomer()">
|
||||
<div class="cursor-pointer grid grid-cols-4 even:bg-gray-100 odd:bg-white"
|
||||
x-on:click="getAddress(index);">
|
||||
<div x-text="customer.name"></div>
|
||||
<div x-text="customer.email"></div>
|
||||
<div x-text="(customer.address) ? customer.address.address : ''"></div>
|
||||
<div x-text="(customer.address) ? customer.address.city : ''"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address data -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"
|
||||
x-show="customer_id != 0 || step == 2">
|
||||
<div class="max-w">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Select address') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("invoice.Select your customer and address") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-row my-8">
|
||||
<div x-show="address_id != 0" class="w-1/2">
|
||||
<x-address-card/>
|
||||
</div>
|
||||
|
||||
<div x-show="delivery_id != 0" class="w-1/2">
|
||||
<x-delivery-card x-data="{card: {name: 'Chris'}}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice data -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Invoice items') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("invoice.Enter your invoice items. Click add for an additional invoice item.") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- New invoice item -->
|
||||
<div class="flex flex-row items-end gap-2 w-full mt-4">
|
||||
<x-input-label for="invoice_item.amount" :value="__('invoice.Amount')" class="w-1/12"/>
|
||||
<x-input-label for="invoice_item.name" :value="__('invoice.Name')" class="w-2/3"/>
|
||||
<x-input-label for="invoice_item.price" :value="__('invoice.Price')" class="w-1/12"/>
|
||||
<x-input-label for="invoice_item.tax" :value="__('invoice.Tax')" class="w-1/12"/>
|
||||
<div class="w-1/12 relative h-10"></div>
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<x-text-input id="invoice_item.amount" name="invoice_item.amount" type="number"
|
||||
class="mt-1 block w-1/12"
|
||||
autofocus
|
||||
x-model="invoice_item.amount"/>
|
||||
|
||||
<x-text-input id="invoice_item.name" name="invoice_item.name" type="text"
|
||||
class="mt-1 block w-2/3"
|
||||
autofocus
|
||||
placeholder="{{ __('invoice.Name') }}"
|
||||
x-model="invoice_item.name"/>
|
||||
|
||||
<x-text-input id="invoice_item.price" name="invoice_item.price" type="number"
|
||||
class="mt-1 block w-1/12"
|
||||
autofocus
|
||||
x-model="invoice_item.price"/>
|
||||
|
||||
<select name="invoice_item.tax" x-model="invoice_item.tax" id="invoice_item.tax"
|
||||
class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm w-1/12">
|
||||
<template x-for="tax in tax_rates">
|
||||
<option x-bind:value="tax.rate" x-text="tax.rate_percentage"
|
||||
:selected="(invoice_item.tax == tax.rate) ? true : tax.active"></option>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div class="w-1/12 relative h-10">
|
||||
<x-primary-button x-on:click="addItem();" class="absolute right-0">+</x-primary-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<textarea placeholder="{{ __('invoice.Description') }}" name="invoice_item.description"
|
||||
x-model="invoice_item.description" x-text="invoice_item.description"
|
||||
class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm mt-1 block w-2/3 offset-1"></textarea>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Existing invoice items -->
|
||||
<div x-sort="handle" x-bind="items" class="mt-4">
|
||||
<template x-for="(item, index) in items">
|
||||
<div x-sort:item="index">
|
||||
<div class="flex flex-row items-end gap-2 w-full relative">
|
||||
<x-text-input id="items[index].amount" name="items[index].amount" type="number"
|
||||
class="mt-1 block w-1/12"
|
||||
autofocus
|
||||
x-model="items[index].amount"/>
|
||||
|
||||
<x-text-input id="items[index].name" name="items[index].name" type="text"
|
||||
class="mt-1 block w-2/3"
|
||||
autofocus
|
||||
placeholder="{{ __('invoice.Name') }}"
|
||||
x-model="items[index].name"/>
|
||||
|
||||
<x-text-input id="items[index].price" name="items[index].price" type="text"
|
||||
class="mt-1 block w-1/12"
|
||||
autofocus
|
||||
x-model="items[index].price"/>
|
||||
|
||||
<select name="items[index].tax" x-model="items[index].tax"
|
||||
class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm w-1/12">
|
||||
<template x-for="tax in tax_rates">
|
||||
<option x-bind:value="tax.rate" x-text="tax.rate_percentage"
|
||||
:selected="items[index].tax == tax.rate"></option>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div class="flex flex-row w-1/12 h-10 relative">
|
||||
<x-sort-icon x-sort:handle class="cursor-move"/>
|
||||
<x-danger-button x-on:click="deleteItem(index);" class="absolute right-0">
|
||||
-
|
||||
</x-danger-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<textarea placeholder="{{ __('invoice.Description') }}"
|
||||
name="items[index].description" x-model="items[index].description"
|
||||
x-text="items[index].description"
|
||||
class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm mt-1 block w-2/3"></textarea>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
<div class="w-1/12 h-10"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<x-primary-button x-on:click="submitForm();" class="">Sprrelkfdsiodsaich</x-primary-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
||||
|
||||
<script>
|
||||
let self;
|
||||
|
||||
function invoiceForm() {
|
||||
return {
|
||||
step: 1,
|
||||
|
||||
card: {},
|
||||
customers: {},
|
||||
customer: {},
|
||||
customer_id: 0,
|
||||
addresses: {},
|
||||
address_id: 0,
|
||||
address: {},
|
||||
delivery_id: null,
|
||||
delivery: {},
|
||||
tax_rates: {},
|
||||
tax_standard: 0,
|
||||
|
||||
invoice_item: {},
|
||||
|
||||
items: [],
|
||||
sort: [],
|
||||
|
||||
selection: '',
|
||||
|
||||
search: '',
|
||||
|
||||
error: false,
|
||||
message: '',
|
||||
|
||||
init() {
|
||||
// this.items.push(this.newItem());
|
||||
this.getCustomers();
|
||||
this.getTaxRates();
|
||||
self = this;
|
||||
},
|
||||
|
||||
newItem() {
|
||||
return {
|
||||
name: '',
|
||||
description: '',
|
||||
amount: 1,
|
||||
discount: 0,
|
||||
tax: this.tax_standard,
|
||||
price: 0,
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
|
||||
getCustomers() {
|
||||
let vm = this;
|
||||
|
||||
axios.get('/customer')
|
||||
.then(function (response) {
|
||||
vm.customers = response.data;
|
||||
})
|
||||
.catch(function (error) {
|
||||
vm.error = true;
|
||||
vm.message = error.response.data.message;
|
||||
})
|
||||
},
|
||||
|
||||
getTaxRates() {
|
||||
let vm = this;
|
||||
|
||||
axios.get('/taxrate')
|
||||
.then(function (response) {
|
||||
vm.tax_rates = response.data;
|
||||
let test = vm.tax_rates.find(function (tax) {
|
||||
return tax.active;
|
||||
});
|
||||
vm.tax_standard = test.rate;
|
||||
vm.invoice_item = vm.newItem();
|
||||
})
|
||||
},
|
||||
|
||||
getFilteredCustomer() {
|
||||
if (this.search === '') {
|
||||
return this.customers;
|
||||
}
|
||||
return this.customers.filter((customer) => {
|
||||
return customer.name
|
||||
.replace(/ /g, '')
|
||||
.toLowerCase()
|
||||
.includes(this.search.replace(/ /g, '').toLowerCase());
|
||||
})
|
||||
},
|
||||
|
||||
getAddress(index) {
|
||||
this.customer = this.customers[index];
|
||||
this.customer_id = this.customer.id;
|
||||
this.step = 2;
|
||||
if (this.customer.address) {
|
||||
this.address_id = this.customer.address.id;
|
||||
this.address = this.customer.address;
|
||||
}
|
||||
if (this.customer.delivery) {
|
||||
this.delivery_id = this.customer.delivery.id;
|
||||
this.delivery = this.customer.delivery;
|
||||
}
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.items.push(this.invoice_item);
|
||||
this.sort.push(this.items.length - 1);
|
||||
this.invoice_item = this.newItem();
|
||||
},
|
||||
|
||||
deleteItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
let position = this.sort[index];
|
||||
this.sort.splice(index, 1);
|
||||
for (let i = 0; i < this.sort.length; i++) {
|
||||
if (this.sort[i] > position) {
|
||||
this.sort[i] = --this.sort[i];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
let sum = 0;
|
||||
let tax = 0;
|
||||
let sort_flipped = Object.entries(this.sort)
|
||||
.reduce((obj, [key, value]) => ({...obj, [value]: key}), {});
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
this.items[i].total = this.items[i].amount * this.items[i].price * (1 + this.items[i].tax / 100);
|
||||
tax += this.items[i].amount * this.items[i].price * this.items[i].tax / 100;
|
||||
sum += this.items[i].total;
|
||||
}
|
||||
|
||||
axios.post('invoice', {
|
||||
customer_id: this.customer_id,
|
||||
address_id: this.address_id,
|
||||
delivery_id: this.delivery_id,
|
||||
sum: sum,
|
||||
tax: tax
|
||||
})
|
||||
.then(function (response) {
|
||||
for (let i = 0; i < self.items.length; i++) {
|
||||
let pos = sort_flipped[i];
|
||||
let item = self.items[pos];
|
||||
axios.post('invoice/' + response.data.id + '/item', item)
|
||||
.then(function (response) {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
}
|
||||
console.log(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
console.log(this.customer_id);
|
||||
console.log(this.address_id);
|
||||
console.log(this.delivery_id);
|
||||
console.log(sum);
|
||||
console.log(tax);
|
||||
|
||||
console.log(this.items);
|
||||
},
|
||||
|
||||
handle(item, position) {
|
||||
if (position > self.sort[item]) {
|
||||
for (let i = 0; i < self.sort.length; i++) {
|
||||
if (self.sort[i] <= position && self.sort[i] > self.sort[item]) {
|
||||
self.sort[i] = --self.sort[i];
|
||||
}
|
||||
}
|
||||
self.sort[item] = position;
|
||||
}
|
||||
|
||||
if (position < self.sort[item]) {
|
||||
for (let i = 0; i < self.sort.length; i++) {
|
||||
if (self.sort[i] >= position && self.sort[i] < self.sort[item]) {
|
||||
self.sort[i] = ++self.sort[i];
|
||||
}
|
||||
}
|
||||
self.sort[item] = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
119
resources/views/invoice/index.blade.php
Normal file
119
resources/views/invoice/index.blade.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('invoice.Invoices') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Add new invoice') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("invoice.Add new invoice by clicking add") }}
|
||||
</p>
|
||||
|
||||
</header>
|
||||
<a class="mt-6 inline-block" href="{{ route('invoice.create') }}"><x-primary-button>{{ __('form.Add') }}</x-primary-button></a>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w" x-data="invoiceForm">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Existing invoices') }}
|
||||
</h2>
|
||||
<div class="flex flex-row space-x-4 items-center">
|
||||
<x-input-label for="from" :value="__('invoice.From')"/>
|
||||
<x-text-input type="date" id="from" name="from" x-model="from" x-on:change="fetchInvoices()"/>
|
||||
<x-input-label for="end" :value="__('invoice.End')"/>
|
||||
<x-text-input type="date" id="end" name="end" x-model="end" x-on:change="fetchInvoices()"/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<summary class="cursor-pointer flex flex-row w-full mt-4">
|
||||
<div class="w-1/6 font-bold border-b-2">{{ __('invoice.Invoice Number') }}</div>
|
||||
<div class="w-1/6 font-bold border-b-2">{{ __('common.Name') }}</div>
|
||||
<div class="w-1/4 font-bold border-b-2">{{ __('common.Email') }}</div>
|
||||
<div class="w-1/12 font-bold border-b-2">{{ __('invoice.State') }}</div>
|
||||
<div class="w-1/6 font-bold border-b-2 text-right">{{ __('invoice.Sum') }}</div>
|
||||
<div class="w-1/6 font-bold border-b-2 text-right">{{ __('common.Created at') }}</div>
|
||||
</summary>
|
||||
|
||||
<template x-for="invoice in invoices">
|
||||
<details class="even:bg-gray-100 odd:bg-white">
|
||||
<summary class="cursor-pointer flex flex-row w-full" @click="window.location.href='/invoice/' + invoice.id;">
|
||||
<div class="w-1/6" x-text="invoice.number"></div>
|
||||
<div class="w-1/6" x-text="invoice.address.name"></div>
|
||||
<div class="w-1/4" x-text="invoice.address.email"></div>
|
||||
<div class="w-1/12" x-text="invoice.status"></div>
|
||||
<div class="w-1/6 text-right" x-text="invoice.sum"></div>
|
||||
<div class="w-1/6 text-right" x-text="invoice.created"></div>
|
||||
</summary>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-2 border-t-2">
|
||||
<div>{{ __('invoice.Sum') }}</div>
|
||||
<div x-text="sum"></div>
|
||||
<div>{{ __('invoice.Tax') }}</div>
|
||||
<div x-text="tax"></div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
||||
|
||||
<script>
|
||||
function invoiceForm() {
|
||||
return {
|
||||
from: "{{ \Illuminate\Support\Facades\Date::now()->firstOfMonth()->format('Y-m-d') }}",
|
||||
end: "{{ \Illuminate\Support\Facades\Date::now()->format('Y-m-d') }}",
|
||||
invoices: [],
|
||||
sum: 0,
|
||||
tax: 0,
|
||||
|
||||
init() {
|
||||
this.fetchInvoices();
|
||||
},
|
||||
|
||||
fetchInvoices() {
|
||||
let vm = this;
|
||||
axios.get('/invoice/' + this.from + '/' + this.end)
|
||||
.then(function (response) {
|
||||
vm.invoices = response.data;
|
||||
vm.calculateSum();
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
},
|
||||
|
||||
calculateSum() {
|
||||
this.sum = 0;
|
||||
this.tax = 0;
|
||||
for (const [key, invoice] of Object.entries(this.invoices)) {
|
||||
this.sum += parseFloat(invoice.sum);
|
||||
this.tax += parseFloat(invoice.tax)
|
||||
}
|
||||
this.sum = this.sum.toFixed(2)
|
||||
this.tax = this.tax.toFixed(2);
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
88
resources/views/invoice/show.blade.php
Normal file
88
resources/views/invoice/show.blade.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('invoice.Invoice') }} {{ $invoice->number }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
<!-- Customer and addresses -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('customer.Customer') }}: {{ $invoice->customer->name }} ({{ $invoice->customer->email }})
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div>{{ __("customer.Invoice Address") }}</div>
|
||||
<div>{{ __("customer.Delivery Address") }}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-2">
|
||||
<div>
|
||||
<x-php-card :address="$invoice->address"/>
|
||||
</div>
|
||||
<div>
|
||||
<x-php-card :address="$invoice->delivery"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice items -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('invoice.Invoice items') }}
|
||||
</h2>
|
||||
<div class="flex flex-row items-end gap-2 w-full mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<x-input-label for="invoice_item.amount" :value="__('invoice.Amount')" class="w-1/12"/>
|
||||
<x-input-label for="invoice_item.name" :value="__('invoice.Name')" class="w-2/3"/>
|
||||
<x-input-label for="invoice_item.price" :value="__('invoice.Price')" class="w-1/12 text-right"/>
|
||||
<x-input-label for="invoice_item.tax" :value="__('invoice.Tax')" class="w-1/12 text-right"/>
|
||||
<x-input-label for="invoice_item.tax" :value="__('invoice.Sum')" class="w-1/12 text-right"/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
@foreach($invoice->items as $item)
|
||||
<x-invoice-item :item="$item"/>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice information -->
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w">
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<div class="mt-1 block w-2/3">{{ __('invoice.Net') }}</div>
|
||||
<div class="w-1/4 text-right">{{ \Illuminate\Support\Number::currency($invoice->sum - $invoice->tax) }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<div class="mt-1 block w-2/3">{{ __('invoice.Tax') }}</div>
|
||||
<div class="w-1/4 text-right">{{ \Illuminate\Support\Number::currency($invoice->tax) }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-end gap-2 w-full">
|
||||
<div class="w-1/12"></div>
|
||||
<div class="mt-1 block w-2/3">{{ __('invoice.Sum') }}</div>
|
||||
<div class="w-1/4 text-right">{{ \Illuminate\Support\Number::currency($invoice->sum) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
||||
@@ -19,6 +19,10 @@
|
||||
:active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'customer.')">
|
||||
{{ __('customer.Customers') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('invoice.index')"
|
||||
:active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'invoice.')">
|
||||
{{ __('invoice.Invoices') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('taxrate.index')"
|
||||
:active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'taxrate.')">
|
||||
{{ __('configuration.Taxrates') }}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
use App\Http\Controllers\Api\AddressController;
|
||||
use App\Http\Controllers\Api\AuthController;
|
||||
use App\Http\Controllers\Api\CustomerController;
|
||||
use App\Http\Controllers\Api\InvoiceController;
|
||||
use App\Http\Controllers\Api\InvoiceitemController;
|
||||
use App\Http\Controllers\Api\TaxrateController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -22,6 +24,8 @@ Route::group(['as' => 'api.'], function () {
|
||||
Route::apiResource('/customer', CustomerController::class);
|
||||
Route::apiResource('/customer.address', AddressController::class)->shallow()->except(['update']);
|
||||
Route::apiResource('/taxrate', TaxRateController::class)->except(['show']);
|
||||
Route::get('/invoice/{start?}/{end?}', [InvoiceController::class, 'index'])->name('invoice.index');
|
||||
Route::apiResource('/invoice.item', InvoiceItemController::class)->shallow();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\CustomerController;
|
||||
use App\Http\Controllers\InvoiceController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\TaxrateController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -20,6 +21,7 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
Route::resource('/customer', CustomerController::class)->only(['index', 'create', 'edit']);
|
||||
Route::resource('/taxrate', TaxrateController::class)->only(['index', 'create', 'edit']);
|
||||
Route::resource('/invoice', InvoiceController::class)->only(['index', 'create', 'show', 'edit']);
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
||||
Reference in New Issue
Block a user