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\Concerns\AsAction;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class InvoiceStoreAction
|
||||
|
@ -56,5 +57,7 @@ class InvoiceStoreAction
|
|||
foreach ($request->validated('positions') as $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>
|
||||
<page-layout>
|
||||
<template #toolbar>
|
||||
<page-toolbar-button color="primary" icon="plus" @click="model = { ...props.meta.default }">Rechnung
|
||||
anlegen</page-toolbar-button>
|
||||
<page-toolbar-button color="primary" icon="plus" @click="create">Rechnung anlegen</page-toolbar-button>
|
||||
<page-toolbar-button color="primary" icon="plus" @click="massstore = { year: '' }">Massenrechnung
|
||||
anlegen</page-toolbar-button>
|
||||
</template>
|
||||
|
@ -17,6 +16,45 @@
|
|||
</section>
|
||||
</form>
|
||||
</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">
|
||||
<thead>
|
||||
<th>Empfänger</th>
|
||||
|
@ -54,9 +92,9 @@
|
|||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { indexProps, useIndex } from '../../composables/useIndex.js';
|
||||
import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
|
||||
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);
|
||||
|
||||
async function sendMassstore() {
|
||||
|
|
Loading…
Reference in New Issue