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,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.')">
{{ __('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') }}