This commit is contained in:
2026-07-21 23:39:19 +02:00
commit b33b217bdb
217 changed files with 32076 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
@props(['field', 'wireModel'])
<div class="field">
<label>{{ $field->label }}{{ $field->required ? ' *' : '' }}</label>
@switch($field->type)
@case('text')
<input class="input" wire:model="{{ $wireModel }}.{{ $field->id }}">
@break
@case('textarea')
<textarea class="input" wire:model="{{ $wireModel }}.{{ $field->id }}"></textarea>
@break
@case('number')
<input class="input" type="number" wire:model="{{ $wireModel }}.{{ $field->id }}">
@break
@case('date')
<input class="input" type="date" wire:model="{{ $wireModel }}.{{ $field->id }}">
@break
@case('select')
<select class="input" wire:model="{{ $wireModel }}.{{ $field->id }}">
<option value="">Wybierz</option>
@foreach ($field->options ?? [] as $option)
<option value="{{ $option }}">{{ $option }}</option>
@endforeach
</select>
@break
@case('checkbox')
<label class="radio">
<input type="checkbox" wire:model="{{ $wireModel }}.{{ $field->id }}" style="position:static;opacity:1;width:auto;height:auto">
Tak
</label>
@break
@endswitch
</div>

View File

@@ -0,0 +1,17 @@
@props(['html' => '', 'type' => 'info'])
{{--
"ql-editor" is included alongside our own classes purely so Quill's own
CSS (quill.snow.css, loaded in layouts/app.blade.php) applies here too
it's what makes alignment, indent, headings, blockquote and code-block
formatting saved from the editor actually render outside of it (those
are plain classes/tags in the stored HTML, and several of Quill's rules
for them are scoped under .ql-editor). See app.css for the min-height /
overflow reset that undoes the parts of that scoping meant for the live,
editable textarea rather than static output like this.
--}}
@if (trim(strip_tags($html)) !== '')
<div {{ $attributes->merge(['class' => 'login-notice login-notice-'.$type.' ql-editor']) }}>
{!! $html !!}
</div>
@endif

View File

@@ -0,0 +1,9 @@
@props(['attachment'])
<a
href="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($attachment->path) }}"
target="_blank"
style="display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:5px 10px;border:1px solid var(--color-divider);border-radius:6px;font-size:12.5px;color:inherit;text-decoration:none;background:color-mix(in srgb, var(--color-text) 5%, transparent)"
>
<span class="material-symbols-outlined" style="font-size:15px">attach_file</span>{{ $attachment->original_name }}
</a>

View File

@@ -0,0 +1,40 @@
@props(['options', 'selectedIds', 'toggleAction', 'placeholder' => 'Wybierz…'])
@php
$selected = collect($options)->filter(fn ($o) => in_array($o['id'], $selectedIds, true))->values();
$available = collect($options)->filter(fn ($o) => ! in_array($o['id'], $selectedIds, true))->values();
@endphp
<div x-data="{ open: false }" style="position:relative">
<div
@click="open = ! open"
style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;border:1px solid var(--color-divider);border-radius:7px;padding:7px 8px;min-height:38px;cursor:pointer;background:var(--color-bg)"
>
@forelse ($selected as $opt)
<span class="tag tag-accent" style="display:inline-flex;align-items:center;gap:5px">
{{ $opt['label'] }}
<span
class="material-symbols-outlined"
style="font-size:14px;cursor:pointer"
wire:click.stop="{{ $toggleAction }}({{ $opt['id'] }})"
>close</span>
</span>
@empty
<span class="text-muted" style="font-size:13px">{{ $placeholder }}</span>
@endforelse
<span class="material-symbols-outlined" style="font-size:18px;margin-left:auto;opacity:0.6">expand_more</span>
</div>
<div
x-show="open"
x-cloak
@click.outside="open = false"
style="position:absolute;top:100%;left:0;right:0;margin-top:4px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);max-height:220px;overflow:auto;z-index:30"
>
@forelse ($available as $opt)
<button type="button" class="theme-toggle-option" wire:click="{{ $toggleAction }}({{ $opt['id'] }})">{{ $opt['label'] }}</button>
@empty
<div class="text-muted" style="padding:8px 12px;font-size:12.5px">Brak więcej opcji do dodania.</div>
@endforelse
</div>
</div>

View File

@@ -0,0 +1,53 @@
@php
$user = auth()->user();
$areas = [];
if ($user) {
if ($user->isClient()) {
$areas[] = ['label' => 'Panel Klienta', 'url' => route('client.dashboard'), 'active' => request()->routeIs('client.*')];
}
if ($user->isOperator()) {
$areas[] = ['label' => 'Panel Operatora', 'url' => route('operator.queue'), 'active' => request()->routeIs('operator.*')];
}
if ($user->isAdmin()) {
$areas[] = ['label' => 'Panel Administratora', 'url' => route('admin.panel'), 'active' => request()->routeIs('admin.*')];
}
}
@endphp
@if ($user)
<div x-data="{ open: false }" @click.outside="open = false" style="position:relative;display:inline-block">
<button type="button" class="btn btn-secondary" @click="open = !open" style="display:flex;align-items:center;gap:6px">
<span class="material-symbols-outlined" style="font-size:18px">account_circle</span>
<span class="profile-menu-name">{{ $user->name }}</span>
<span class="material-symbols-outlined" style="font-size:16px">expand_more</span>
</button>
<div
x-show="open"
x-cloak
style="position:absolute;top:100%;right:0;margin-top:6px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);min-width:200px;overflow:hidden;z-index:30"
>
@foreach ($areas as $area)
<a
href="{{ $area['url'] }}"
wire:navigate
@click="open = false"
class="theme-toggle-option"
style="text-decoration:none;color:{{ $area['active'] ? 'var(--color-accent)' : 'var(--color-text)' }};font-size:12.5px"
>{{ $area['label'] }}</a>
@endforeach
@if (count($areas))
<div style="border-top:1px solid var(--color-divider)"></div>
@endif
<a
href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('profile-menu-logout-form').submit();"
class="theme-toggle-option"
style="text-decoration:none;color:var(--color-danger);font-size:12.5px"
>Wyloguj się</a>
<form id="profile-menu-logout-form" action="{{ route('logout') }}" method="POST" style="display:none">@csrf</form>
</div>
</div>
@endif

