Merge branch 'master' into Development
This commit is contained in:
@@ -35,6 +35,15 @@ const filteredSenders = computed(() => {
|
||||
return props.senders.filter((s) => s.profile_id === form.profile_id);
|
||||
});
|
||||
|
||||
function onTemplateChange() {
|
||||
const template = props.templates.find(t => t.id === form.template_id)
|
||||
if (template?.content) {
|
||||
form.body = template.content
|
||||
} else {
|
||||
form.body = ''
|
||||
}
|
||||
}
|
||||
|
||||
function submitCreate() {
|
||||
const lines = (form.numbers || "")
|
||||
.split(/\r?\n/)
|
||||
@@ -77,71 +86,39 @@ function submitCreate() {
|
||||
}
|
||||
|
||||
// Contracts mode state & actions
|
||||
const contracts = ref({
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
});
|
||||
const segmentId = ref(null);
|
||||
const search = ref("");
|
||||
const clientId = ref(null);
|
||||
const onlyMobile = ref(false);
|
||||
const onlyValidated = ref(false);
|
||||
const loadingContracts = ref(false);
|
||||
const selectedContractIds = ref(new Set());
|
||||
const deletingId = ref(null);
|
||||
const creatingFromContracts = ref(false);
|
||||
|
||||
const allOnPageSelected = computed(() => {
|
||||
const ids = (contracts.value.data || []).map((c) => c.id);
|
||||
if (!ids.length) return false;
|
||||
return ids.every((id) => selectedContractIds.value.has(id));
|
||||
});
|
||||
|
||||
function pageContractIds() {
|
||||
return (contracts.value.data || []).map((c) => c.id);
|
||||
}
|
||||
|
||||
function toggleSelectAllOnPage() {
|
||||
const ids = pageContractIds();
|
||||
if (!ids.length) return;
|
||||
const set = new Set(selectedContractIds.value);
|
||||
if (allOnPageSelected.value) {
|
||||
// deselect all visible
|
||||
ids.forEach((id) => set.delete(id));
|
||||
} else {
|
||||
// select all visible
|
||||
ids.forEach((id) => set.add(id));
|
||||
}
|
||||
selectedContractIds.value = new Set(Array.from(set));
|
||||
}
|
||||
const contracts = ref({ data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } })
|
||||
const segmentId = ref(null)
|
||||
const search = ref('')
|
||||
const clientId = ref(null)
|
||||
const startDateFrom = ref('')
|
||||
const startDateTo = ref('')
|
||||
const promiseDateFrom = ref('')
|
||||
const promiseDateTo = ref('')
|
||||
const onlyMobile = ref(false)
|
||||
const onlyValidated = ref(false)
|
||||
const loadingContracts = ref(false)
|
||||
const selectedContractIds = ref(new Set())
|
||||
const perPage = ref(25)
|
||||
|
||||
async function loadContracts(url = null) {
|
||||
if (!segmentId.value) {
|
||||
contracts.value = {
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
return;
|
||||
}
|
||||
loadingContracts.value = true;
|
||||
loadingContracts.value = true
|
||||
try {
|
||||
const target =
|
||||
url ||
|
||||
`${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
|
||||
segmentId.value
|
||||
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
|
||||
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
|
||||
}${onlyMobile.value ? `&only_mobile=1` : ""}${
|
||||
onlyValidated.value ? `&only_validated=1` : ""
|
||||
}`;
|
||||
const res = await fetch(target, {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
const json = await res.json();
|
||||
contracts.value = {
|
||||
data: json.data || [],
|
||||
meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
const params = new URLSearchParams()
|
||||
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||
if (search.value) params.append('q', search.value)
|
||||
if (clientId.value) params.append('client_id', clientId.value)
|
||||
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||
if (onlyValidated.value) params.append('only_validated', '1')
|
||||
params.append('per_page', perPage.value)
|
||||
|
||||
const target = url || `${route('admin.packages.contracts')}?${params.toString()}`
|
||||
const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
const json = await res.json()
|
||||
contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
|
||||
} finally {
|
||||
loadingContracts.value = false;
|
||||
}
|
||||
@@ -176,18 +153,52 @@ function deletePackage(pkg) {
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const currentPageIds = contracts.value.data.map(c => c.id)
|
||||
const allSelected = currentPageIds.every(id => selectedContractIds.value.has(id))
|
||||
|
||||
if (allSelected) {
|
||||
// Deselect all on current page
|
||||
currentPageIds.forEach(id => selectedContractIds.value.delete(id))
|
||||
} else {
|
||||
// Select all on current page
|
||||
currentPageIds.forEach(id => selectedContractIds.value.add(id))
|
||||
}
|
||||
|
||||
// Force reactivity
|
||||
selectedContractIds.value = new Set(Array.from(selectedContractIds.value))
|
||||
}
|
||||
|
||||
const allCurrentPageSelected = computed(() => {
|
||||
if (!contracts.value.data.length) return false
|
||||
return contracts.value.data.every(c => selectedContractIds.value.has(c.id))
|
||||
})
|
||||
|
||||
const someCurrentPageSelected = computed(() => {
|
||||
if (!contracts.value.data.length) return false
|
||||
return contracts.value.data.some(c => selectedContractIds.value.has(c.id)) && !allCurrentPageSelected.value
|
||||
})
|
||||
|
||||
function goContractsPage(delta) {
|
||||
const { current_page } = contracts.value.meta;
|
||||
const nextPage = current_page + delta;
|
||||
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return;
|
||||
const base = `${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
|
||||
segmentId.value
|
||||
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
|
||||
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
|
||||
}${onlyMobile.value ? `&only_mobile=1` : ""}${
|
||||
onlyValidated.value ? `&only_validated=1` : ""
|
||||
}&page=${nextPage}`;
|
||||
loadContracts(base);
|
||||
const { current_page } = contracts.value.meta
|
||||
const nextPage = current_page + delta
|
||||
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||
if (search.value) params.append('q', search.value)
|
||||
if (clientId.value) params.append('client_id', clientId.value)
|
||||
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||
if (onlyValidated.value) params.append('only_validated', '1')
|
||||
params.append('per_page', perPage.value)
|
||||
params.append('page', nextPage)
|
||||
|
||||
const base = `${route('admin.packages.contracts')}?${params.toString()}`
|
||||
loadContracts(base)
|
||||
}
|
||||
|
||||
function submitCreateFromContracts() {
|
||||
@@ -283,10 +294,7 @@ function submitCreateFromContracts() {
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Predloga</label>
|
||||
<select
|
||||
v-model.number="form.template_id"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<select v-model.number="form.template_id" @change="onTemplateChange" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
@@ -331,88 +339,134 @@ function submitCreateFromContracts() {
|
||||
|
||||
<!-- Contracts mode -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Segment</label>
|
||||
<select
|
||||
v-model.number="segmentId"
|
||||
@change="loadContracts()"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">
|
||||
{{ s.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Stranka</label>
|
||||
<select
|
||||
v-model.number="clientId"
|
||||
@change="loadContracts()"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Iskanje</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="search"
|
||||
@keyup.enter="loadContracts()"
|
||||
type="text"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
placeholder="referenca..."
|
||||
/>
|
||||
<button @click="loadContracts()" class="px-3 py-1.5 rounded border text-sm">
|
||||
Išči
|
||||
<div class="sm:col-span-3 space-y-4">
|
||||
<!-- Basic filters -->
|
||||
<div class="grid sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Segment</label>
|
||||
<select v-model.number="segmentId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">Vsi segmenti</option>
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Stranka</label>
|
||||
<select v-model.number="clientId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">Vse stranke</option>
|
||||
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Iskanje po referenci</label>
|
||||
<input v-model="search" @keyup.enter="loadContracts()" type="text" class="w-full rounded border-gray-300 text-sm" placeholder="Vnesi referenco...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date range filters -->
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-xs font-semibold text-gray-700 mb-3">Datumski filtri</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">Datum začetka pogodbe</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||
<input v-model="startDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||
<input v-model="startDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">Datum obljube plačila</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||
<input v-model="promiseDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||
<input v-model="promiseDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone filters -->
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-xs font-semibold text-gray-700 mb-3">Telefonski filtri</h4>
|
||||
<div class="flex items-center gap-6 text-sm text-gray-700">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" class="rounded border-gray-300">
|
||||
Samo mobilne številke
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" class="rounded border-gray-300">
|
||||
Samo potrjene številke
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<button @click="loadContracts()" class="px-4 py-2 rounded bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700">
|
||||
Išči pogodbe
|
||||
</button>
|
||||
<button
|
||||
@click="segmentId = null; clientId = null; search = ''; startDateFrom = ''; startDateTo = ''; promiseDateFrom = ''; promiseDateTo = ''; onlyMobile = false; onlyValidated = false; contracts.value = { data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }"
|
||||
class="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50"
|
||||
>
|
||||
Počisti filtre
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-3 flex items-center gap-6 text-sm text-gray-700">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" />
|
||||
Samo s mobilno številko
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" />
|
||||
Telefonska številka mora biti potrjena
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Results table -->
|
||||
<div class="sm:col-span-3">
|
||||
<div class="overflow-hidden rounded border bg-white">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr class="text-xs text-gray-500">
|
||||
<th class="px-3 py-2 w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allOnPageSelected"
|
||||
:disabled="!contracts.data?.length"
|
||||
@change="toggleSelectAllOnPage"
|
||||
title="Izberi vse na strani"
|
||||
/>
|
||||
<th class="px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allCurrentPageSelected"
|
||||
:indeterminate="someCurrentPageSelected"
|
||||
@change="toggleSelectAll"
|
||||
:disabled="!contracts.data.length"
|
||||
class="rounded"
|
||||
title="Izberi vse na tej strani"
|
||||
>
|
||||
</th>
|
||||
<th class="px-3 py-2 text-left">Pogodba</th>
|
||||
<th class="px-3 py-2 text-left">Primer</th>
|
||||
<th class="px-3 py-2 text-left">Stranka</th>
|
||||
<th class="px-3 py-2 text-left">Datum začetka</th>
|
||||
<th class="px-3 py-2 text-left">Zadnja obljuba</th>
|
||||
<th class="px-3 py-2 text-left">Izbrana številka</th>
|
||||
<th class="px-3 py-2 text-left">Opomba</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" v-if="!loadingContracts">
|
||||
<tr v-for="c in contracts.data" :key="c.id" class="text-sm">
|
||||
<tr v-for="c in contracts.data" :key="c.id" class="text-sm hover:bg-gray-50">
|
||||
<td class="px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedContractIds.has(c.id)"
|
||||
@change="toggleSelectContract(c.id)"
|
||||
/>
|
||||
<input type="checkbox" :checked="selectedContractIds.has(c.id)" @change="toggleSelectContract(c.id)" class="rounded">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-mono text-xs text-gray-600">{{ c.uuid }}</div>
|
||||
<div class="text-xs text-gray-800">{{ c.reference }}</div>
|
||||
<a
|
||||
v-if="c.case?.uuid"
|
||||
:href="route('clientCase.show', c.case.uuid)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs font-medium text-indigo-600 hover:text-indigo-800 hover:underline"
|
||||
>
|
||||
{{ c.reference }}
|
||||
</a>
|
||||
<div v-else class="text-xs font-medium text-gray-800">{{ c.reference }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-800">
|
||||
@@ -422,6 +476,12 @@ function submitCreateFromContracts() {
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-800">{{ c.client?.name || "—" }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-700">{{ c.start_date ? new Date(c.start_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-700">{{ c.promise_date ? new Date(c.promise_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div v-if="c.selected_phone" class="text-xs">
|
||||
{{ c.selected_phone.number }}
|
||||
@@ -437,55 +497,45 @@ function submitCreateFromContracts() {
|
||||
{{ c.no_phone_reason || "—" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!contracts.data?.length">
|
||||
<td colspan="6" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||
Ni rezultatov.
|
||||
</td>
|
||||
<tr v-if="!contracts.data?.length">
|
||||
<td colspan="8" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||
Ni rezultatov. Poskusite z drugimi filtri.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-6 text-center text-sm text-gray-500">
|
||||
Nalaganje...
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="8" class="px-3 py-6 text-center text-sm text-gray-500">Nalaganje...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between text-sm">
|
||||
<div class="text-gray-600">
|
||||
Prikazano stran {{ contracts.meta.current_page }} od
|
||||
{{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
||||
<div class="text-gray-600 flex items-center gap-4">
|
||||
<span v-if="contracts.data.length">
|
||||
Prikazano stran {{ contracts.meta.current_page }} od {{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-gray-500">Na stran:</label>
|
||||
<select v-model.number="perPage" @change="loadContracts()" class="rounded border-gray-300 text-xs py-1">
|
||||
<option :value="10">10</option>
|
||||
<option :value="25">25</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
<option :value="200">200</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
@click="goContractsPage(-1)"
|
||||
:disabled="contracts.meta.current_page <= 1"
|
||||
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
|
||||
>
|
||||
Nazaj
|
||||
</button>
|
||||
<button
|
||||
@click="goContractsPage(1)"
|
||||
:disabled="contracts.meta.current_page >= contracts.meta.last_page"
|
||||
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
|
||||
>
|
||||
Naprej
|
||||
</button>
|
||||
<button @click="goContractsPage(-1)" :disabled="contracts.meta.current_page <= 1" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Nazaj</button>
|
||||
<button @click="goContractsPage(1)" :disabled="contracts.meta.current_page >= contracts.meta.last_page" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Naprej</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-3 flex items-center justify-end gap-2">
|
||||
<div class="text-sm text-gray-600 mr-auto">
|
||||
Izbrano: {{ selectedContractIds.size }}
|
||||
<div class="sm:col-span-3 flex items-center justify-between gap-2 pt-4 border-t">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">Izbrano: {{ selectedContractIds.size }}</span>
|
||||
<span v-if="selectedContractIds.size > 0" class="ml-2 text-gray-500">({{ selectedContractIds.size === 1 ? '1 pogodba' : `${selectedContractIds.size} pogodb` }})</span>
|
||||
</div>
|
||||
<button
|
||||
@click="submitCreateFromContracts"
|
||||
:disabled="selectedContractIds.size === 0 || creatingFromContracts"
|
||||
class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm disabled:opacity-50"
|
||||
>
|
||||
Ustvari paket
|
||||
</button>
|
||||
<button @click="submitCreateFromContracts" :disabled="selectedContractIds.size === 0" class="px-4 py-2 rounded bg-emerald-600 text-white text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:bg-emerald-700">Ustvari paket</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link } from "@inertiajs/vue3";
|
||||
import { useForm, Link, router } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMagnifyingGlass, faFloppyDisk } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMagnifyingGlass, faFloppyDisk, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
@@ -65,6 +66,49 @@ const filteredUsers = computed(() => {
|
||||
});
|
||||
|
||||
const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
|
||||
// Create user modal
|
||||
const showCreateModal = ref(false);
|
||||
const createForm = useForm({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
roles: [],
|
||||
});
|
||||
|
||||
function openCreateModal() {
|
||||
createForm.reset();
|
||||
createForm.clearErrors();
|
||||
showCreateModal.value = true;
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
showCreateModal.value = false;
|
||||
createForm.reset();
|
||||
}
|
||||
|
||||
function submitCreateUser() {
|
||||
createForm.post(route("admin.users.store"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
closeCreateModal();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCreateRole(roleId) {
|
||||
const exists = createForm.roles.includes(roleId);
|
||||
createForm.roles = exists
|
||||
? createForm.roles.filter((id) => id !== roleId)
|
||||
: [...createForm.roles, roleId];
|
||||
}
|
||||
|
||||
function toggleUserActive(userId) {
|
||||
router.patch(route("admin.users.toggle-active", { user: userId }), {}, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -127,6 +171,14 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateModal"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" />
|
||||
Ustvari uporabnika
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitAll"
|
||||
@@ -151,6 +203,9 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
||||
Uporabnik
|
||||
</th>
|
||||
<th class="p-2 text-center font-medium text-[11px] uppercase tracking-wide">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
@@ -172,6 +227,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
:class="[
|
||||
'border-t border-slate-100',
|
||||
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
||||
!user.active && 'opacity-60',
|
||||
]"
|
||||
>
|
||||
<td class="p-2 whitespace-nowrap align-top">
|
||||
@@ -191,6 +247,19 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2 text-center align-top">
|
||||
<button
|
||||
@click="toggleUserActive(user.id)"
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium transition"
|
||||
:class="
|
||||
user.active
|
||||
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
"
|
||||
>
|
||||
{{ user.active ? 'Aktiven' : 'Neaktiven' }}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
@@ -223,7 +292,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</tr>
|
||||
<tr v-if="!filteredUsers.length">
|
||||
<td
|
||||
:colspan="props.roles.length + 2"
|
||||
:colspan="props.roles.length + 3"
|
||||
class="p-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
Ni rezultatov
|
||||
@@ -265,5 +334,107 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
<DialogModal :show="showCreateModal" @close="closeCreateModal" max-width="2xl">
|
||||
<template #title>Ustvari novega uporabnika</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ime</label>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
type="text"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="Ime uporabnika"
|
||||
/>
|
||||
<div v-if="createForm.errors.name" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">E-pošta</label>
|
||||
<input
|
||||
v-model="createForm.email"
|
||||
type="email"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="uporabnik@example.com"
|
||||
/>
|
||||
<div v-if="createForm.errors.email" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Geslo</label>
|
||||
<input
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
<div v-if="createForm.errors.password" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Potrdi geslo</label>
|
||||
<input
|
||||
v-model="createForm.password_confirmation"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Vloge</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="role in props.roles"
|
||||
:key="'create-role-' + role.id"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition"
|
||||
:class="
|
||||
createForm.roles.includes(role.id)
|
||||
? 'bg-indigo-50 border-indigo-600 text-indigo-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
:checked="createForm.roles.includes(role.id)"
|
||||
@change="toggleCreateRole(role.id)"
|
||||
/>
|
||||
<span class="text-sm font-medium">{{ role.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="createForm.errors.roles" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.roles }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeCreateModal"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitCreateUser"
|
||||
:disabled="createForm.processing"
|
||||
class="ml-3 px-4 py-2 rounded-md text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="createForm.processing">Ustvarjanje...</span>
|
||||
<span v-else>Ustvari</span>
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user