- Updated PriceRulesPage.vue to replace "Neu" button with a more intuitive "+" button for creating new rules. - Enhanced QuotesPage.vue with improved handling of quote lines, including read-only states, unique line validation, and better formatting for monetary values. - Added accordion sections for better organization of quote details and improved user experience. - Updated SuppliersPage.vue to replace "Neu" button with a "+" button for adding new suppliers. - Introduced new database migrations to add price category and default sales price fields to activities, and to include 'invoice_created' status in quotes.
113 lines
4.9 KiB
Vue
113 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, reactive, ref, watch } from "vue";
|
|
import { apiDelete, apiGet, apiPost, apiPut } from "../api";
|
|
import FormStatus from "../components/FormStatus.vue";
|
|
import PageHeader from "../components/PageHeader.vue";
|
|
import { decimalString } from "../format";
|
|
import { liveUpdateState } from "../realtime";
|
|
import type { CashDiscountTerm } from "../types";
|
|
|
|
const emptyForm = () => ({
|
|
code: "",
|
|
name: "",
|
|
discount_percent: "2",
|
|
discount_days: 10,
|
|
net_days: 30 as number | null,
|
|
valid_from: null as string | null,
|
|
valid_until: null as string | null,
|
|
is_default_customer_term: false,
|
|
is_default_supplier_term: false,
|
|
is_active: true
|
|
});
|
|
|
|
const terms = ref<CashDiscountTerm[]>([]);
|
|
const selectedId = ref<string | null>(null);
|
|
const form = reactive(emptyForm());
|
|
const status = ref("");
|
|
const kind = ref<"info" | "success" | "error">("info");
|
|
|
|
async function load() {
|
|
const result = await apiGet<CashDiscountTerm[]>("/api/v1/cash-discount-terms");
|
|
if (result.ok) terms.value = result.data;
|
|
else { status.value = result.message; kind.value = "error"; }
|
|
}
|
|
|
|
function createNew() {
|
|
selectedId.value = null;
|
|
Object.assign(form, emptyForm());
|
|
}
|
|
|
|
function select(term: CashDiscountTerm) {
|
|
selectedId.value = term.id;
|
|
Object.assign(form, {
|
|
code: term.code,
|
|
name: term.name,
|
|
discount_percent: term.discount_percent,
|
|
discount_days: term.discount_days,
|
|
net_days: term.net_days ?? null,
|
|
valid_from: term.valid_from ?? null,
|
|
valid_until: term.valid_until ?? null,
|
|
is_default_customer_term: term.is_default_customer_term,
|
|
is_default_supplier_term: term.is_default_supplier_term,
|
|
is_active: term.is_active
|
|
});
|
|
}
|
|
|
|
async function save() {
|
|
const payload = {
|
|
...form,
|
|
discount_percent: decimalString(form.discount_percent)
|
|
};
|
|
const result = selectedId.value
|
|
? await apiPut<CashDiscountTerm>(`/api/v1/cash-discount-terms/${selectedId.value}`, payload)
|
|
: await apiPost<CashDiscountTerm>("/api/v1/cash-discount-terms", payload);
|
|
status.value = result.ok ? "Skonto-Regel gespeichert." : result.message;
|
|
kind.value = result.ok ? "success" : "error";
|
|
if (result.ok) { selectedId.value = result.data.id; await load(); }
|
|
}
|
|
|
|
async function deactivate() {
|
|
if (!selectedId.value) return;
|
|
const result = await apiDelete(`/api/v1/cash-discount-terms/${selectedId.value}`);
|
|
status.value = result.ok ? "Skonto-Regel deaktiviert." : result.message;
|
|
kind.value = result.ok ? "success" : "error";
|
|
if (result.ok) await load();
|
|
}
|
|
|
|
onMounted(load);
|
|
watch(() => liveUpdateState.revision, load);
|
|
</script>
|
|
|
|
<template>
|
|
<PageHeader title="Skonto-Regeln" description="Zahlungsbedingungen für Kunden, Lieferanten und Belege." />
|
|
<div class="workspace-split">
|
|
<section class="panel list-panel">
|
|
<div class="section-title"><h2>Regeln</h2><button type="button" class="add-button" title="Neue Regel" @click="createNew">+</button></div>
|
|
<button v-for="term in terms" :key="term.id" type="button" class="list-row" :class="{ selected: selectedId === term.id }" @click="select(term)">
|
|
<strong>{{ term.code }}</strong>
|
|
<span>{{ term.name }}</span>
|
|
<small>{{ term.discount_percent }} % / {{ term.discount_days }} Tage</small>
|
|
</button>
|
|
<p v-if="terms.length === 0" class="empty">Keine Skonto-Regeln vorhanden.</p>
|
|
</section>
|
|
<section class="panel detail-panel">
|
|
<form @submit.prevent="save">
|
|
<div class="form-grid">
|
|
<label class="field"><span>Code</span><input v-model="form.code" required /></label>
|
|
<label class="field"><span>Name</span><input v-model="form.name" required /></label>
|
|
<label class="field"><span>Skonto %</span><input v-model="form.discount_percent" inputmode="decimal" required /></label>
|
|
<label class="field"><span>Skontofrist Tage</span><input v-model="form.discount_days" type="number" min="0" required /></label>
|
|
<label class="field"><span>Nettoziel Tage</span><input v-model="form.net_days" type="number" min="0" /></label>
|
|
<label class="field"><span>Gültig ab</span><input v-model="form.valid_from" type="date" /></label>
|
|
<label class="field"><span>Gültig bis</span><input v-model="form.valid_until" type="date" /></label>
|
|
<label class="check-row"><input v-model="form.is_default_customer_term" type="checkbox" /><span>Standard für Kunden</span></label>
|
|
<label class="check-row"><input v-model="form.is_default_supplier_term" type="checkbox" /><span>Standard für Lieferanten</span></label>
|
|
<label class="check-row"><input v-model="form.is_active" type="checkbox" /><span>Aktiv</span></label>
|
|
</div>
|
|
<div class="form-actions"><button type="submit">Speichern</button><button v-if="selectedId" type="button" class="secondary" @click="deactivate">Deaktivieren</button></div>
|
|
<FormStatus :message="status" :kind="kind" />
|
|
</form>
|
|
</section>
|
|
</div>
|
|
</template>
|