Add: Store invoice
This commit is contained in:
parent
ebeb9bc0b0
commit
0b9eb77e77
|
@ -7,6 +7,7 @@ use App\Invoice\Enums\InvoiceStatus;
|
||||||
use Lorisleiva\Actions\ActionRequest;
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use App\Invoice\Models\Invoice;
|
use App\Invoice\Models\Invoice;
|
||||||
|
use App\Lib\Events\Succeeded;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class InvoiceStoreAction
|
class InvoiceStoreAction
|
||||||
|
@ -56,5 +57,7 @@ class InvoiceStoreAction
|
||||||
foreach ($request->validated('positions') as $position) {
|
foreach ($request->validated('positions') as $position) {
|
||||||
$invoice->positions()->create($position);
|
$invoice->positions()->create($position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Succeeded::message('Rechnung erstellt.')->dispatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class Succeeded implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
final private function __construct(public string $message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function message(string $message): self
|
||||||
|
{
|
||||||
|
return new self($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(): void
|
||||||
|
{
|
||||||
|
event($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return array<int, Channel>
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Channel('jobs'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import {computed, ref, inject, onBeforeUnmount} from 'vue';
|
||||||
|
import {router} from '@inertiajs/vue3';
|
||||||
|
import useQueueEvents from './useQueueEvents.js';
|
||||||
|
|
||||||
|
export function useIndex(props, siteName) {
|
||||||
|
const axios = inject('axios');
|
||||||
|
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
|
||||||
|
const single = ref(null);
|
||||||
|
const rawProps = JSON.parse(JSON.stringify(props));
|
||||||
|
const inner = {
|
||||||
|
data: ref(rawProps.data),
|
||||||
|
meta: ref(rawProps.meta),
|
||||||
|
};
|
||||||
|
|
||||||
|
function toFilterString(data) {
|
||||||
|
return btoa(encodeURIComponent(JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterString = computed(() => toFilterString(inner.meta.value.filter));
|
||||||
|
|
||||||
|
function reload(resetPage = true, withMeta = true, data) {
|
||||||
|
data = {
|
||||||
|
filter: filterString.value,
|
||||||
|
page: resetPage ? 1 : inner.meta.value.current_page,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
router.visit(window.location.pathname, {
|
||||||
|
data,
|
||||||
|
preserveState: true,
|
||||||
|
only: ['data'],
|
||||||
|
onSuccess: (page) => {
|
||||||
|
inner.data.value = page.props.data.data;
|
||||||
|
if (withMeta) {
|
||||||
|
inner.meta.value = {
|
||||||
|
...inner.meta.value,
|
||||||
|
...page.props.data.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadPage(page) {
|
||||||
|
reload(false, true, {page: page});
|
||||||
|
}
|
||||||
|
|
||||||
|
function can(permission) {
|
||||||
|
return inner.meta.value.can[permission];
|
||||||
|
}
|
||||||
|
|
||||||
|
function create() {
|
||||||
|
single.value = JSON.parse(JSON.stringify(inner.meta.value.default));
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(model) {
|
||||||
|
single.value = JSON.parse(JSON.stringify(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
single.value.id ? await axios.patch(single.value.links.update, single.value) : await axios.post(inner.meta.value.links.store, single.value);
|
||||||
|
reload();
|
||||||
|
single.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(model) {
|
||||||
|
await axios.delete(model.links.destroy);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function can(permission) {
|
||||||
|
return inner.meta.value.can[permission];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
single.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startListener();
|
||||||
|
onBeforeUnmount(() => stopListener());
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: inner.data,
|
||||||
|
meta: inner.meta,
|
||||||
|
single,
|
||||||
|
create,
|
||||||
|
edit,
|
||||||
|
reload,
|
||||||
|
reloadPage,
|
||||||
|
can,
|
||||||
|
router,
|
||||||
|
submit,
|
||||||
|
remove,
|
||||||
|
cancel,
|
||||||
|
axios,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexProps = {
|
||||||
|
data: {
|
||||||
|
default: () => {
|
||||||
|
return {data: [], meta: {}};
|
||||||
|
},
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export {indexProps};
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<page-layout>
|
<page-layout>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<page-toolbar-button color="primary" icon="plus" @click="model = { ...props.meta.default }">Rechnung
|
<page-toolbar-button color="primary" icon="plus" @click="create">Rechnung anlegen</page-toolbar-button>
|
||||||
anlegen</page-toolbar-button>
|
|
||||||
<page-toolbar-button color="primary" icon="plus" @click="massstore = { year: '' }">Massenrechnung
|
<page-toolbar-button color="primary" icon="plus" @click="massstore = { year: '' }">Massenrechnung
|
||||||
anlegen</page-toolbar-button>
|
anlegen</page-toolbar-button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -17,6 +16,45 @@
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</ui-popup>
|
</ui-popup>
|
||||||
|
<ui-popup v-if="single !== null" heading="Rechnung erstellen" inner-width="max-w-4xl" @close="cancel">
|
||||||
|
<form class="grid grid-cols-2 gap-3 mt-4" @submit.prevent="submit">
|
||||||
|
<ui-box heading="Empfänger" container-class="grid grid-cols-2 gap-3">
|
||||||
|
<f-text id="to_name" v-model="single.to.name" name="to_name" label="Name" class="col-span-full"
|
||||||
|
required></f-text>
|
||||||
|
<f-text id="to_address" v-model="single.to.address" name="to_address" class="col-span-full"
|
||||||
|
label="Adresse" required></f-text>
|
||||||
|
<f-text id="to_zip" v-model="single.to.zip" name="to_zip" label="PLZ" required></f-text>
|
||||||
|
<f-text id="to_location" v-model="single.to.location" name="to_location" label="Ort" required></f-text>
|
||||||
|
</ui-box>
|
||||||
|
<ui-box heading="Status" container-class="grid gap-3">
|
||||||
|
<f-select id="status" v-model="single.status" :options="meta.statuses" name="status" label="Status"
|
||||||
|
required></f-select>
|
||||||
|
<f-select id="via" v-model="single.via" :options="meta.vias" name="via" label="Rechnungsweg"
|
||||||
|
required></f-select>
|
||||||
|
<f-text id="greeting" v-model="single.greeting" name="greeting" label="Anrede" required></f-text>
|
||||||
|
</ui-box>
|
||||||
|
<ui-box heading="Positionen" class="col-span-full" container-class="grid gap-3">
|
||||||
|
<template #in-title>
|
||||||
|
<ui-icon-button class="ml-3 btn-primary" icon="plus"
|
||||||
|
@click="single.positions.push({ ...meta.default_position })">Neu</ui-icon-button>
|
||||||
|
</template>
|
||||||
|
<div v-for="(position, index) in single.positions" :key="index" class="flex items-end space-x-3">
|
||||||
|
<f-text :id="`position-description-${index}`" v-model="position.description" class="grow"
|
||||||
|
:name="`position-description-${index}`" label="Beschreibung" required></f-text>
|
||||||
|
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area"
|
||||||
|
:name="`position-price-${index}`" label="Preis" required></f-text>
|
||||||
|
<f-select :id="`position-member-${index}`" v-model="position.member_id" :options="meta.members"
|
||||||
|
:name="`position-member-${index}`" label="Mitglied" required></f-select>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm h-[35px]" icon="trash"
|
||||||
|
@click="single.positions.splice(index, 1)"><ui-sprite src="trash"></ui-sprite></button>
|
||||||
|
</div>
|
||||||
|
</ui-box>
|
||||||
|
<section class="flex mt-4 space-x-2">
|
||||||
|
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
|
||||||
|
<ui-button class="btn-primary" @click.prevent="cancel">Abbrechen</ui-button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</ui-popup>
|
||||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Empfänger</th>
|
<th>Empfänger</th>
|
||||||
|
@ -54,9 +92,9 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { indexProps, useIndex } from '../../composables/useIndex.js';
|
import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
|
||||||
const props = defineProps(indexProps);
|
const props = defineProps(indexProps);
|
||||||
var { axios, meta, data, reloadPage } = useIndex(props.data, 'invoice');
|
var { axios, meta, data, reloadPage, create, single, cancel, submit } = useIndex(props.data, 'invoice');
|
||||||
const massstore = ref(null);
|
const massstore = ref(null);
|
||||||
|
|
||||||
async function sendMassstore() {
|
async function sendMassstore() {
|
||||||
|
|
Loading…
Reference in New Issue