From dc41862afc55870ef9bb061213cc9258d935bc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Sat, 10 Jan 2026 20:36:32 +0100 Subject: [PATCH] Client contracts view added excel export option --- app/Exports/ClientContractsExport.php | 158 +++++++++ app/Http/Controllers/ClientController.php | 81 +++++ .../Requests/ExportClientContractsRequest.php | 43 +++ resources/js/Pages/Client/Contracts.vue | 329 ++++++++++++++++-- routes/web.php | 1 + 5 files changed, 577 insertions(+), 35 deletions(-) create mode 100644 app/Exports/ClientContractsExport.php create mode 100644 app/Http/Requests/ExportClientContractsRequest.php diff --git a/app/Exports/ClientContractsExport.php b/app/Exports/ClientContractsExport.php new file mode 100644 index 0000000..2153f74 --- /dev/null +++ b/app/Exports/ClientContractsExport.php @@ -0,0 +1,158 @@ + + */ + private array $columnLetterMap = []; + + /** + * @var array + */ + public const COLUMN_METADATA = [ + 'reference' => ['label' => 'Referenca'], + 'customer' => ['label' => 'Stranka'], + 'start' => ['label' => 'Začetek'], + 'segment' => ['label' => 'Segment'], + 'balance' => ['label' => 'Stanje'], + ]; + + /** + * @param array $columns + */ + public function __construct(private Builder $query, private array $columns) {} + + /** + * @return array + */ + 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 + */ + public function map($row): array + { + return array_map(fn (string $column) => $this->resolveValue($row, $column), $this->columns); + } + + /** + * @return array + */ + public function headings(): array + { + return array_map(fn (string $column) => self::columnLabel($column), $this->columns); + } + + /** + * @return array + */ + 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 + */ + 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); + } +} diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 2e5a528..4ccccbb 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -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) { diff --git a/app/Http/Requests/ExportClientContractsRequest.php b/app/Http/Requests/ExportClientContractsRequest.php new file mode 100644 index 0000000..50397c4 --- /dev/null +++ b/app/Http/Requests/ExportClientContractsRequest.php @@ -0,0 +1,43 @@ +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'), + ]); + } +} diff --git a/resources/js/Pages/Client/Contracts.vue b/resources/js/Pages/Client/Contracts.vue index 39a65bc..ea74a0b 100644 --- a/resources/js/Pages/Client/Contracts.vue +++ b/resources/js/Pages/Client/Contracts.vue @@ -1,7 +1,8 @@