View File

@@ -0,0 +1,74 @@
@props(['value' => '', 'onChange', 'minHeight' => '160px'])
{{--
A QuillJS rich-text editor whose HTML is pushed back to the Livewire
component via the given method name, debounced so typing doesn't fire a
request per keystroke. wire:ignore keeps Livewire from touching Quill's
own DOM once it's mounted either the wrapping @if in the parent view
(opening a dialog) or a changing wire:key passed in via $attributes (e.g.
after a server-side "Resetuj" reset) is what gives us a fresh node and
therefore a fresh x-init reading the new $value when the content changes
from outside the editor itself.
--}}
<div
{{ $attributes }}
wire:ignore
x-data="{
quill: null,
timer: null,
init() {
// Quill has no built-in horizontal-rule button — register a
// custom block-embed format for it once (guarded since every
// editor instance on the page runs this same init). Community
// pattern: https://github.com/quilljs/quill/issues/1211
if (! window.quillDividerRegistered) {
const BlockEmbed = Quill.import('blots/block/embed');
class DividerBlot extends BlockEmbed {}
DividerBlot.blotName = 'divider';
DividerBlot.tagName = 'hr';
Quill.register(DividerBlot);
Quill.import('ui/icons').divider = `<svg viewBox='0 0 18 18'><line class='ql-stroke' x1='3' y1='9' x2='15' y2='9'></line></svg>`;
window.quillDividerRegistered = true;
}
this.quill = new Quill(this.$refs.editor, {
theme: 'snow',
placeholder: 'Treść…',
modules: {
toolbar: {
container: [
[{ header: [2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ align: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ script: 'sub' }, { script: 'super' }],
['blockquote', 'code-block'],
['divider'],
['link'],
['clean'],
],
handlers: {
divider() {
const range = this.quill.getSelection(true);
this.quill.insertText(range.index, '\n', Quill.sources.USER);
this.quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
this.quill.setSelection(range.index + 2, 0, Quill.sources.SILENT);
},
},
},
},
});
this.quill.root.innerHTML = @js($value);
this.quill.on('text-change', () => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
$wire.call(@js($onChange), this.quill.root.innerHTML);
}, 500);
});
},
}"
>
<div x-ref="editor" style="min-height: {{ $minHeight }}"></div>
</div>

View File

@@ -0,0 +1,35 @@
<div
x-data="{
theme: localStorage.getItem('theme') || 'auto',
open: false,
icon() { return this.theme === 'light' ? 'light_mode' : this.theme === 'dark' ? 'dark_mode' : 'brightness_auto' },
apply(t) {
this.theme = t
localStorage.setItem('theme', t)
let systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
document.documentElement.setAttribute('data-theme', t === 'auto' ? (systemDark ? 'dark' : 'light') : t)
this.open = false
}
}"
@click.outside="open = false"
style="position:relative;display:inline-block"
>
<button type="button" class="btn btn-secondary btn-icon" @click="open = !open">
<span class="material-symbols-outlined" x-text="icon()"></span>
</button>
<div
x-show="open"
x-cloak
style="position:absolute;top:100%;right:0;margin-top:6px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);min-width:150px;overflow:hidden;z-index:20"
>
<button type="button" class="theme-toggle-option" @click="apply('light')">
<span class="material-symbols-outlined" style="font-size:16px">light_mode</span>Jasny
</button>
<button type="button" class="theme-toggle-option" @click="apply('dark')">
<span class="material-symbols-outlined" style="font-size:16px">dark_mode</span>Ciemny
</button>
<button type="button" class="theme-toggle-option" @click="apply('auto')">
<span class="material-symbols-outlined" style="font-size:16px">brightness_auto</span>Auto
</button>
</div>
</div>

View File

@@ -0,0 +1,22 @@
@php
$panelLabel = match (true) {
request()->routeIs('client.*') => 'Panel Klienta',
request()->routeIs('operator.*') => 'Panel Operatora',
request()->routeIs('admin.*') => 'Panel Administratora',
default => null,
};
@endphp
<div class="nav" style="position:relative;padding:16px 28px;border-bottom:1px solid var(--color-divider)">
<span class="nav-brand">{{ \App\Support\Settings::get('company_name') }}</span>
@if ($panelLabel)
<span class="nav-panel-label" style="position:absolute;left:50%;top:50%;transform:translate(-50%, -50%);font-weight:500;font-size:13.5px;white-space:nowrap">{{ $panelLabel }}</span>
@endif
<x-theme-toggle />
{{ $slot }}
<x-profile-menu />
</div>