Package and individual mail sender, new report, and other changes

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Simon Pocrnjič
2026-05-11 21:32:30 +02:00
parent b6bfa17980
commit e3bc5da7e3
49 changed files with 4754 additions and 249 deletions
+83 -1
View File
@@ -1,5 +1,5 @@
<script setup>
import { reactive, ref, computed, onMounted } from "vue";
import { reactive, ref, computed, onMounted, watch } from "vue";
import { Link, router, usePage } from "@inertiajs/vue3";
import axios from "axios";
import AppLayout from "@/Layouts/AppLayout.vue";
@@ -138,11 +138,61 @@ const hasClientFilter = computed(() =>
(props.inputs || []).some((i) => i.type === "select:client")
);
// Async action options for select:action inputs
const actionOptions = ref([]);
const actionLoading = ref(false);
async function fetchActions() {
actionLoading.value = true;
try {
const res = await axios.get(route("reports.actions"));
actionOptions.value = Array.isArray(res.data) ? res.data : [];
} finally {
actionLoading.value = false;
}
}
const hasActionFilter = computed(() =>
(props.inputs || []).some((i) => i.type === "select:action")
);
// Async decision options for select:decision inputs (filtered by selected action)
const decisionOptions = ref([]);
const decisionLoading = ref(false);
async function fetchDecisions(actionId = null) {
decisionLoading.value = true;
try {
const params = actionId ? { action_id: actionId } : {};
const res = await axios.get(route("reports.decisions"), { params });
decisionOptions.value = Array.isArray(res.data) ? res.data : [];
} finally {
decisionLoading.value = false;
}
}
const hasDecisionFilter = computed(() =>
(props.inputs || []).some((i) => i.type === "select:decision")
);
onMounted(() => {
if (hasUserFilter.value) fetchUsers(true);
if (hasClientFilter.value) fetchClients(true);
if (hasActionFilter.value) fetchActions();
if (hasDecisionFilter.value) {
const actionInput = (props.inputs || []).find((i) => i.type === "select:action");
fetchDecisions(actionInput ? (filters[actionInput.key] ?? null) : null);
}
});
// When action filter changes, reload decisions filtered to that action
const actionKey = (props.inputs || []).find((i) => i.type === "select:action")?.key;
if (hasDecisionFilter.value && actionKey) {
watch(
() => filters[actionKey],
(newActionId) => {
filters.decision_id = null;
fetchDecisions(newActionId ?? null);
}
);
}
// Formatting helpers (EU style)
function formatNumberEU(val) {
if (typeof val !== "number") return String(val ?? "");
@@ -382,6 +432,38 @@ function formatCell(value, key) {
Nalagam
</div>
</template>
<template v-else-if="inp.type === 'select:action'">
<AppCombobox
v-model="filters[inp.key]"
:items="
actionOptions.map((a) => ({ value: a.id, label: a.name }))
"
placeholder="Brez"
search-placeholder="Išči akcijo..."
empty-text="Ni akcij"
:disabled="actionLoading"
button-class="w-full"
/>
<div v-if="actionLoading" class="text-xs text-muted-foreground">
Nalagam
</div>
</template>
<template v-else-if="inp.type === 'select:decision'">
<AppCombobox
v-model="filters[inp.key]"
:items="
decisionOptions.map((d) => ({ value: d.id, label: d.name }))
"
placeholder="Brez"
search-placeholder="Išči odločitev..."
empty-text="Ni odločitev"
:disabled="decisionLoading"
button-class="w-full"
/>
<div v-if="decisionLoading" class="text-xs text-muted-foreground">
Nalagam
</div>
</template>
<template v-else>
<Input
v-model="filters[inp.key]"