Add: Store invoice

This commit is contained in:
Philipp Lang 2023-12-16 23:53:18 +01:00
parent ebeb9bc0b0
commit 0b9eb77e77
4 changed files with 193 additions and 4 deletions

View File

@ -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();
}
}

View File

@ -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'),
];
}
}

View File

@ -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};

View File

@ -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() {