Client contracts view added excel export option
This commit is contained in:
parent
fb6474ab88
commit
dc41862afc
158
app/Exports/ClientContractsExport.php
Normal file
158
app/Exports/ClientContractsExport.php
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Contract;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Maatwebsite\Excel\Concerns\FromQuery;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomValueBinder;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
|
||||
class ClientContractsExport extends DefaultValueBinder implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithCustomValueBinder, WithHeadings, WithMapping
|
||||
{
|
||||
public const DATE_EXCEL_FORMAT = 'dd"."mm"."yyyy';
|
||||
|
||||
public const TEXT_EXCEL_FORMAT = NumberFormat::FORMAT_TEXT;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $columnLetterMap = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array{label: string}>
|
||||
*/
|
||||
public const COLUMN_METADATA = [
|
||||
'reference' => ['label' => 'Referenca'],
|
||||
'customer' => ['label' => 'Stranka'],
|
||||
'start' => ['label' => 'Začetek'],
|
||||
'segment' => ['label' => 'Segment'],
|
||||
'balance' => ['label' => 'Stanje'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<int, string> $columns
|
||||
*/
|
||||
public function __construct(private Builder $query, private array $columns) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function allowedColumns(): array
|
||||
{
|
||||
return array_keys(self::COLUMN_METADATA);
|
||||
}
|
||||
|
||||
public static function columnLabel(string $column): string
|
||||
{
|
||||
return self::COLUMN_METADATA[$column]['label'] ?? $column;
|
||||
}
|
||||
|
||||
public function query(): Builder
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function map($row): array
|
||||
{
|
||||
return array_map(fn (string $column) => $this->resolveValue($row, $column), $this->columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function headings(): array
|
||||
{
|
||||
return array_map(fn (string $column) => self::columnLabel($column), $this->columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function columnFormats(): array
|
||||
{
|
||||
$formats = [];
|
||||
|
||||
foreach ($this->getColumnLetterMap() as $letter => $column) {
|
||||
if ($column === 'reference') {
|
||||
$formats[$letter] = self::TEXT_EXCEL_FORMAT;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($column === 'start') {
|
||||
$formats[$letter] = self::DATE_EXCEL_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
private function resolveValue(Contract $contract, string $column): mixed
|
||||
{
|
||||
return match ($column) {
|
||||
'reference' => $contract->reference,
|
||||
'customer' => optional($contract->clientCase?->person)->full_name,
|
||||
'start' => $this->formatDate($contract->start_date),
|
||||
'segment' => $contract->segments?->first()?->name,
|
||||
'balance' => optional($contract->account)->balance_amount,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function formatDate(?string $date): mixed
|
||||
{
|
||||
if (empty($date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$carbon = Carbon::parse($date);
|
||||
|
||||
return ExcelDate::dateTimeToExcel($carbon);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getColumnLetterMap(): array
|
||||
{
|
||||
if ($this->columnLetterMap !== []) {
|
||||
return $this->columnLetterMap;
|
||||
}
|
||||
|
||||
$letter = 'A';
|
||||
foreach ($this->columns as $column) {
|
||||
$this->columnLetterMap[$letter] = $column;
|
||||
$letter++;
|
||||
}
|
||||
|
||||
return $this->columnLetterMap;
|
||||
}
|
||||
|
||||
public function bindValue(Cell $cell, $value): bool
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::bindValue($cell, $value);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exports\ClientContractsExport;
|
||||
use App\Http\Requests\ExportClientContractsRequest;
|
||||
use App\Models\Client;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
|
|
@ -175,6 +179,83 @@ public function contracts(Client $client, Request $request)
|
|||
]);
|
||||
}
|
||||
|
||||
public function exportContracts(ExportClientContractsRequest $request, Client $client)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$columns = array_values(array_unique($data['columns']));
|
||||
|
||||
$from = $data['from'] ?? null;
|
||||
$to = $data['to'] ?? null;
|
||||
$search = $data['search'] ?? null;
|
||||
$segmentsParam = $data['segments'] ?? null;
|
||||
$segmentIds = $segmentsParam ? array_filter(explode(',', $segmentsParam)) : [];
|
||||
|
||||
$query = \App\Models\Contract::query()
|
||||
->whereHas('clientCase', function ($q) use ($client) {
|
||||
$q->where('client_id', $client->id);
|
||||
})
|
||||
->with([
|
||||
'clientCase:id,uuid,person_id',
|
||||
'clientCase.person:id,full_name',
|
||||
'segments' => function ($q) {
|
||||
$q->wherePivot('active', true)->select('segments.id', 'segments.name');
|
||||
},
|
||||
'account:id,accounts.contract_id,balance_amount',
|
||||
])
|
||||
->select(['id', 'uuid', 'reference', 'start_date', 'client_case_id'])
|
||||
->whereNull('deleted_at')
|
||||
->when($from || $to, function ($q) use ($from, $to) {
|
||||
if (! empty($from)) {
|
||||
$q->whereDate('start_date', '>=', $from);
|
||||
}
|
||||
if (! empty($to)) {
|
||||
$q->whereDate('start_date', '<=', $to);
|
||||
}
|
||||
})
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->where(function ($inner) use ($search) {
|
||||
$inner->where('reference', 'ilike', '%'.$search.'%')
|
||||
->orWhereHas('clientCase.person', function ($p) use ($search) {
|
||||
$p->where('full_name', 'ilike', '%'.$search.'%');
|
||||
});
|
||||
});
|
||||
})
|
||||
->when($segmentIds, function ($q) use ($segmentIds) {
|
||||
$q->whereHas('segments', function ($s) use ($segmentIds) {
|
||||
$s->whereIn('segments.id', $segmentIds)
|
||||
->where('contract_segment.active', true);
|
||||
});
|
||||
})
|
||||
->orderByDesc('start_date');
|
||||
|
||||
if (($data['scope'] ?? ExportClientContractsRequest::SCOPE_ALL) === ExportClientContractsRequest::SCOPE_CURRENT) {
|
||||
$page = max(1, (int) ($data['page'] ?? 1));
|
||||
$perPage = max(1, min(200, (int) ($data['per_page'] ?? 15)));
|
||||
$query->forPage($page, $perPage);
|
||||
}
|
||||
|
||||
$filename = $this->buildExportFilename($client);
|
||||
|
||||
return Excel::download(new ClientContractsExport($query, $columns), $filename);
|
||||
}
|
||||
|
||||
private function buildExportFilename(Client $client): string
|
||||
{
|
||||
$datePrefix = now()->format('dmy');
|
||||
$clientName = $this->slugify($client->person?->full_name ?? 'stranka');
|
||||
|
||||
return sprintf('%s_%s-Pogodbe.xlsx', $datePrefix, $clientName);
|
||||
}
|
||||
|
||||
private function slugify(?string $value): string
|
||||
{
|
||||
if (empty($value)) {
|
||||
return 'data';
|
||||
}
|
||||
|
||||
return Str::slug($value, '-') ?: 'data';
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
|
||||
|
|
|
|||
43
app/Http/Requests/ExportClientContractsRequest.php
Normal file
43
app/Http/Requests/ExportClientContractsRequest.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Exports\ClientContractsExport;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ExportClientContractsRequest extends FormRequest
|
||||
{
|
||||
public const SCOPE_CURRENT = 'current';
|
||||
|
||||
public const SCOPE_ALL = 'all';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$columnRule = Rule::in(ClientContractsExport::allowedColumns());
|
||||
|
||||
return [
|
||||
'scope' => ['required', Rule::in([self::SCOPE_CURRENT, self::SCOPE_ALL])],
|
||||
'columns' => ['required', 'array', 'min:1'],
|
||||
'columns.*' => ['string', $columnRule],
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'segments' => ['nullable', 'string'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'per_page' => $this->input('per_page') ?? $this->input('perPage'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { Link, router, useForm } from "@inertiajs/vue3";
|
||||
import axios from "axios";
|
||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
||||
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
|
||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
|
|
@ -31,6 +32,29 @@ const showSegmentModal = ref(false);
|
|||
const targetSegmentId = ref(null);
|
||||
const segmentForm = useForm({ segment_id: null, contracts: [] });
|
||||
|
||||
const exportDialogOpen = ref(false);
|
||||
const exportScope = ref("current");
|
||||
const exportColumns = ref(["reference", "customer", "start", "segment", "balance"]);
|
||||
const exportError = ref("");
|
||||
const isExporting = ref(false);
|
||||
|
||||
const exportableColumns = [
|
||||
{ key: "reference", label: "Referenca" },
|
||||
{ key: "customer", label: "Stranka" },
|
||||
{ key: "start", label: "Začetek" },
|
||||
{ key: "segment", label: "Segment" },
|
||||
{ key: "balance", label: "Stanje" },
|
||||
];
|
||||
|
||||
const allColumnsSelected = computed(
|
||||
() => exportColumns.value.length === exportableColumns.length
|
||||
);
|
||||
const exportDisabled = computed(
|
||||
() => exportColumns.value.length === 0 || isExporting.value
|
||||
);
|
||||
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
|
||||
const totalContracts = computed(() => props.contracts?.total ?? 0);
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectedRows.value.length === props.contracts.data.length) {
|
||||
selectedRows.value = [];
|
||||
|
|
@ -149,6 +173,128 @@ function formatDate(value) {
|
|||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllColumns(checked) {
|
||||
exportColumns.value = checked ? exportableColumns.map((col) => col.key) : [];
|
||||
}
|
||||
|
||||
function openExportDialog() {
|
||||
exportDialogOpen.value = true;
|
||||
exportError.value = "";
|
||||
}
|
||||
|
||||
function closeExportDialog() {
|
||||
exportDialogOpen.value = false;
|
||||
}
|
||||
|
||||
async function submitExport() {
|
||||
if (exportColumns.value.length === 0) {
|
||||
exportError.value = "Izberi vsaj en stolpec.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
exportError.value = "";
|
||||
isExporting.value = true;
|
||||
|
||||
const payload = {
|
||||
scope: exportScope.value,
|
||||
columns: [...exportColumns.value],
|
||||
from: fromDate.value || "",
|
||||
to: toDate.value || "",
|
||||
search: search.value || "",
|
||||
segments:
|
||||
selectedSegments.value.length > 0
|
||||
? selectedSegments.value.map((s) => s.id).join(",")
|
||||
: "",
|
||||
page: props.contracts.current_page,
|
||||
per_page: props.contracts.per_page,
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
route("client.contracts.export", { uuid: props.client.uuid }),
|
||||
payload,
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
const filename =
|
||||
extractFilenameFromHeaders(response.headers) || buildDefaultFilename();
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
exportDialogOpen.value = false;
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
console.error("Error response:", error.response);
|
||||
|
||||
let errorMessage = "Izvoz je spodletel. Poskusi znova.";
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
errorMessage = "Pot za izvoz ne obstaja. Prosim kontaktiraj administratorja.";
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Napaka na strežniku. Poskusi znova.";
|
||||
} else if (error.response?.data) {
|
||||
try {
|
||||
const text = await error.response.data.text();
|
||||
const json = JSON.parse(text);
|
||||
errorMessage = json.message || errorMessage;
|
||||
} catch (e) {
|
||||
console.error("Could not parse error response:", e);
|
||||
}
|
||||
}
|
||||
|
||||
exportError.value = errorMessage;
|
||||
} finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
if (!value) {
|
||||
return "data";
|
||||
}
|
||||
const slug = value.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "");
|
||||
return slug || "data";
|
||||
}
|
||||
|
||||
function buildDefaultFilename() {
|
||||
const now = new Date();
|
||||
const dd = String(now.getDate()).padStart(2, "0");
|
||||
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const yy = String(now.getFullYear()).slice(-2);
|
||||
const clientName = props.client?.person?.full_name || "stranka";
|
||||
return `${dd}${mm}${yy}_${slugify(clientName)}-Pogodbe.xlsx`;
|
||||
}
|
||||
|
||||
function extractFilenameFromHeaders(headers) {
|
||||
if (!headers) {
|
||||
return null;
|
||||
}
|
||||
const disposition =
|
||||
headers["content-disposition"] || headers["Content-Disposition"] || "";
|
||||
if (!disposition) {
|
||||
return null;
|
||||
}
|
||||
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (utf8Match?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
} catch (error) {
|
||||
return utf8Match[1];
|
||||
}
|
||||
}
|
||||
const asciiMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
return asciiMatch?.[1] || null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -325,41 +471,50 @@ function formatDate(value) {
|
|||
:only-props="['contracts']"
|
||||
>
|
||||
<template #toolbar-extra>
|
||||
<div v-if="selectedRows.length" class="flex items-center gap-2">
|
||||
<div class="text-sm text-gray-700">
|
||||
Izbrano: <span class="font-medium">{{ selectedRows.length }}</span>
|
||||
</div>
|
||||
<Dropdown width="48" align="left">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Akcije
|
||||
<svg
|
||||
class="ml-1 h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
|
||||
@click="openExportDialog"
|
||||
>
|
||||
Izvozi v Excel
|
||||
</button>
|
||||
<div v-if="selectedRows.length" class="flex items-center gap-2">
|
||||
<div class="text-sm text-gray-700">
|
||||
Izbrano: <span class="font-medium">{{ selectedRows.length }}</span>
|
||||
</div>
|
||||
<Dropdown width="48" align="left">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@click="openSegmentModal"
|
||||
>
|
||||
Preusmeri v segment
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
Akcije
|
||||
<svg
|
||||
class="ml-1 h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@click="openSegmentModal"
|
||||
>
|
||||
Preusmeri v segment
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-select>
|
||||
|
|
@ -462,6 +617,110 @@ function formatDate(value) {
|
|||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- Export Dialog Modal -->
|
||||
<DialogModal
|
||||
:show="exportDialogOpen"
|
||||
max-width="3xl"
|
||||
@close="closeExportDialog"
|
||||
>
|
||||
<template #title>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Izvoz v Excel</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
Izberi stolpce in obseg podatkov za izvoz.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form
|
||||
id="contract-export-form"
|
||||
class="space-y-6"
|
||||
@submit.prevent="submitExport"
|
||||
>
|
||||
<div>
|
||||
<span class="text-sm font-semibold text-gray-700"
|
||||
>Obseg podatkov</span
|
||||
>
|
||||
<div class="mt-2 space-y-2">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="current"
|
||||
class="text-indigo-600"
|
||||
v-model="exportScope"
|
||||
/>
|
||||
Trenutna stran ({{ currentPageCount }} zapisov)
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="all"
|
||||
class="text-indigo-600"
|
||||
v-model="exportScope"
|
||||
/>
|
||||
Vse pogodbe ({{ totalContracts }} zapisov)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-gray-700">Stolpci</span>
|
||||
<label class="flex items-center gap-2 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allColumnsSelected"
|
||||
@change="toggleAllColumns($event.target.checked)"
|
||||
/>
|
||||
Označi vse
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label
|
||||
v-for="col in exportableColumns"
|
||||
:key="col.key"
|
||||
class="flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="columns[]"
|
||||
:value="col.key"
|
||||
v-model="exportColumns"
|
||||
class="text-indigo-600"
|
||||
/>
|
||||
{{ col.label }}
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="exportError" class="mt-2 text-sm text-red-600">
|
||||
{{ exportError }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-gray-600 hover:text-gray-900"
|
||||
@click="closeExportDialog"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="contract-export-form"
|
||||
class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
:disabled="exportDisabled"
|
||||
>
|
||||
<span v-if="!isExporting">Prenesi Excel</span>
|
||||
<span v-else>Pripravljam ...</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</div>
|
||||
<!-- Pagination handled by DataTableServer -->
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@
|
|||
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
||||
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
||||
Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts');
|
||||
Route::post('clients/{client:uuid}/contracts/export', [ClientController::class, 'exportContracts'])->name('client.contracts.export');
|
||||
|
||||
Route::middleware('permission:client-edit')->group(function () {
|
||||
Route::post('clients', [ClientController::class, 'store'])->name('client.store');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user