First invoice implementation.

This commit is contained in:
2025-01-04 14:25:13 +01:00
parent 96c7fc272a
commit 3b51ab109d
15 changed files with 942 additions and 2 deletions

View 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)
{
//
}
}

View 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)
{
//
}
}

View 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)
{
//
}
}

View File

@@ -3,8 +3,77 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Invoice extends Model 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);
}
} }

View File

@@ -7,5 +7,20 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Invoiceitem extends Model 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
View 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.',
];

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -19,6 +19,10 @@
:active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'customer.')"> :active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'customer.')">
{{ __('customer.Customers') }} {{ __('customer.Customers') }}
</x-nav-link> </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')" <x-nav-link :href="route('taxrate.index')"
:active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'taxrate.')"> :active="\Illuminate\Support\Str::startsWith(request()->route()->getName(), 'taxrate.')">
{{ __('configuration.Taxrates') }} {{ __('configuration.Taxrates') }}

View File

@@ -3,6 +3,8 @@
use App\Http\Controllers\Api\AddressController; use App\Http\Controllers\Api\AddressController;
use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\CustomerController; 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 App\Http\Controllers\Api\TaxrateController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -22,6 +24,8 @@ Route::group(['as' => 'api.'], function () {
Route::apiResource('/customer', CustomerController::class); Route::apiResource('/customer', CustomerController::class);
Route::apiResource('/customer.address', AddressController::class)->shallow()->except(['update']); Route::apiResource('/customer.address', AddressController::class)->shallow()->except(['update']);
Route::apiResource('/taxrate', TaxRateController::class)->except(['show']); 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();
}); });
}); });

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\CustomerController; use App\Http\Controllers\CustomerController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TaxrateController; use App\Http\Controllers\TaxrateController;
use Illuminate\Support\Facades\Route; 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('/customer', CustomerController::class)->only(['index', 'create', 'edit']);
Route::resource('/taxrate', TaxrateController::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'; require __DIR__.'/auth.php';