Option to add installment to contract/account to increace balance amount same as payment and can be deleted which will reduce balance amount by new amount of the installment deleted, call later added badge to show active call laters
This commit is contained in:
@@ -15,6 +15,8 @@ import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
|
||||
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
||||
import PaymentDialog from "./PaymentDialog.vue";
|
||||
import ViewPaymentsDialog from "./ViewPaymentsDialog.vue";
|
||||
import InstallmentDialog from "./InstallmentDialog.vue";
|
||||
import ViewInstallmentsDialog from "./ViewInstallmentsDialog.vue";
|
||||
import ContractMetaEditDialog from "./ContractMetaEditDialog.vue";
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import ConfirmationDialog from "@/Components/Dialogs/ConfirmationDialog.vue";
|
||||
@@ -31,6 +33,7 @@ import {
|
||||
faSpinner,
|
||||
faTags,
|
||||
faFolderOpen,
|
||||
faArrowUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import EmptyState from "@/Components/EmptyState.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
@@ -444,6 +447,52 @@ const closePaymentsDialog = () => {
|
||||
selectedContract.value = null;
|
||||
};
|
||||
|
||||
// Installments
|
||||
const showInstallmentDialog = ref(false);
|
||||
const installmentContract = ref(null);
|
||||
const installmentForm = useForm({
|
||||
amount: null,
|
||||
currency: "EUR",
|
||||
installment_at: null,
|
||||
reference: "",
|
||||
});
|
||||
|
||||
const openInstallmentDialog = (c) => {
|
||||
installmentContract.value = c;
|
||||
installmentForm.reset();
|
||||
installmentForm.installment_at = todayStr.value;
|
||||
showInstallmentDialog.value = true;
|
||||
};
|
||||
|
||||
const closeInstallmentDialog = () => {
|
||||
showInstallmentDialog.value = false;
|
||||
installmentContract.value = null;
|
||||
};
|
||||
|
||||
const submitInstallment = () => {
|
||||
if (!installmentContract.value?.account?.id) return;
|
||||
const accountId = installmentContract.value.account.id;
|
||||
installmentForm.post(route("accounts.installments.store", { account: accountId }), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
closeInstallmentDialog();
|
||||
router.reload({ only: ["contracts", "activities"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showInstallmentsDialog = ref(false);
|
||||
|
||||
const openInstallmentsDialog = (c) => {
|
||||
selectedContract.value = c;
|
||||
showInstallmentsDialog.value = true;
|
||||
};
|
||||
|
||||
const closeInstallmentsDialog = () => {
|
||||
showInstallmentsDialog.value = false;
|
||||
selectedContract.value = null;
|
||||
};
|
||||
|
||||
// Meta edit dialog
|
||||
const showMetaEditDialog = ref(false);
|
||||
|
||||
@@ -832,6 +881,26 @@ const availableSegmentsCount = computed(() => {
|
||||
@click="openPaymentDialog(row)"
|
||||
/>
|
||||
|
||||
<div class="my-1 border-t border-gray-100" />
|
||||
|
||||
<!-- Installments -->
|
||||
<div
|
||||
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
|
||||
>
|
||||
Obroki
|
||||
</div>
|
||||
<ActionMenuItem
|
||||
:icon="faCircleInfo"
|
||||
label="Pokaži obroke"
|
||||
@click="openInstallmentsDialog(row)"
|
||||
/>
|
||||
<ActionMenuItem
|
||||
v-if="row.active && row?.account"
|
||||
:icon="faArrowUp"
|
||||
label="Dodaj obrok"
|
||||
@click="openInstallmentDialog(row)"
|
||||
/>
|
||||
|
||||
<!-- Archive -->
|
||||
<template v-if="edit">
|
||||
<div class="my-1 border-t border-gray-100" />
|
||||
@@ -937,6 +1006,20 @@ const availableSegmentsCount = computed(() => {
|
||||
:edit="edit"
|
||||
/>
|
||||
|
||||
<InstallmentDialog
|
||||
:show="showInstallmentDialog"
|
||||
:form="installmentForm"
|
||||
@close="closeInstallmentDialog"
|
||||
@submit="submitInstallment"
|
||||
/>
|
||||
|
||||
<ViewInstallmentsDialog
|
||||
:show="showInstallmentsDialog"
|
||||
:contract="selectedContract"
|
||||
@close="closeInstallmentsDialog"
|
||||
:edit="edit"
|
||||
/>
|
||||
|
||||
<ContractMetaEditDialog
|
||||
:show="showMetaEditDialog"
|
||||
:client_case="client_case"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import CurrencyInput from "@/Components/CurrencyInput.vue";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import DatePicker from "@/Components/DatePicker.vue";
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
form: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "submit"]);
|
||||
|
||||
const onClose = () => emit("close");
|
||||
const onSubmit = () => emit("submit");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CreateDialog
|
||||
:show="show"
|
||||
title="Dodaj obrok"
|
||||
confirm-text="Shrani"
|
||||
:processing="form.processing"
|
||||
@close="onClose"
|
||||
@confirm="onSubmit"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="installmentAmount">Znesek</Label>
|
||||
<CurrencyInput
|
||||
id="installmentAmount"
|
||||
v-model="form.amount"
|
||||
:precision="{ min: 0, max: 2 }"
|
||||
placeholder="0,00"
|
||||
class="w-full"
|
||||
/>
|
||||
<p v-if="form.errors?.amount" class="text-sm text-red-600">
|
||||
{{ form.errors.amount }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="installmentCurrency">Valuta</Label>
|
||||
<Input
|
||||
id="installmentCurrency"
|
||||
type="text"
|
||||
v-model="form.currency"
|
||||
maxlength="3"
|
||||
placeholder="EUR"
|
||||
/>
|
||||
<p v-if="form.errors?.currency" class="text-sm text-red-600">
|
||||
{{ form.errors.currency }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="installmentDate">Datum</Label>
|
||||
<DatePicker
|
||||
id="installmentDate"
|
||||
v-model="form.installment_at"
|
||||
format="dd.MM.yyyy"
|
||||
:error="form.errors?.installment_at"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="installmentReference">Sklic</Label>
|
||||
<Input
|
||||
id="installmentReference"
|
||||
type="text"
|
||||
v-model="form.reference"
|
||||
placeholder="Sklic"
|
||||
/>
|
||||
<p v-if="form.errors?.reference" class="text-sm text-red-600">
|
||||
{{ form.errors.reference }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CreateDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
contract: { type: Object, default: null },
|
||||
edit: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const installments = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const contractRef = computed(() => props.contract?.reference || "—");
|
||||
const accountId = computed(() => props.contract?.account?.id || null);
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return "-";
|
||||
const dt = new Date(d);
|
||||
return isNaN(dt.getTime()) ? "-" : dt.toLocaleDateString("de");
|
||||
}
|
||||
|
||||
async function loadInstallments() {
|
||||
if (!accountId.value) {
|
||||
installments.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
route("accounts.installments.list", { account: accountId.value })
|
||||
);
|
||||
installments.value = data.installments || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit("close");
|
||||
installments.value = [];
|
||||
}
|
||||
|
||||
function deleteInstallment(installmentId) {
|
||||
if (!accountId.value) return;
|
||||
router.delete(
|
||||
route("accounts.installments.destroy", {
|
||||
account: accountId.value,
|
||||
installment: installmentId,
|
||||
}),
|
||||
{
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
only: ["contracts", "activities"],
|
||||
onSuccess: async () => {
|
||||
await loadInstallments();
|
||||
},
|
||||
onError: async () => {
|
||||
await loadInstallments();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await loadInstallments();
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.contract?.account?.id,
|
||||
async () => {
|
||||
if (props.show) {
|
||||
await loadInstallments();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal :show="show" @close="close">
|
||||
<template #title>
|
||||
Obroki za pogodbo <span class="text-gray-600">{{ contractRef }}</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<div v-if="loading" class="text-sm text-gray-500">Nalaganje…</div>
|
||||
<template v-else>
|
||||
<div v-if="installments.length === 0" class="text-sm text-gray-500">Ni obrokov.</div>
|
||||
<div v-else class="divide-y divide-gray-100 border rounded">
|
||||
<div
|
||||
v-for="i in installments"
|
||||
:key="i.id"
|
||||
class="px-3 py-2 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm text-gray-800">
|
||||
{{
|
||||
Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: i.currency || "EUR",
|
||||
}).format(i.amount ?? 0)
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<span>{{ formatDate(i.installment_at) }}</span>
|
||||
<span v-if="i.reference" class="ml-2">Sklic: {{ i.reference }}</span>
|
||||
<span v-if="i.balance_before !== undefined" class="ml-2">
|
||||
Stanje pred:
|
||||
{{
|
||||
Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: i.currency || "EUR",
|
||||
}).format(i.balance_before ?? 0)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2" v-if="edit">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50"
|
||||
@click="deleteInstallment(i.id)"
|
||||
title="Izbriši obrok"
|
||||
>
|
||||
<span class="text-sm">Briši</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2 w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
|
||||
@click="loadInstallments"
|
||||
>
|
||||
Osveži
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
|
||||
@click="close"
|
||||
>
|
||||
Zapri
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user