Compare commits
No commits in common. "f66bbbf8426f989de271b7c81c318ae31bcb7b84" and "c4d2f6e473897d99bdce1c9a3d45287ad206624c" have entirely different histories.
f66bbbf842
...
c4d2f6e473
|
|
@ -1,158 +0,0 @@
|
||||||
<?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,15 +2,11 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Exports\ClientContractsExport;
|
|
||||||
use App\Http\Requests\ExportClientContractsRequest;
|
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Services\ReferenceDataCache;
|
use App\Services\ReferenceDataCache;
|
||||||
use DB;
|
use DB;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
|
||||||
|
|
||||||
class ClientController extends Controller
|
class ClientController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -170,83 +166,6 @@ 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)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<?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'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,14 +2,9 @@
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { Link, router, usePage } from "@inertiajs/vue3";
|
import { Link, router, usePage } from "@inertiajs/vue3";
|
||||||
import axios from "axios";
|
|
||||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||||
import DialogModal from "@/Components/DialogModal.vue";
|
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Input } from "@/Components/ui/input";
|
import { Input } from "@/Components/ui/input";
|
||||||
import { Checkbox } from "@/Components/ui/checkbox";
|
|
||||||
import { Label } from "@/Components/ui/label";
|
|
||||||
import { Switch } from "@/Components/ui/switch";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -24,7 +19,7 @@ import DateRangePicker from "@/Components/DateRangePicker.vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { ButtonGroup } from "@/Components/ui/button-group";
|
import { ButtonGroup } from "@/Components/ui/button-group";
|
||||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||||
import { Filter, LinkIcon, FileDown } from "lucide-vue-next";
|
import { Filter, LinkIcon } from "lucide-vue-next";
|
||||||
import { Card } from "@/Components/ui/card";
|
import { Card } from "@/Components/ui/card";
|
||||||
import { Badge } from "@/Components/ui/badge";
|
import { Badge } from "@/Components/ui/badge";
|
||||||
import { hasPermission } from "@/Services/permissions";
|
import { hasPermission } from "@/Services/permissions";
|
||||||
|
|
@ -60,31 +55,6 @@ const selectedSegments = ref(
|
||||||
);
|
);
|
||||||
const filterPopoverOpen = ref(false);
|
const filterPopoverOpen = ref(false);
|
||||||
|
|
||||||
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 contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
|
|
||||||
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
|
||||||
const totalContracts = computed(() => props.contracts?.total ?? 0);
|
|
||||||
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
|
|
||||||
const allColumnsSelected = computed(
|
|
||||||
() => exportColumns.value.length === exportableColumns.length
|
|
||||||
);
|
|
||||||
const exportDisabled = computed(
|
|
||||||
() => exportColumns.value.length === 0 || isExporting.value
|
|
||||||
);
|
|
||||||
|
|
||||||
function applyDateFilter() {
|
function applyDateFilter() {
|
||||||
filterPopoverOpen.value = false;
|
filterPopoverOpen.value = false;
|
||||||
const params = Object.fromEntries(
|
const params = Object.fromEntries(
|
||||||
|
|
@ -154,139 +124,6 @@ function formatDate(value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAllColumns(checked) {
|
|
||||||
exportColumns.value = checked ? exportableColumns.map((col) => col.key) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleColumnToggle(key, checked) {
|
|
||||||
if (checked) {
|
|
||||||
if (!exportColumns.value.includes(key)) {
|
|
||||||
exportColumns.value = [...exportColumns.value, key];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exportColumns.value = exportColumns.value.filter((col) => col !== key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setExportScopeFromSwitch(checked) {
|
|
||||||
exportScope.value = checked ? "all" : "current";
|
|
||||||
}
|
|
||||||
|
|
||||||
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: dateRange.value?.start || "",
|
|
||||||
to: dateRange.value?.end || "",
|
|
||||||
search: search.value || "",
|
|
||||||
segments: selectedSegments.value.length > 0 ? selectedSegments.value.join(",") : "",
|
|
||||||
page: contractsCurrentPage.value,
|
|
||||||
per_page: contractsPerPage.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -383,103 +220,89 @@ function extractFilenameFromHeaders(headers) {
|
||||||
:show-toolbar="true"
|
:show-toolbar="true"
|
||||||
>
|
>
|
||||||
<template #toolbar-filters>
|
<template #toolbar-filters>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<AppPopover
|
||||||
<AppPopover
|
v-model:open="filterPopoverOpen"
|
||||||
v-model:open="filterPopoverOpen"
|
align="start"
|
||||||
align="start"
|
content-class="w-[400px]"
|
||||||
content-class="w-[400px]"
|
>
|
||||||
>
|
<template #trigger>
|
||||||
<template #trigger>
|
<Button variant="outline" size="sm" class="gap-2">
|
||||||
<Button variant="outline" size="sm" class="gap-2">
|
<Filter class="h-4 w-4" />
|
||||||
<Filter class="h-4 w-4" />
|
Filtri
|
||||||
Filtri
|
<span
|
||||||
<span
|
v-if="
|
||||||
v-if="
|
dateRange?.start || dateRange?.end || selectedSegments?.length
|
||||||
dateRange?.start || dateRange?.end || selectedSegments?.length
|
"
|
||||||
"
|
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
||||||
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
>
|
||||||
>
|
{{
|
||||||
{{
|
[
|
||||||
[
|
dateRange?.start || dateRange?.end ? 1 : 0,
|
||||||
dateRange?.start || dateRange?.end ? 1 : 0,
|
selectedSegments?.length ? 1 : 0,
|
||||||
selectedSegments?.length ? 1 : 0,
|
].reduce((a, b) => a + b, 0)
|
||||||
].reduce((a, b) => a + b, 0)
|
}}
|
||||||
}}
|
</span>
|
||||||
</span>
|
</Button>
|
||||||
</Button>
|
</template>
|
||||||
</template>
|
<div class="space-y-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-2">
|
||||||
<div class="space-y-2">
|
<h4 class="font-medium text-sm">Filtri pogodb</h4>
|
||||||
<h4 class="font-medium text-sm">Filtri pogodb</h4>
|
<p class="text-sm text-muted-foreground">
|
||||||
<p class="text-sm text-muted-foreground">
|
Izberite filtre za prikaz pogodb
|
||||||
Izberite filtre za prikaz pogodb
|
</p>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<InputLabel>Iskanje</InputLabel>
|
||||||
|
<Input
|
||||||
|
v-model="search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Išči po referenci, stranki..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-1.5">
|
||||||
<div class="space-y-1.5">
|
<InputLabel>Datumska območja</InputLabel>
|
||||||
<InputLabel>Iskanje</InputLabel>
|
<DateRangePicker
|
||||||
<Input
|
v-model="dateRange"
|
||||||
v-model="search"
|
format="dd.MM.yyyy"
|
||||||
type="text"
|
placeholder="Izberi datumska območja"
|
||||||
placeholder="Išči po referenci, stranki..."
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div class="space-y-1.5">
|
||||||
<div class="space-y-1.5">
|
<InputLabel>Segmenti</InputLabel>
|
||||||
<InputLabel>Datumska območja</InputLabel>
|
<AppMultiSelect
|
||||||
<DateRangePicker
|
v-model="selectedSegments"
|
||||||
v-model="dateRange"
|
:items="
|
||||||
format="dd.MM.yyyy"
|
segments.map((s) => ({ value: String(s.id), label: s.name }))
|
||||||
placeholder="Izberi datumska območja"
|
"
|
||||||
/>
|
placeholder="Vsi segmenti"
|
||||||
</div>
|
search-placeholder="Išči segment..."
|
||||||
<div class="space-y-1.5">
|
empty-text="Ni segmentov"
|
||||||
<InputLabel>Segmenti</InputLabel>
|
chip-variant="secondary"
|
||||||
<AppMultiSelect
|
/>
|
||||||
v-model="selectedSegments"
|
</div>
|
||||||
:items="
|
<div class="flex justify-end gap-2 pt-2 border-t">
|
||||||
segments.map((s) => ({
|
<Button
|
||||||
value: String(s.id),
|
type="button"
|
||||||
label: s.name,
|
variant="outline"
|
||||||
}))
|
size="sm"
|
||||||
"
|
:disabled="
|
||||||
placeholder="Vsi segmenti"
|
!dateRange?.start &&
|
||||||
search-placeholder="Išči segment..."
|
!dateRange?.end &&
|
||||||
empty-text="Ni segmentov"
|
selectedSegments.length === 0 &&
|
||||||
chip-variant="secondary"
|
search === ''
|
||||||
/>
|
"
|
||||||
</div>
|
@click="clearDateFilter"
|
||||||
<div class="flex justify-end gap-2 pt-2 border-t">
|
>
|
||||||
<Button
|
Počisti
|
||||||
type="button"
|
</Button>
|
||||||
variant="outline"
|
<Button type="button" size="sm" @click="applyDateFilter">
|
||||||
size="sm"
|
Uporabi
|
||||||
:disabled="
|
</Button>
|
||||||
!dateRange?.start &&
|
|
||||||
!dateRange?.end &&
|
|
||||||
selectedSegments.length === 0 &&
|
|
||||||
search === ''
|
|
||||||
"
|
|
||||||
@click="clearDateFilter"
|
|
||||||
>
|
|
||||||
Počisti
|
|
||||||
</Button>
|
|
||||||
<Button type="button" size="sm" @click="applyDateFilter">
|
|
||||||
Uporabi
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppPopover>
|
</div>
|
||||||
<Button
|
</AppPopover>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="gap-2"
|
|
||||||
@click="openExportDialog"
|
|
||||||
>
|
|
||||||
<FileDown class="h-4 w-4" />
|
|
||||||
Izvozi v Excel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #cell-reference="{ row }">
|
<template #cell-reference="{ row }">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -514,112 +337,5 @@ function extractFilenameFromHeaders(headers) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
|
||||||
<template #title>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-lg font-semibold leading-6 text-foreground">Izvoz v Excel</h3>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Izberi stolpce in obseg podatkov za izvoz.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<form id="contract-export-form" class="space-y-5" @submit.prevent="submitExport">
|
|
||||||
<div class="space-y-3 rounded-lg border bg-muted/40 p-4">
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-foreground">Obseg podatkov</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Preklopi, ali izvoziš samo trenutni pogled ali vse pogodbe.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-3 rounded-md bg-background px-3 py-2 shadow-sm"
|
|
||||||
>
|
|
||||||
<span class="text-xs font-medium text-muted-foreground">Stran</span>
|
|
||||||
<Switch
|
|
||||||
:model-value="exportScope === 'all'"
|
|
||||||
@update:modelValue="setExportScopeFromSwitch"
|
|
||||||
aria-label="Preklopi obseg izvoza"
|
|
||||||
/>
|
|
||||||
<span class="text-xs font-medium text-muted-foreground">Vse</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
|
||||||
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
|
||||||
<p class="text-sm font-semibold text-foreground">Trenutna stran</p>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
{{ currentPageCount }} zapisov
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
|
||||||
<p class="text-sm font-semibold text-foreground">Vse pogodbe</p>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ totalContracts }} zapisov</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4 rounded-lg border bg-muted/40 p-4">
|
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-foreground">Stolpci</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Izberi, katere stolpce želiš vključiti v izvoz.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="export-columns-all"
|
|
||||||
:model-value="allColumnsSelected"
|
|
||||||
@update:modelValue="toggleAllColumns"
|
|
||||||
aria-label="Označi vse stolpce"
|
|
||||||
/>
|
|
||||||
<Label for="export-columns-all" class="text-sm text-muted-foreground">
|
|
||||||
Označi vse
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
|
||||||
<label
|
|
||||||
v-for="col in exportableColumns"
|
|
||||||
:key="col.key"
|
|
||||||
class="flex items-start gap-3 rounded-lg border bg-background px-3 py-3 text-sm shadow-sm transition hover:border-primary/40"
|
|
||||||
:for="`export-col-${col.key}`"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
:id="`export-col-${col.key}`"
|
|
||||||
:model-value="exportColumns.includes(col.key)"
|
|
||||||
:value="col.key"
|
|
||||||
@update:modelValue="(checked) => handleColumnToggle(col.key, checked)"
|
|
||||||
class="mt-0.5"
|
|
||||||
/>
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<p class="font-medium text-foreground">{{ col.label }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground">Vključi stolpec v datoteko.</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p v-if="exportError" class="text-sm text-destructive">{{ exportError }}</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<Button type="button" variant="ghost" @click="closeExportDialog">
|
|
||||||
Prekliči
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
form="contract-export-form"
|
|
||||||
:disabled="exportDisabled"
|
|
||||||
class="gap-2"
|
|
||||||
>
|
|
||||||
<span v-if="!isExporting">Prenesi Excel</span>
|
|
||||||
<span v-else>Pripravljam ...</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</DialogModal>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,6 @@
|
||||||
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
||||||
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
||||||
Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts');
|
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::middleware('permission:client-edit')->group(function () {
|
||||||
Route::post('clients', [ClientController::class, 'store'])->name('client.store');
|
Route::post('clients', [ClientController::class, 'store'])->name('client.store');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user