production #1
BIN
app/Http/Controllers/ClientCaseContoller.original
Normal file
BIN
app/Http/Controllers/ClientCaseContoller.original
Normal file
Binary file not shown.
|
|
@ -4,15 +4,18 @@
|
||||||
|
|
||||||
use App\Http\Requests\StoreContractRequest;
|
use App\Http\Requests\StoreContractRequest;
|
||||||
use App\Http\Requests\UpdateContractRequest;
|
use App\Http\Requests\UpdateContractRequest;
|
||||||
|
use App\Models\Client;
|
||||||
use App\Models\ClientCase;
|
use App\Models\ClientCase;
|
||||||
use App\Models\Contract;
|
use App\Models\Contract;
|
||||||
use App\Models\Document;
|
use App\Models\Document;
|
||||||
|
use App\Models\Segment;
|
||||||
use App\Services\Documents\DocumentStreamService;
|
use App\Services\Documents\DocumentStreamService;
|
||||||
use App\Services\ReferenceDataCache;
|
use App\Services\ReferenceDataCache;
|
||||||
use App\Services\Sms\SmsService;
|
use App\Services\Sms\SmsService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
|
@ -30,6 +33,16 @@ public function __construct(
|
||||||
public function index(ClientCase $clientCase, Request $request)
|
public function index(ClientCase $clientCase, Request $request)
|
||||||
{
|
{
|
||||||
$search = $request->input('search');
|
$search = $request->input('search');
|
||||||
|
$from = $this->normalizeDate($request->input('from'));
|
||||||
|
$to = $this->normalizeDate($request->input('to'));
|
||||||
|
$clientFilter = collect(explode(',', (string) $request->input('clients')))
|
||||||
|
->filter()
|
||||||
|
->map(fn ($value) => (int) $value)
|
||||||
|
->filter(fn ($value) => $value > 0)
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$perPage = $this->resolvePerPage($request);
|
||||||
|
|
||||||
$query = $clientCase::query()
|
$query = $clientCase::query()
|
||||||
->select('client_cases.*')
|
->select('client_cases.*')
|
||||||
|
|
@ -39,7 +52,6 @@ public function index(ClientCase $clientCase, Request $request)
|
||||||
->groupBy('client_cases.id');
|
->groupBy('client_cases.id');
|
||||||
})
|
})
|
||||||
->where('client_cases.active', 1)
|
->where('client_cases.active', 1)
|
||||||
// Use LEFT JOINs for aggregated data to avoid subqueries
|
|
||||||
->leftJoin('contracts', function ($join) {
|
->leftJoin('contracts', function ($join) {
|
||||||
$join->on('contracts.client_case_id', '=', 'client_cases.id')
|
$join->on('contracts.client_case_id', '=', 'client_cases.id')
|
||||||
->whereNull('contracts.deleted_at');
|
->whereNull('contracts.deleted_at');
|
||||||
|
|
@ -49,11 +61,18 @@ public function index(ClientCase $clientCase, Request $request)
|
||||||
->where('contract_segment.active', true);
|
->where('contract_segment.active', true);
|
||||||
})
|
})
|
||||||
->leftJoin('accounts', 'accounts.contract_id', '=', 'contracts.id')
|
->leftJoin('accounts', 'accounts.contract_id', '=', 'contracts.id')
|
||||||
|
->when($clientFilter->isNotEmpty(), function ($que) use ($clientFilter) {
|
||||||
|
$que->whereIn('client_cases.client_id', $clientFilter->all());
|
||||||
|
})
|
||||||
|
->when($from, function ($que) use ($from) {
|
||||||
|
$que->whereDate('client_cases.created_at', '>=', $from);
|
||||||
|
})
|
||||||
|
->when($to, function ($que) use ($to) {
|
||||||
|
$que->whereDate('client_cases.created_at', '<=', $to);
|
||||||
|
})
|
||||||
->groupBy('client_cases.id')
|
->groupBy('client_cases.id')
|
||||||
->addSelect([
|
->addSelect([
|
||||||
// Count of active contracts (a contract is considered active if it has an active pivot in contract_segment)
|
|
||||||
\DB::raw('COUNT(DISTINCT CASE WHEN contract_segment.id IS NOT NULL THEN contracts.id END) as active_contracts_count'),
|
\DB::raw('COUNT(DISTINCT CASE WHEN contract_segment.id IS NOT NULL THEN contracts.id END) as active_contracts_count'),
|
||||||
// Sum of balances for accounts of active contracts
|
|
||||||
\DB::raw('COALESCE(SUM(CASE WHEN contract_segment.id IS NOT NULL THEN accounts.balance_amount END), 0) as active_contracts_balance_sum'),
|
\DB::raw('COALESCE(SUM(CASE WHEN contract_segment.id IS NOT NULL THEN accounts.balance_amount END), 0) as active_contracts_balance_sum'),
|
||||||
])
|
])
|
||||||
->with(['person.client', 'client.person'])
|
->with(['person.client', 'client.person'])
|
||||||
|
|
@ -61,12 +80,49 @@ public function index(ClientCase $clientCase, Request $request)
|
||||||
|
|
||||||
return Inertia::render('Cases/Index', [
|
return Inertia::render('Cases/Index', [
|
||||||
'client_cases' => $query
|
'client_cases' => $query
|
||||||
->paginate($request->integer('perPage', 15), ['*'], 'clientCasesPage')
|
->paginate($perPage, ['*'], 'clientCasesPage')
|
||||||
->withQueryString(),
|
->withQueryString(),
|
||||||
'filters' => $request->only(['search']),
|
'filters' => [
|
||||||
|
'search' => $search,
|
||||||
|
'from' => $from,
|
||||||
|
'to' => $to,
|
||||||
|
'clients' => $clientFilter->map(fn ($value) => (string) $value)->all(),
|
||||||
|
'perPage' => $perPage,
|
||||||
|
],
|
||||||
|
'clients' => Client::query()
|
||||||
|
->select(['clients.id', 'person.full_name as name'])
|
||||||
|
->join('person', 'person.id', '=', 'clients.person_id')
|
||||||
|
->orderBy('person.full_name')
|
||||||
|
->get()
|
||||||
|
->map(fn ($client) => [
|
||||||
|
'id' => (int) $client->id,
|
||||||
|
'name' => (string) ($client->name ?? ''),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolvePerPage(Request $request): int
|
||||||
|
{
|
||||||
|
$allowed = [10, 15, 25, 50, 100];
|
||||||
|
|
||||||
|
$perPage = (int) $request->integer('perPage', 15);
|
||||||
|
|
||||||
|
return in_array($perPage, $allowed, true) ? $perPage : 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDate(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (! $value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::parse($value)->toDateString();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new resource.
|
* Show the form for creating a new resource.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1031,7 +1087,7 @@ public function emergencyCreatePerson(ClientCase $clientCase, Request $request)
|
||||||
if ($existing && ! $existing->trashed()) {
|
if ($existing && ! $existing->trashed()) {
|
||||||
return back()->with('flash', [
|
return back()->with('flash', [
|
||||||
'type' => 'info',
|
'type' => 'info',
|
||||||
'message' => 'Person already exists – emergency creation not needed.',
|
'message' => 'Person already exists ÔÇô emergency creation not needed.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1136,10 +1192,10 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
if (! empty($validated['sender_id'])) {
|
if (! empty($validated['sender_id'])) {
|
||||||
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
||||||
if (! $sender) {
|
if (! $sender) {
|
||||||
return back()->with('error', 'Izbran pošiljatelj ne obstaja.');
|
return back()->with('error', 'Izbran pošiljatelj ne obstaja.');
|
||||||
}
|
}
|
||||||
if ($profile && (int) $sender->profile_id !== (int) $profile->id) {
|
if ($profile && (int) $sender->profile_id !== (int) $profile->id) {
|
||||||
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (! $profile) {
|
if (! $profile) {
|
||||||
|
|
@ -1182,7 +1238,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an activity before sending
|
// Create an activity before sending
|
||||||
$activityNote = sprintf('Št: %s | Telo: %s', (string) $phone->nu, (string) $validated['message']);
|
$activityNote = sprintf('Št: %s | Telo: %s', (string) $phone->nu, (string) $validated['message']);
|
||||||
$activityData = [
|
$activityData = [
|
||||||
'note' => $activityNote,
|
'note' => $activityNote,
|
||||||
'user_id' => optional($request->user())->id,
|
'user_id' => optional($request->user())->id,
|
||||||
|
|
@ -1220,7 +1276,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
activityId: $activity?->id,
|
activityId: $activity?->id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return back()->with('success', 'SMS je bil dodan v čakalno vrsto.');
|
return back()->with('success', 'SMS je bil dodan v ─Źakalno vrsto.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::warning('SMS enqueue failed', [
|
\Log::warning('SMS enqueue failed', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
|
@ -1228,7 +1284,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
'phone_id' => $phone_id,
|
'phone_id' => $phone_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return back()->with('error', 'SMS ni bil dodan v čakalno vrsto.');
|
return back()->with('error', 'SMS ni bil dodan v ─Źakalno vrsto.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
304
package-lock.json
generated
304
package-lock.json
generated
|
|
@ -5,21 +5,21 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@internationalized/date": "^3.9.0",
|
"@internationalized/date": "^3.10.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@unovis/ts": "^1.6.2",
|
"@unovis/ts": "^1.6.2",
|
||||||
"@unovis/vue": "^1.6.2",
|
"@unovis/vue": "^1.6.2",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.3",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"apexcharts": "^4.0.0",
|
"apexcharts": "^4.7.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -28,37 +28,37 @@
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"preline": "^2.7.0",
|
"preline": "^2.7.0",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
"reka-ui": "^2.6.0",
|
"reka-ui": "^2.6.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-inner-border": "^0.2.0",
|
"tailwindcss-inner-border": "^0.2.0",
|
||||||
"v-calendar": "^3.1.2",
|
"v-calendar": "^3.1.2",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue-currency-input": "^3.2.1",
|
"vue-currency-input": "^3.2.1",
|
||||||
"vue-multiselect": "^3.1.0",
|
"vue-multiselect": "^3.4.0",
|
||||||
"vue-search-input": "^1.1.16",
|
"vue-search-input": "^1.1.19",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vue3-apexcharts": "^1.7.0",
|
"vue3-apexcharts": "^1.10.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inertiajs/vue3": "2.0",
|
"@inertiajs/vue3": "2.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.22",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.13.2",
|
||||||
"laravel-vite-plugin": "^2.0.1",
|
"laravel-vite-plugin": "^2.0.1",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.2.7",
|
||||||
"vue": "^3.3.13",
|
"vue": "^3.3.13",
|
||||||
"vue-tsc": "^3.1.5"
|
"vue-tsc": "^3.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -1472,9 +1472,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||||
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
|
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1484,37 +1484,37 @@
|
||||||
"lightningcss": "1.30.2",
|
"lightningcss": "1.30.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.1.17"
|
"tailwindcss": "4.1.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||||
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
|
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.1.17",
|
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
|
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.1.17",
|
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
|
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||||
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
|
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1529,9 +1529,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||||
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
|
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1546,9 +1546,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||||
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
|
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1563,9 +1563,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||||
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
|
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1580,9 +1580,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||||
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
|
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1597,9 +1597,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||||
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
|
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1614,9 +1614,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||||
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
|
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1631,9 +1631,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||||
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
|
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1648,9 +1648,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||||
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
|
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1665,9 +1665,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||||
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
|
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
|
|
@ -1683,10 +1683,10 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "^1.6.0",
|
"@emnapi/core": "^1.7.1",
|
||||||
"@emnapi/runtime": "^1.6.0",
|
"@emnapi/runtime": "^1.7.1",
|
||||||
"@emnapi/wasi-threads": "^1.1.0",
|
"@emnapi/wasi-threads": "^1.1.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
"@tybys/wasm-util": "^0.10.1",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1695,7 +1695,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1706,7 +1706,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1726,14 +1726,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "^1.5.0",
|
"@emnapi/core": "^1.7.1",
|
||||||
"@emnapi/runtime": "^1.5.0",
|
"@emnapi/runtime": "^1.7.1",
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1755,9 +1755,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||||
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
|
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1772,9 +1772,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||||
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
|
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1789,17 +1789,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/postcss": {
|
"node_modules/@tailwindcss/postcss": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
|
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"@tailwindcss/node": "4.1.17",
|
"@tailwindcss/node": "4.1.18",
|
||||||
"@tailwindcss/oxide": "4.1.17",
|
"@tailwindcss/oxide": "4.1.18",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"tailwindcss": "4.1.17"
|
"tailwindcss": "4.1.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
|
@ -1829,9 +1829,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/virtual-core": {
|
"node_modules/@tanstack/virtual-core": {
|
||||||
"version": "3.13.12",
|
"version": "3.13.13",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz",
|
||||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
"integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -1858,12 +1858,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/vue-virtual": {
|
"node_modules/@tanstack/vue-virtual": {
|
||||||
"version": "3.13.12",
|
"version": "3.13.13",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.13.tgz",
|
||||||
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
|
"integrity": "sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/virtual-core": "3.13.12"
|
"@tanstack/virtual-core": "3.13.13"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -2208,9 +2208,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2414,30 +2414,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.26",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz",
|
||||||
"integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
|
"integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/source-map": "2.4.23"
|
"@volar/source-map": "2.4.26"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/source-map": {
|
"node_modules/@volar/source-map": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.26",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz",
|
||||||
"integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
|
"integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@volar/typescript": {
|
"node_modules/@volar/typescript": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.26",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz",
|
||||||
"integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
|
"integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.4.23",
|
"@volar/language-core": "2.4.26",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"vscode-uri": "^3.0.8"
|
"vscode-uri": "^3.0.8"
|
||||||
}
|
}
|
||||||
|
|
@ -2526,13 +2526,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.8.tgz",
|
||||||
"integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==",
|
"integrity": "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.4.23",
|
"@volar/language-core": "2.4.26",
|
||||||
"@vue/compiler-dom": "^3.5.0",
|
"@vue/compiler-dom": "^3.5.0",
|
||||||
"@vue/shared": "^3.5.0",
|
"@vue/shared": "^3.5.0",
|
||||||
"alien-signals": "^3.0.0",
|
"alien-signals": "^3.0.0",
|
||||||
|
|
@ -2764,9 +2764,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.32",
|
"version": "2.9.6",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
|
||||||
"integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==",
|
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -2774,18 +2774,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "2.8.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
"integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==",
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.0",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -2803,11 +2803,11 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
"electron-to-chromium": "^1.5.249",
|
"electron-to-chromium": "^1.5.263",
|
||||||
"node-releases": "^2.0.27",
|
"node-releases": "^2.0.27",
|
||||||
"update-browserslist-db": "^1.1.4"
|
"update-browserslist-db": "^1.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
|
|
@ -2873,9 +2873,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001757",
|
"version": "1.0.30001760",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
|
||||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -3627,9 +3627,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.263",
|
"version": "1.5.267",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||||
"integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==",
|
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
|
@ -5413,9 +5413,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
|
|
@ -5561,9 +5561,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -5655,9 +5655,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.2.6",
|
"version": "7.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -5849,14 +5849,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.8.tgz",
|
||||||
"integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==",
|
"integrity": "sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/typescript": "2.4.23",
|
"@volar/typescript": "2.4.26",
|
||||||
"@vue/language-core": "3.1.5"
|
"@vue/language-core": "3.1.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-tsc": "bin/vue-tsc.js"
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
|
|
||||||
50
package.json
50
package.json
|
|
@ -9,37 +9,37 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inertiajs/vue3": "2.0",
|
"@inertiajs/vue3": "2.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.22",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.13.2",
|
||||||
"laravel-vite-plugin": "^2.0.1",
|
"laravel-vite-plugin": "^2.0.1",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.2.7",
|
||||||
"vue": "^3.3.13",
|
"vue": "^3.3.13",
|
||||||
"vue-tsc": "^3.1.5"
|
"vue-tsc": "^3.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@internationalized/date": "^3.9.0",
|
"@internationalized/date": "^3.10.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@unovis/ts": "^1.6.2",
|
"@unovis/ts": "^1.6.2",
|
||||||
"@unovis/vue": "^1.6.2",
|
"@unovis/vue": "^1.6.2",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.3",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"apexcharts": "^4.0.0",
|
"apexcharts": "^4.7.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -48,17 +48,17 @@
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"preline": "^2.7.0",
|
"preline": "^2.7.0",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
"reka-ui": "^2.6.0",
|
"reka-ui": "^2.6.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-inner-border": "^0.2.0",
|
"tailwindcss-inner-border": "^0.2.0",
|
||||||
"v-calendar": "^3.1.2",
|
"v-calendar": "^3.1.2",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue-currency-input": "^3.2.1",
|
"vue-currency-input": "^3.2.1",
|
||||||
"vue-multiselect": "^3.1.0",
|
"vue-multiselect": "^3.4.0",
|
||||||
"vue-search-input": "^1.1.16",
|
"vue-search-input": "^1.1.19",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vue3-apexcharts": "^1.7.0",
|
"vue3-apexcharts": "^1.10.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -441,12 +441,18 @@ function doServerRequest(overrides = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = route(props.routeName, props.routeParams || {});
|
const url = route(props.routeName, props.routeParams || {});
|
||||||
router.get(url, q, {
|
const onlyProps = Array.isArray(props.onlyProps) ? props.onlyProps : [];
|
||||||
|
const inertiaOptions = {
|
||||||
preserveScroll: props.preserveScroll,
|
preserveScroll: props.preserveScroll,
|
||||||
preserveState: props.preserveState,
|
preserveState: props.preserveState,
|
||||||
replace: true,
|
replace: true,
|
||||||
only: props.onlyProps.length ? props.onlyProps : undefined,
|
};
|
||||||
});
|
|
||||||
|
if (onlyProps.length > 0) {
|
||||||
|
inertiaOptions.only = onlyProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(url, q, inertiaOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row key helper
|
// Row key helper
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref, watch, computed } from "vue";
|
import { onMounted, onUnmounted, ref, watch, computed, h } from "vue";
|
||||||
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
||||||
import ApplicationMark from "@/Components/ApplicationMark.vue";
|
import ApplicationMark from "@/Components/ApplicationMark.vue";
|
||||||
import Banner from "@/Components/Banner.vue";
|
import Banner from "@/Components/Banner.vue";
|
||||||
|
|
@ -23,6 +23,22 @@ import {
|
||||||
faMap,
|
faMap,
|
||||||
faGear,
|
faGear,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { MenuIcon } from "lucide-vue-next";
|
||||||
|
import { SearchIcon } from "lucide-vue-next";
|
||||||
|
import { ChevronDownIcon } from "lucide-vue-next";
|
||||||
|
import { LayoutDashboardIcon } from "lucide-vue-next";
|
||||||
|
import { FileChartColumn } from "lucide-vue-next";
|
||||||
|
import { UsersIcon } from "lucide-vue-next";
|
||||||
|
import { FoldersIcon } from "lucide-vue-next";
|
||||||
|
import { LayoutIcon } from "lucide-vue-next";
|
||||||
|
import { ImportIcon } from "lucide-vue-next";
|
||||||
|
import { FilePlusIcon } from "lucide-vue-next";
|
||||||
|
import { ListIndentIncreaseIcon } from "lucide-vue-next";
|
||||||
|
import { MapIcon } from "lucide-vue-next";
|
||||||
|
import { SettingsIcon } from "lucide-vue-next";
|
||||||
|
import { ShieldUserIcon } from "lucide-vue-next";
|
||||||
|
import { SmartphoneIcon } from "lucide-vue-next";
|
||||||
|
import { TabletSmartphoneIcon } from "lucide-vue-next";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -115,12 +131,14 @@ const rawMenuGroups = [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "dashboard",
|
key: "dashboard",
|
||||||
|
icon: LayoutDashboardIcon,
|
||||||
title: "Nadzorna plošča",
|
title: "Nadzorna plošča",
|
||||||
routeName: "dashboard",
|
routeName: "dashboard",
|
||||||
active: ["dashboard"],
|
active: ["dashboard"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "reports",
|
key: "reports",
|
||||||
|
icon: FileChartColumn,
|
||||||
title: "Poročila",
|
title: "Poročila",
|
||||||
routeName: "reports.index",
|
routeName: "reports.index",
|
||||||
active: ["reports.index", "reports.show"],
|
active: ["reports.index", "reports.show"],
|
||||||
|
|
@ -133,18 +151,21 @@ const rawMenuGroups = [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "clients",
|
key: "clients",
|
||||||
|
icon: UsersIcon,
|
||||||
title: "Naročniki",
|
title: "Naročniki",
|
||||||
routeName: "client",
|
routeName: "client",
|
||||||
active: ["client", "client.*"],
|
active: ["client", "client.*"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cases",
|
key: "cases",
|
||||||
|
icon: FoldersIcon,
|
||||||
title: "Primeri",
|
title: "Primeri",
|
||||||
routeName: "clientCase",
|
routeName: "clientCase",
|
||||||
active: ["clientCase", "clientCase.*"],
|
active: ["clientCase", "clientCase.*"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "segments",
|
key: "segments",
|
||||||
|
icon: LayoutIcon,
|
||||||
title: "Segmenti",
|
title: "Segmenti",
|
||||||
routeName: "segments.index",
|
routeName: "segments.index",
|
||||||
active: ["segments.index"],
|
active: ["segments.index"],
|
||||||
|
|
@ -157,18 +178,21 @@ const rawMenuGroups = [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "imports",
|
key: "imports",
|
||||||
|
icon: ImportIcon,
|
||||||
title: "Uvozi",
|
title: "Uvozi",
|
||||||
routeName: "imports.index",
|
routeName: "imports.index",
|
||||||
active: ["imports.index", "imports.*"],
|
active: ["imports.index", "imports.*"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "import-templates",
|
key: "import-templates",
|
||||||
|
icon: ListIndentIncreaseIcon,
|
||||||
title: "Uvozne predloge",
|
title: "Uvozne predloge",
|
||||||
routeName: "importTemplates.index",
|
routeName: "importTemplates.index",
|
||||||
active: ["importTemplates.index"],
|
active: ["importTemplates.index"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "import-templates-new",
|
key: "import-templates-new",
|
||||||
|
icon: FilePlusIcon,
|
||||||
title: "Nova uvozna predloga",
|
title: "Nova uvozna predloga",
|
||||||
routeName: "importTemplates.create",
|
routeName: "importTemplates.create",
|
||||||
active: ["importTemplates.create"],
|
active: ["importTemplates.create"],
|
||||||
|
|
@ -181,6 +205,7 @@ const rawMenuGroups = [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "fieldjobs",
|
key: "fieldjobs",
|
||||||
|
icon: MapIcon,
|
||||||
title: "Terenske naloge",
|
title: "Terenske naloge",
|
||||||
routeName: "fieldjobs.index",
|
routeName: "fieldjobs.index",
|
||||||
active: ["fieldjobs.index"],
|
active: ["fieldjobs.index"],
|
||||||
|
|
@ -195,6 +220,7 @@ const rawMenuGroups = [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "settings",
|
key: "settings",
|
||||||
|
icon: SettingsIcon,
|
||||||
title: "Nastavitve",
|
title: "Nastavitve",
|
||||||
routeName: "settings",
|
routeName: "settings",
|
||||||
active: ["settings", "settings.*"],
|
active: ["settings", "settings.*"],
|
||||||
|
|
@ -204,6 +230,7 @@ const rawMenuGroups = [
|
||||||
// We'll filter it out below if not authorized.
|
// We'll filter it out below if not authorized.
|
||||||
{
|
{
|
||||||
key: "admin-panel",
|
key: "admin-panel",
|
||||||
|
icon: ShieldUserIcon,
|
||||||
title: "Administrator",
|
title: "Administrator",
|
||||||
routeName: "admin.index",
|
routeName: "admin.index",
|
||||||
active: ["admin.index", "admin.users.index", "admin.permissions.create"],
|
active: ["admin.index", "admin.users.index", "admin.permissions.create"],
|
||||||
|
|
@ -247,21 +274,6 @@ const menuGroups = computed(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Icon map for menu keys -> FontAwesome icon definitions
|
|
||||||
const menuIconMap = {
|
|
||||||
dashboard: faGaugeHigh,
|
|
||||||
segments: faLayerGroup,
|
|
||||||
clients: faUserGroup,
|
|
||||||
cases: faFolderOpen,
|
|
||||||
imports: faFileImport,
|
|
||||||
"import-templates": faTableList,
|
|
||||||
"import-templates-new": faFileCirclePlus,
|
|
||||||
fieldjobs: faMap,
|
|
||||||
settings: faGear,
|
|
||||||
"admin-panel": faUserGroup,
|
|
||||||
reports: faTableList,
|
|
||||||
};
|
|
||||||
|
|
||||||
function isActive(patterns) {
|
function isActive(patterns) {
|
||||||
try {
|
try {
|
||||||
return patterns?.some((p) => route().current(p));
|
return patterns?.some((p) => route().current(p));
|
||||||
|
|
@ -289,7 +301,7 @@ function isActive(patterns) {
|
||||||
<aside
|
<aside
|
||||||
:class="[
|
:class="[
|
||||||
sidebarCollapsed ? 'w-16' : 'w-64',
|
sidebarCollapsed ? 'w-16' : 'w-64',
|
||||||
'bg-white border-r border-gray-200 transition-all duration-300 ease-in-out z-50',
|
'bg-sidebar text-sidebar-foreground border-r border-sidebar-border transition-all duration-300 ease-in-out z-50',
|
||||||
// Off-canvas behavior on mobile; sticky fixed-like sidebar on desktop
|
// Off-canvas behavior on mobile; sticky fixed-like sidebar on desktop
|
||||||
isMobile
|
isMobile
|
||||||
? 'fixed inset-y-0 left-0 transform shadow-strong ' +
|
? 'fixed inset-y-0 left-0 transform shadow-strong ' +
|
||||||
|
|
@ -297,7 +309,9 @@ function isActive(patterns) {
|
||||||
: 'sticky top-0 h-screen overflow-y-auto',
|
: 'sticky top-0 h-screen overflow-y-auto',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="h-16 px-4 flex items-center justify-between border-b border-gray-200 bg-white">
|
<div
|
||||||
|
class="h-16 px-4 flex items-center justify-between border-b border-sidebar-border bg-sidebar"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
:href="route('dashboard')"
|
:href="route('dashboard')"
|
||||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||||
|
|
@ -305,7 +319,7 @@ function isActive(patterns) {
|
||||||
<ApplicationMark />
|
<ApplicationMark />
|
||||||
<span
|
<span
|
||||||
v-if="!sidebarCollapsed"
|
v-if="!sidebarCollapsed"
|
||||||
class="text-sm font-semibold text-gray-900 transition-opacity"
|
class="text-sm font-semibold text-sidebar-foreground transition-opacity"
|
||||||
>
|
>
|
||||||
Teren
|
Teren
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -316,7 +330,7 @@ function isActive(patterns) {
|
||||||
<li v-for="group in menuGroups" :key="group.label">
|
<li v-for="group in menuGroups" :key="group.label">
|
||||||
<div
|
<div
|
||||||
v-if="!sidebarCollapsed"
|
v-if="!sidebarCollapsed"
|
||||||
class="px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-gray-400"
|
class="px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-sidebar-foreground/60"
|
||||||
>
|
>
|
||||||
{{ group.label }}
|
{{ group.label }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,19 +341,15 @@ function isActive(patterns) {
|
||||||
:class="[
|
:class="[
|
||||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||||
isActive(item.active)
|
isActive(item.active)
|
||||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||||
]"
|
]"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
>
|
>
|
||||||
<!-- Unified FontAwesome icon rendering -->
|
<component
|
||||||
<FontAwesomeIcon
|
v-if="item.icon"
|
||||||
v-if="menuIconMap[item.key]"
|
:is="item.icon"
|
||||||
:icon="menuIconMap[item.key]"
|
class="w-5 h-5 shrink-0 transition-colors"
|
||||||
:class="[
|
|
||||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
|
||||||
isActive(item.active) ? 'text-primary-600' : 'text-gray-500',
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<span
|
<span
|
||||||
|
|
@ -361,7 +371,7 @@ function isActive(patterns) {
|
||||||
<div class="flex-1 flex flex-col min-w-0">
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
<div
|
<div
|
||||||
class="h-16 bg-white border-b border-gray-200 px-4 flex items-center justify-between sticky top-0 z-30 backdrop-blur-sm bg-white/95 shadow-sm"
|
class="h-16 border-b border-gray-200 px-4 flex items-center justify-between sticky top-0 z-30 backdrop-blur-sm bg-white/95 shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- Sidebar toggle -->
|
<!-- Sidebar toggle -->
|
||||||
|
|
@ -373,42 +383,11 @@ function isActive(patterns) {
|
||||||
aria-label="Toggle sidebar"
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
<!-- Hamburger (Bars) icon -->
|
<!-- Hamburger (Bars) icon -->
|
||||||
<svg
|
<MenuIcon />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
</Button>
|
||||||
<!-- Search trigger -->
|
<!-- Search trigger -->
|
||||||
<Button
|
<Button variant="outline" size="default" @click="openSearch" class="gap-2">
|
||||||
variant="outline"
|
<SearchIcon />
|
||||||
size="default"
|
|
||||||
@click="openSearch"
|
|
||||||
class="gap-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 1010.5 18.5a7.5 7.5 0 006.15-1.85z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
||||||
<kbd
|
<kbd
|
||||||
class="hidden sm:inline ml-2 text-[10px] px-1.5 py-0.5 rounded border border-gray-300 bg-gray-100 text-gray-600 font-medium"
|
class="hidden sm:inline ml-2 text-[10px] px-1.5 py-0.5 rounded border border-gray-300 bg-gray-100 text-gray-600 font-medium"
|
||||||
|
|
@ -428,7 +407,7 @@ function isActive(patterns) {
|
||||||
title="Phone"
|
title="Phone"
|
||||||
>
|
>
|
||||||
<Link :href="route('phone.index')">
|
<Link :href="route('phone.index')">
|
||||||
<FontAwesomeIcon :icon="faMobileScreenButton" class="h-5 w-5" />
|
<TabletSmartphoneIcon />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div class="ms-3 relative">
|
<div class="ms-3 relative">
|
||||||
|
|
@ -446,27 +425,9 @@ function isActive(patterns) {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span v-else class="inline-flex">
|
<span v-else class="inline-flex">
|
||||||
<Button
|
<Button variant="outline" size="default" type="button" class="gap-2">
|
||||||
variant="outline"
|
|
||||||
size="default"
|
|
||||||
type="button"
|
|
||||||
class="gap-2"
|
|
||||||
>
|
|
||||||
{{ $page.props.auth.user.name }}
|
{{ $page.props.auth.user.name }}
|
||||||
<svg
|
<ChevronDownIcon />
|
||||||
class="h-4 w-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -495,10 +456,7 @@ function isActive(patterns) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page Heading -->
|
<!-- Page Heading -->
|
||||||
<header
|
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
|
||||||
v-if="$slots.header"
|
|
||||||
class="bg-white border-b border-gray-200 shadow-sm"
|
|
||||||
>
|
|
||||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ import {
|
||||||
faClipboardList,
|
faClipboardList,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ClipboardCheckIcon,
|
||||||
|
ClipboardListIcon,
|
||||||
|
MenuIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -136,7 +144,9 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
: 'sticky top-0 h-screen overflow-y-auto',
|
: 'sticky top-0 h-screen overflow-y-auto',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="h-16 px-4 flex items-center justify-between border-b border-gray-200 bg-white">
|
<div
|
||||||
|
class="h-16 px-4 flex items-center justify-between border-b border-sidebar-border bg-sidebar"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
:href="route('phone.index')"
|
:href="route('phone.index')"
|
||||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||||
|
|
@ -144,7 +154,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
<ApplicationMark />
|
<ApplicationMark />
|
||||||
<span
|
<span
|
||||||
v-if="showLabels"
|
v-if="showLabels"
|
||||||
class="text-sm font-semibold text-gray-900 transition-opacity"
|
class="text-sm font-semibold text-sidebar-foreground transition-opacity"
|
||||||
>
|
>
|
||||||
Teren
|
Teren
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -160,21 +170,12 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||||
route().current('phone.index') ||
|
route().current('phone.index') ||
|
||||||
(route().current('phone.case') && !isCompletedMode)
|
(route().current('phone.case') && !isCompletedMode)
|
||||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||||
]"
|
]"
|
||||||
title="Opravila"
|
title="Opravila"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<ClipboardListIcon class="w-5 h-5 shrink-0 transition-colors" />
|
||||||
:icon="faClipboardList"
|
|
||||||
:class="[
|
|
||||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
|
||||||
route().current('phone.index') ||
|
|
||||||
(route().current('phone.case') && !isCompletedMode)
|
|
||||||
? 'text-primary-600'
|
|
||||||
: 'text-gray-500',
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
v-if="showLabels"
|
v-if="showLabels"
|
||||||
class="truncate transition-opacity"
|
class="truncate transition-opacity"
|
||||||
|
|
@ -196,21 +197,12 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||||
route().current('phone.completed') ||
|
route().current('phone.completed') ||
|
||||||
(route().current('phone.case') && isCompletedMode)
|
(route().current('phone.case') && isCompletedMode)
|
||||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||||
]"
|
]"
|
||||||
title="Zaključeno danes"
|
title="Zaključeno danes"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<ClipboardCheckIcon class="w-5 h-5 shrink-0 transition-colors" />
|
||||||
:icon="faCircleCheck"
|
|
||||||
:class="[
|
|
||||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
|
||||||
route().current('phone.completed') ||
|
|
||||||
(route().current('phone.case') && isCompletedMode)
|
|
||||||
? 'text-primary-600'
|
|
||||||
: 'text-gray-500',
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
v-if="showLabels"
|
v-if="showLabels"
|
||||||
class="truncate transition-opacity"
|
class="truncate transition-opacity"
|
||||||
|
|
@ -243,42 +235,11 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
||||||
aria-label="Toggle sidebar"
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
<svg
|
<MenuIcon />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
</Button>
|
||||||
<!-- Search trigger -->
|
<!-- Search trigger -->
|
||||||
<Button
|
<Button variant="outline" size="default" @click="openSearch" class="gap-2">
|
||||||
variant="outline"
|
<SearchIcon />
|
||||||
size="default"
|
|
||||||
@click="openSearch"
|
|
||||||
class="gap-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 1010.5 18.5a7.5 7.5 0 006.15-1.85z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
||||||
<kbd
|
<kbd
|
||||||
class="hidden sm:inline ml-2 text-[10px] px-1.5 py-0.5 rounded border border-gray-300 bg-gray-100 text-gray-600 font-medium"
|
class="hidden sm:inline ml-2 text-[10px] px-1.5 py-0.5 rounded border border-gray-300 bg-gray-100 text-gray-600 font-medium"
|
||||||
|
|
@ -288,7 +249,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
</div>
|
</div>
|
||||||
<!-- Notifications + User drop menu + Desktop switch button -->
|
<!-- Notifications + User drop menu + Desktop switch button -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<NotificationsBell class="mr-2" />
|
<NotificationsBell />
|
||||||
<!-- Desktop page quick access button -->
|
<!-- Desktop page quick access button -->
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -298,7 +259,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
title="Desktop"
|
title="Desktop"
|
||||||
>
|
>
|
||||||
<Link :href="route('clientCase')">
|
<Link :href="route('clientCase')">
|
||||||
<FontAwesomeIcon :icon="faDesktop" class="h-5 w-5" />
|
<MonitorIcon />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div class="ms-3 relative">
|
<div class="ms-3 relative">
|
||||||
|
|
@ -316,27 +277,9 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span v-else class="inline-flex">
|
<span v-else class="inline-flex">
|
||||||
<Button
|
<Button variant="outline" size="default" type="button" class="gap-2">
|
||||||
variant="outline"
|
|
||||||
size="default"
|
|
||||||
type="button"
|
|
||||||
class="gap-2"
|
|
||||||
>
|
|
||||||
{{ $page.props.auth.user.name }}
|
{{ $page.props.auth.user.name }}
|
||||||
<svg
|
<ChevronDownIcon />
|
||||||
class="h-4 w-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -365,10 +308,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page Heading -->
|
<!-- Page Heading -->
|
||||||
<header
|
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
|
||||||
v-if="$slots.header"
|
|
||||||
class="bg-white border-b border-gray-200 shadow-sm"
|
|
||||||
>
|
|
||||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { usePage, Link, router } from "@inertiajs/vue3";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { BellIcon } from "lucide-vue-next";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
const due = computed(
|
const due = computed(
|
||||||
|
|
@ -73,7 +76,7 @@ function markRead(item) {
|
||||||
// Rollback on failure
|
// Rollback on failure
|
||||||
items.value.splice(idx, 0, removed);
|
items.value.splice(idx, 0, removed);
|
||||||
},
|
},
|
||||||
preserveScroll: true
|
preserveScroll: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -86,18 +89,17 @@ function markRead(item) {
|
||||||
:content-classes="['p-0', 'bg-white', 'max-h-96', 'overflow-hidden']"
|
:content-classes="['p-0', 'bg-white', 'max-h-96', 'overflow-hidden']"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<Button variant="ghost" size="default" class="relative">
|
||||||
type="button"
|
<BellIcon />
|
||||||
class="relative inline-flex items-center justify-center w-9 h-9 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
|
||||||
aria-label="Notifications"
|
<Badge
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faBell" class="w-5 h-5" />
|
|
||||||
<span
|
|
||||||
v-if="count"
|
v-if="count"
|
||||||
class="absolute -top-1 -right-1 inline-flex items-center justify-center h-5 min-w-[1.25rem] px-1 rounded-full text-[11px] bg-red-600 text-white"
|
class="absolute -top-1 -right-1 h-5 min-w-5 inline-flex items-center justify-center rounded-full px-1 font-mono tabular-nums text-accent"
|
||||||
>{{ count }}</span
|
variant="destructive"
|
||||||
>
|
>
|
||||||
</button>
|
{{ count }}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,87 @@
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
import { Link, router } from "@inertiajs/vue3";
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
|
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Pagination from "@/Components/Pagination.vue";
|
import Pagination from "@/Components/Pagination.vue";
|
||||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
import { FolderOpenIcon } from "lucide-vue-next";
|
import { Filter, FolderOpenIcon } from "lucide-vue-next";
|
||||||
import CardTitle from "@/Components/ui/card/CardTitle.vue";
|
import CardTitle from "@/Components/ui/card/CardTitle.vue";
|
||||||
|
import { fmtCurrency, fmtDateDMY } from "@/Utilities/functions";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||||
|
import InputLabel from "@/Components/InputLabel.vue";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import DateRangePicker from "@/Components/DateRangePicker.vue";
|
||||||
|
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client_cases: Object,
|
client_cases: Object,
|
||||||
filters: Object,
|
filters: Object,
|
||||||
|
clients: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial search for DataTable toolbar
|
// Initial search for DataTable toolbar
|
||||||
const search = ref(props.filters?.search || "");
|
const search = ref(props.filters?.search || "");
|
||||||
|
const dateRange = ref({
|
||||||
|
start: props.filters?.from || null,
|
||||||
|
end: props.filters?.to || null,
|
||||||
|
});
|
||||||
|
const selectedClients = ref(
|
||||||
|
Array.isArray(props.filters?.clients)
|
||||||
|
? props.filters.clients.map((value) => String(value))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
const filterPopoverOpen = ref(false);
|
||||||
|
|
||||||
// Format helpers
|
const appliedFilterCount = computed(() => {
|
||||||
const fmtCurrency = (v) => {
|
let count = 0;
|
||||||
const n = Number(v ?? 0);
|
if (search.value?.trim()) count += 1;
|
||||||
try {
|
if (dateRange.value?.start || dateRange.value?.end) count += 1;
|
||||||
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(
|
if (selectedClients.value.length) count += 1;
|
||||||
n
|
return count;
|
||||||
);
|
});
|
||||||
} catch (e) {
|
|
||||||
return `${n.toFixed(2)} €`;
|
function applyFilters() {
|
||||||
|
filterPopoverOpen.value = false;
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const currentPerPage = searchParams.get("perPage");
|
||||||
|
if (currentPerPage) {
|
||||||
|
params.perPage = currentPerPage;
|
||||||
|
}
|
||||||
|
if (search.value && search.value.trim() !== "") {
|
||||||
|
params.search = search.value.trim();
|
||||||
|
}
|
||||||
|
if (dateRange.value?.start) {
|
||||||
|
params.from = dateRange.value.start;
|
||||||
|
}
|
||||||
|
if (dateRange.value?.end) {
|
||||||
|
params.to = dateRange.value.end;
|
||||||
|
}
|
||||||
|
if (selectedClients.value.length > 0) {
|
||||||
|
params.clients = selectedClients.value.join(",");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const fmtDateDMY = (v) => {
|
router.get(route("clientCase"), params, {
|
||||||
if (!v) return "-";
|
preserveState: true,
|
||||||
const d = new Date(v);
|
replace: true,
|
||||||
if (isNaN(d)) return "-";
|
preserveScroll: true,
|
||||||
const dd = String(d.getDate()).padStart(2, "0");
|
});
|
||||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
}
|
||||||
const yyyy = d.getFullYear();
|
|
||||||
return `${dd}.${mm}.${yyyy}`;
|
function clearFilters() {
|
||||||
};
|
dateRange.value = { start: null, end: null };
|
||||||
|
selectedClients.value = [];
|
||||||
|
search.value = "";
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Client cases">
|
<AppLayout title="Client cases">
|
||||||
|
|
@ -80,13 +123,99 @@ const fmtDateDMY = (v) => {
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:data="client_cases.data || []"
|
:data="client_cases.data || []"
|
||||||
|
:meta="client_cases"
|
||||||
|
route-name="clientCase"
|
||||||
|
page-param-name="clientCasesPage"
|
||||||
|
per-page-param-name="perPage"
|
||||||
:page-size="client_cases.per_page"
|
:page-size="client_cases.per_page"
|
||||||
|
:page-size-options="[10, 15, 25, 50, 100]"
|
||||||
:show-pagination="false"
|
:show-pagination="false"
|
||||||
:show-toolbar="true"
|
:show-toolbar="true"
|
||||||
:hoverable="true"
|
:hoverable="true"
|
||||||
row-key="uuid"
|
row-key="uuid"
|
||||||
empty-text="Ni najdenih primerov."
|
empty-text="Ni najdenih primerov."
|
||||||
>
|
>
|
||||||
|
<template #toolbar-filters>
|
||||||
|
<AppPopover
|
||||||
|
v-model:open="filterPopoverOpen"
|
||||||
|
align="start"
|
||||||
|
content-class="w-[400px]"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<Button variant="outline" size="sm" class="gap-2">
|
||||||
|
<Filter class="h-4 w-4" />
|
||||||
|
Filtri
|
||||||
|
<span
|
||||||
|
v-if="appliedFilterCount > 0"
|
||||||
|
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
||||||
|
>
|
||||||
|
{{ appliedFilterCount }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium text-sm">Filtri primerov</h4>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Izberite parametre za zožanje prikaza primerov.
|
||||||
|
</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 imenu, davčni številki ..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<InputLabel>Datumski obseg (ustvarjeno)</InputLabel>
|
||||||
|
<DateRangePicker
|
||||||
|
v-model="dateRange"
|
||||||
|
format="dd.MM.yyyy"
|
||||||
|
placeholder="Izberi datume"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<InputLabel>Stranke</InputLabel>
|
||||||
|
<AppMultiSelect
|
||||||
|
v-model="selectedClients"
|
||||||
|
:items="
|
||||||
|
(props.clients || []).map((client) => ({
|
||||||
|
value: String(client.id),
|
||||||
|
label: client.name,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
placeholder="Vse stranke"
|
||||||
|
search-placeholder="Išči stranko..."
|
||||||
|
empty-text="Ni strank"
|
||||||
|
chip-variant="secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 pt-2 border-t">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:disabled="
|
||||||
|
!dateRange?.start &&
|
||||||
|
!dateRange?.end &&
|
||||||
|
selectedClients.length === 0 &&
|
||||||
|
search === ''
|
||||||
|
"
|
||||||
|
@click="clearFilters"
|
||||||
|
>
|
||||||
|
Počisti
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" @click="applyFilters">
|
||||||
|
Uporabi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppPopover>
|
||||||
|
</template>
|
||||||
<template #cell-nu="{ row }">
|
<template #cell-nu="{ row }">
|
||||||
{{ row.person?.nu || "-" }}
|
{{ row.person?.nu || "-" }}
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,7 @@ const copyToClipboard = async (text) => {
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-[400px]" align="start">
|
<PopoverContent class="w-100" align="start">
|
||||||
<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 aktivnosti</h4>
|
<h4 class="font-medium text-sm">Filtri aktivnosti</h4>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,17 @@
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { Link } from "@inertiajs/vue3";
|
import { Link } from "@inertiajs/vue3";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/Components/ui/card";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
import { Search } from "lucide-vue-next";
|
||||||
|
import { fmtCurrency } from "@/Utilities/functions";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
segments: Array,
|
segments: Array,
|
||||||
|
|
@ -26,74 +37,111 @@ const filtered = computed(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatCurrencyEUR(value) {
|
const totalBalance = computed(() =>
|
||||||
if (value === null || value === undefined) {
|
filtered.value.reduce((sum, segment) => sum + Number(segment.total_balance ?? 0), 0)
|
||||||
return "-";
|
);
|
||||||
}
|
|
||||||
const n = Number(value);
|
|
||||||
if (isNaN(n)) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
n.toLocaleString("sl-SI", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
|
|
||||||
" €"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Segmenti">
|
<AppLayout title="Segmenti">
|
||||||
<template #header></template>
|
<template #header></template>
|
||||||
<div class="pt-12">
|
<div class="py-8">
|
||||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-6xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-4 mb-6">
|
<Card>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="text-2xl font-semibold tracking-tight">Segmenti</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Pregled vseh aktivnih segmentov in njihovih ključnih kazalnikov.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Število segmentov</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ filtered.length }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Skupaj pogodb</p>
|
||||||
|
<p class="text-2xl font-semibold">
|
||||||
|
{{
|
||||||
|
filtered.reduce((sum, s) => sum + Number(s.contracts_count ?? 0), 0)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Skupaj stanj</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ fmtCurrency(totalBalance) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<label class="text-sm font-medium text-muted-foreground"
|
||||||
>Iskanje (segment ali opis)</label
|
>Iskanje (segment ali opis)</label
|
||||||
>
|
>
|
||||||
<input
|
<div class="relative mt-2 max-w-md">
|
||||||
|
<Search
|
||||||
|
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
type="text"
|
type="text"
|
||||||
class="border rounded px-3 py-2 w-full max-w-xl"
|
|
||||||
placeholder="Išči po nazivu segmenta ali opisu"
|
placeholder="Išči po nazivu segmenta ali opisu"
|
||||||
|
class="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
<Card>
|
||||||
<h2 class="text-xl font-semibold mb-4">Aktivni segmenti</h2>
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle>Aktivni segmenti</CardTitle>
|
||||||
|
<CardDescription
|
||||||
|
>Rezultati so razporejeni v kartice. Klik na segment vodi na
|
||||||
|
podrobnosti.</CardDescription
|
||||||
|
>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<div
|
<div
|
||||||
v-if="filtered.length"
|
v-if="filtered.length"
|
||||||
class="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
class="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
||||||
>
|
>
|
||||||
<div
|
<Card
|
||||||
v-for="s in filtered"
|
v-for="s in filtered"
|
||||||
:key="s.id"
|
:key="s.id"
|
||||||
class="border rounded-lg p-4 shadow-sm hover:shadow transition bg-white"
|
class="border border-border/70 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between mb-2">
|
|
||||||
<h3 class="text-base font-semibold text-gray-900">
|
|
||||||
<Link :href="route('segments.show', s.id)" class="hover:underline">{{
|
|
||||||
s.name
|
|
||||||
}}</Link>
|
|
||||||
</h3>
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center text-xs px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-100"
|
|
||||||
>
|
>
|
||||||
|
<CardHeader class="space-y-1 pb-2">
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<CardTitle class="text-lg font-semibold">
|
||||||
|
<Link :href="route('segments.show', s.id)" class="hover:underline">
|
||||||
|
{{ s.name }}
|
||||||
|
</Link>
|
||||||
|
</CardTitle>
|
||||||
|
<Badge variant="secondary" class="text-xs font-medium">
|
||||||
{{ s.contracts_count ?? 0 }} pogodb
|
{{ s.contracts_count ?? 0 }} pogodb
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-600 min-h-[1.25rem]">
|
<CardDescription class="min-h-[1.5rem]">{{
|
||||||
{{ s.description || "" }}
|
s.description || ""
|
||||||
</p>
|
}}</CardDescription>
|
||||||
<div class="mt-4 flex items-center justify-between">
|
</CardHeader>
|
||||||
<div class="text-sm text-gray-500">Vsota stanj</div>
|
<CardContent>
|
||||||
<div class="text-sm font-medium text-gray-900">
|
<dl class="space-y-3 text-sm">
|
||||||
{{ formatCurrencyEUR(s.total_balance) }}
|
<div class="flex items-center justify-between">
|
||||||
|
<dt class="text-muted-foreground">Vsota stanj</dt>
|
||||||
|
<dd class="font-medium">{{ fmtCurrency(s.total_balance) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<dt class="text-muted-foreground">Aktivne pogodbe</dt>
|
||||||
|
<dd class="font-medium">{{ s.contracts_count ?? 0 }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
</dl>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="text-sm text-muted-foreground">Ni aktivnih segmentov.</div>
|
||||||
<div v-else class="text-gray-500">Ni aktivnih segmentov.</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,35 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
import { Link, router } from "@inertiajs/vue3";
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||||
import DialogModal from "@/Components/DialogModal.vue";
|
import DialogModal from "@/Components/DialogModal.vue";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import { Switch } from "@/Components/ui/switch";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/Components/ui/command";
|
||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Check,
|
||||||
|
FileDown,
|
||||||
|
LayoutIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
import { CardTitle } from "@/Components/ui/card";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
segment: Object,
|
segment: Object,
|
||||||
|
|
@ -17,16 +42,18 @@ const urlParams = new URLSearchParams(window.location.search);
|
||||||
const search = ref(urlParams.get("search") || "");
|
const search = ref(urlParams.get("search") || "");
|
||||||
const initialClient = urlParams.get("client") || urlParams.get("client_id") || "";
|
const initialClient = urlParams.get("client") || urlParams.get("client_id") || "";
|
||||||
const selectedClient = ref(initialClient);
|
const selectedClient = ref(initialClient);
|
||||||
|
const filterPopoverOpen = ref(false);
|
||||||
|
const clientComboboxOpen = ref(false);
|
||||||
|
|
||||||
// Column definitions for the server-driven table
|
// Column definitions for the server-driven table
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "reference", label: "Pogodba", sortable: true },
|
{ key: "reference", label: "Pogodba", sortable: false },
|
||||||
{ key: "client_case", label: "Primer" },
|
{ key: "client_case", label: "Primer", sortable: false },
|
||||||
{ key: "client", label: "Stranka" },
|
{ key: "client", label: "Stranka", sortable: false },
|
||||||
{ key: "type", label: "Vrsta" },
|
{ key: "type", label: "Vrsta", sortable: false },
|
||||||
{ key: "start_date", label: "Začetek", sortable: true },
|
{ key: "start_date", label: "Začetek", sortable: false },
|
||||||
{ key: "end_date", label: "Konec", sortable: true },
|
{ key: "end_date", label: "Konec", sortable: false },
|
||||||
{ key: "account", label: "Stanje", align: "right" },
|
{ key: "account", label: "Stanje", align: "right", sortable: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
const exportDialogOpen = ref(false);
|
const exportDialogOpen = ref(false);
|
||||||
|
|
@ -35,6 +62,21 @@ const exportColumns = ref(columns.map((col) => col.key));
|
||||||
const exportError = ref("");
|
const exportError = ref("");
|
||||||
const isExporting = ref(false);
|
const isExporting = ref(false);
|
||||||
|
|
||||||
|
const hasActiveFilters = computed(() => {
|
||||||
|
return Boolean(search.value?.trim()) || Boolean(selectedClient.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const appliedFilterCount = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
if (search.value?.trim()) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if (selectedClient.value) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
|
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
|
||||||
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
||||||
const totalContracts = computed(
|
const totalContracts = computed(
|
||||||
|
|
@ -43,12 +85,28 @@ const totalContracts = computed(
|
||||||
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
|
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
|
||||||
|
|
||||||
const allColumnsSelected = computed(() => exportColumns.value.length === columns.length);
|
const allColumnsSelected = computed(() => exportColumns.value.length === columns.length);
|
||||||
const exportDisabled = computed(() => exportColumns.value.length === 0 || isExporting.value);
|
const exportDisabled = computed(
|
||||||
|
() => exportColumns.value.length === 0 || isExporting.value
|
||||||
|
);
|
||||||
|
|
||||||
function toggleAllColumns(checked) {
|
function toggleAllColumns(checked) {
|
||||||
exportColumns.value = checked ? columns.map((col) => col.key) : [];
|
exportColumns.value = checked ? columns.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() {
|
function openExportDialog() {
|
||||||
exportDialogOpen.value = true;
|
exportDialogOpen.value = true;
|
||||||
exportError.value = "";
|
exportError.value = "";
|
||||||
|
|
@ -126,18 +184,68 @@ const selectedClientName = computed(() => {
|
||||||
return match?.label || "";
|
return match?.label || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// React to client selection changes by visiting the same route with updated query
|
function isClientFilterEmpty() {
|
||||||
watch(selectedClient, (val) => {
|
return !selectedClient.value;
|
||||||
const query = { search: search.value };
|
}
|
||||||
if (val) {
|
|
||||||
query.client = val;
|
function selectClient(value) {
|
||||||
|
selectedClient.value = value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeClientCombobox() {
|
||||||
|
clientComboboxOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClientValueSelected(value) {
|
||||||
|
return selectedClient.value === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClientSelect(value) {
|
||||||
|
selectClient(value);
|
||||||
|
closeClientCombobox();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryParams() {
|
||||||
|
const params = {};
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
for (const [key, value] of searchParams.entries()) {
|
||||||
|
params[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimmedSearch = search.value.trim();
|
||||||
|
if (trimmedSearch) {
|
||||||
|
params.search = trimmedSearch;
|
||||||
|
} else {
|
||||||
|
delete params.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedClient.value) {
|
||||||
|
params.client = selectedClient.value;
|
||||||
|
} else {
|
||||||
|
delete params.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.page = 1;
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const query = buildQueryParams();
|
||||||
router.get(
|
router.get(
|
||||||
route("segments.show", { segment: props.segment?.id ?? props.segment }),
|
route("segments.show", { segment: props.segment?.id ?? props.segment }),
|
||||||
query,
|
query,
|
||||||
{ preserveState: true, preserveScroll: true, only: ["contracts"], replace: true }
|
{ preserveState: true, preserveScroll: true, only: ["contracts"], replace: true }
|
||||||
);
|
);
|
||||||
});
|
filterPopoverOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
search.value = "";
|
||||||
|
selectedClient.value = "";
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|
@ -207,64 +315,191 @@ function extractFilenameFromHeaders(headers) {
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :title="`Segment: ${segment?.name || ''}`">
|
<AppLayout :title="`Segment: ${segment?.name || ''}`">
|
||||||
<template #header> </template>
|
<template #header> </template>
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="py-6">
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-4">
|
<div class="max-w-7xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
|
||||||
<h2 class="text-lg">{{ segment.name }}</h2>
|
<AppCard
|
||||||
<div class="text-sm text-gray-600 mb-4">{{ segment?.description }}</div>
|
title=""
|
||||||
|
padding="none"
|
||||||
|
class="p-5! gap-0"
|
||||||
|
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||||
|
body-class=""
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm uppercase text-muted-foreground">Segment</p>
|
||||||
|
<h2 class="text-2xl font-semibold text-foreground">{{ segment.name }}</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ segment?.description || "Ni opisa za izbran segment." }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Pogodbe (stran)</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ currentPageCount }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Skupaj pogodb</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ totalContracts }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-muted/40 p-4">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground">Izbrana stranka</p>
|
||||||
|
<p class="text-base font-medium">
|
||||||
|
{{ selectedClientName || "Vse" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppCard>
|
||||||
|
|
||||||
<!-- Filters -->
|
<AppCard
|
||||||
<div class="mb-4 flex flex-col sm:flex-row sm:items-end gap-3">
|
title=""
|
||||||
<div class="flex-1">
|
padding="none"
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Stranka</label>
|
class="p-0! gap-0"
|
||||||
|
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||||
|
body-class=""
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<select
|
<UsersIcon size="18" />
|
||||||
v-model="selectedClient"
|
<CardTitle class="uppercase">Pogodbe</CardTitle>
|
||||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
|
||||||
>
|
|
||||||
<option value="">Vse stranke</option>
|
|
||||||
<option
|
|
||||||
v-for="opt in clientOptions"
|
|
||||||
:key="opt.value || opt.label"
|
|
||||||
:value="opt.value"
|
|
||||||
>
|
|
||||||
{{ opt.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-sm text-gray-600 hover:text-gray-900"
|
|
||||||
@click="selectedClient = ''"
|
|
||||||
v-if="selectedClient"
|
|
||||||
>
|
|
||||||
Počisti
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<DataTable
|
||||||
|
|
||||||
<DataTableServer
|
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="contracts?.data || []"
|
:data="contracts?.data || []"
|
||||||
:meta="contracts || {}"
|
:meta="contracts || {}"
|
||||||
v-model:search="search"
|
|
||||||
route-name="segments.show"
|
route-name="segments.show"
|
||||||
:route-params="{ segment: segment?.id ?? segment }"
|
:route-params="{ segment: segment?.id ?? segment }"
|
||||||
:query="{ client: selectedClient || undefined }"
|
|
||||||
:only-props="['contracts']"
|
:only-props="['contracts']"
|
||||||
:page-size-options="[10, 25, 50]"
|
:page-size="contracts?.per_page ?? 15"
|
||||||
empty-text="Ni pogodb v tem segmentu."
|
:page-size-options="[10, 15, 25, 50]"
|
||||||
row-key="uuid"
|
row-key="uuid"
|
||||||
|
empty-text="Ni pogodb v tem segmentu."
|
||||||
|
per-page-param-name="per_page"
|
||||||
>
|
>
|
||||||
<template #toolbar-extra>
|
<template #toolbar-filters>
|
||||||
<button
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
type="button"
|
<Popover v-model:open="filterPopoverOpen">
|
||||||
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"
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm" class="gap-2">
|
||||||
|
<Filter class="h-4 w-4" />
|
||||||
|
Filtri
|
||||||
|
<span
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
||||||
|
>
|
||||||
|
{{ appliedFilterCount }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-105" align="start">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h4 class="text-sm font-medium">Filtri pogodb</h4>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Zoži prikaz pogodb po iskalnem nizu in stranki.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-sm font-medium">Iskanje</label>
|
||||||
|
<Input
|
||||||
|
v-model="search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Išči po referenci, vrsti ..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-sm font-medium">Stranka</label>
|
||||||
|
<Popover v-model:open="clientComboboxOpen">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
:aria-expanded="clientComboboxOpen"
|
||||||
|
class="w-full justify-between"
|
||||||
|
>
|
||||||
|
{{ selectedClientName || "Vse stranke" }}
|
||||||
|
<ChevronsUpDown
|
||||||
|
class="ml-2 h-4 w-4 shrink-0 opacity-50"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-full p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Išči stranko..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>Ni zadetkov.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
value="vse"
|
||||||
|
@select="handleClientSelect('')"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
isClientFilterEmpty
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
Vse stranke
|
||||||
|
</CommandItem>
|
||||||
|
<CommandItem
|
||||||
|
v-for="client in clientOptions"
|
||||||
|
:key="client.value || client.label"
|
||||||
|
:value="client.label"
|
||||||
|
@select="handleClientSelect(client.value)"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
isClientValueSelected(client.value)
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
{{ client.label }}
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-t pt-2">
|
||||||
|
<Button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="gap-2"
|
||||||
|
@click="clearFilters"
|
||||||
|
>
|
||||||
|
Počisti
|
||||||
|
</Button>
|
||||||
|
<div v-else></div>
|
||||||
|
<Button size="sm" class="gap-2" @click="applyFilters">
|
||||||
|
Uporabi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="gap-2"
|
||||||
@click="openExportDialog"
|
@click="openExportDialog"
|
||||||
>
|
>
|
||||||
|
<FileDown class="h-4 w-4" />
|
||||||
Izvozi v Excel
|
Izvozi v Excel
|
||||||
</button>
|
</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- Primer (client_case) cell with link when available -->
|
|
||||||
<template #cell-client_case="{ row }">
|
<template #cell-client_case="{ row }">
|
||||||
<Link
|
<Link
|
||||||
v-if="row.client_case?.uuid"
|
v-if="row.client_case?.uuid"
|
||||||
|
|
@ -280,120 +515,131 @@ function extractFilenameFromHeaders(headers) {
|
||||||
</Link>
|
</Link>
|
||||||
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
|
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Stranka (client) name -->
|
|
||||||
<template #cell-client="{ row }">
|
<template #cell-client="{ row }">
|
||||||
{{ row.client?.person?.full_name || "-" }}
|
{{ row.client?.person?.full_name || "-" }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Vrsta (type) -->
|
|
||||||
<template #cell-type="{ row }">
|
<template #cell-type="{ row }">
|
||||||
{{ row.type?.name || "-" }}
|
{{ row.type?.name || "-" }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Dates formatted -->
|
|
||||||
<template #cell-start_date="{ row }">
|
<template #cell-start_date="{ row }">
|
||||||
{{ formatDate(row.start_date) }}
|
{{ formatDate(row.start_date) }}
|
||||||
</template>
|
</template>
|
||||||
<template #cell-end_date="{ row }">
|
<template #cell-end_date="{ row }">
|
||||||
{{ formatDate(row.end_date) }}
|
{{ formatDate(row.end_date) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Account balance formatted -->
|
|
||||||
<template #cell-account="{ row }">
|
<template #cell-account="{ row }">
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
{{ formatCurrency(row.account?.balance_amount) }}
|
{{ formatCurrency(row.account?.balance_amount) }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTableServer>
|
</DataTable>
|
||||||
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div>
|
<div class="space-y-1">
|
||||||
<h3 class="text-lg font-semibold">Izvoz v Excel</h3>
|
<h3 class="text-lg font-semibold leading-6 text-foreground">Izvoz v Excel</h3>
|
||||||
<p class="text-sm text-gray-500">Izberi stolpce in obseg podatkov za izvoz.</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Izberi stolpce in obseg podatkov za izvoz.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<form id="segment-export-form" class="space-y-6" @submit.prevent="submitExport">
|
<form id="segment-export-form" class="space-y-5" @submit.prevent="submitExport">
|
||||||
<div>
|
<div class="space-y-3 rounded-lg border bg-muted/40 p-4">
|
||||||
<span class="text-sm font-semibold text-gray-700">Obseg podatkov</span>
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="mt-2 space-y-2">
|
<div class="space-y-1">
|
||||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
<p class="text-sm font-medium text-foreground">Obseg podatkov</p>
|
||||||
<input
|
<p class="text-sm text-muted-foreground">
|
||||||
type="radio"
|
Preklopi, ali izvoziš samo trenutni pogled ali celoten segment.
|
||||||
name="scope"
|
</p>
|
||||||
value="current"
|
</div>
|
||||||
class="text-indigo-600"
|
<div
|
||||||
v-model="exportScope"
|
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"
|
||||||
/>
|
/>
|
||||||
Trenutna stran ({{ currentPageCount }} zapisov)
|
<span class="text-xs font-medium text-muted-foreground">Vse</span>
|
||||||
</label>
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
</div>
|
||||||
<input
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
type="radio"
|
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
||||||
name="scope"
|
<p class="text-sm font-semibold text-foreground">Trenutna stran</p>
|
||||||
value="all"
|
<p class="text-xs text-muted-foreground">
|
||||||
class="text-indigo-600"
|
{{ currentPageCount }} zapisov
|
||||||
v-model="exportScope"
|
</p>
|
||||||
/>
|
</div>
|
||||||
Celoten segment ({{ totalContracts }} zapisov)
|
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
||||||
</label>
|
<p class="text-sm font-semibold text-foreground">Celoten segment</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ totalContracts }} zapisov</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="space-y-4 rounded-lg border bg-muted/40 p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<span class="text-sm font-semibold text-gray-700">Stolpci</span>
|
<div class="space-y-1">
|
||||||
<label class="flex items-center gap-2 text-xs text-gray-600">
|
<p class="text-sm font-medium text-foreground">Stolpci</p>
|
||||||
<input
|
<p class="text-sm text-muted-foreground">
|
||||||
type="checkbox"
|
Izberi, katere stolpce želiš vključiti v izvoz.
|
||||||
:checked="allColumnsSelected"
|
</p>
|
||||||
@change="toggleAllColumns($event.target.checked)"
|
|
||||||
/>
|
|
||||||
Označi vse
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<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
|
<label
|
||||||
v-for="col in columns"
|
v-for="col in columns"
|
||||||
:key="col.key"
|
:key="col.key"
|
||||||
class="flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm"
|
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}`"
|
||||||
>
|
>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
:id="`export-col-${col.key}`"
|
||||||
name="columns[]"
|
:model-value="exportColumns.includes(col.key)"
|
||||||
:value="col.key"
|
:value="col.key"
|
||||||
v-model="exportColumns"
|
@update:modelValue="(checked) => handleColumnToggle(col.key, checked)"
|
||||||
class="text-indigo-600"
|
class="mt-0.5"
|
||||||
/>
|
/>
|
||||||
{{ col.label }}
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="exportError" class="mt-2 text-sm text-red-600">{{ exportError }}</p>
|
<p v-if="exportError" class="text-sm text-destructive">{{ exportError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button
|
<Button type="button" variant="ghost" @click="closeExportDialog">
|
||||||
type="button"
|
|
||||||
class="text-sm text-gray-600 hover:text-gray-900"
|
|
||||||
@click="closeExportDialog"
|
|
||||||
>
|
|
||||||
Prekliči
|
Prekliči
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="segment-export-form"
|
form="segment-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"
|
:disabled="exportDisabled"
|
||||||
|
class="gap-2"
|
||||||
>
|
>
|
||||||
<span v-if="!isExporting">Prenesi Excel</span>
|
<span v-if="!isExporting">Prenesi Excel</span>
|
||||||
<span v-else>Pripravljam ...</span>
|
<span v-else>Pripravljam ...</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DialogModal>
|
</DialogModal>
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,24 @@ export function fmtDateTime(d) {
|
||||||
return String(d);
|
return String(d);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function fmtCurrency(value) {
|
||||||
|
const n = Number(value ?? 0);
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(
|
||||||
|
n
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return `${n.toFixed(2)} €`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtDateDMY(value) {
|
||||||
|
if (!value) return "-";
|
||||||
|
const d = new Date(value);
|
||||||
|
if (isNaN(d)) return "-";
|
||||||
|
const dd = String(d.getDate()).padStart(2, "0");
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const yyyy = d.getFullYear();
|
||||||
|
return `${dd}.${mm}.${yyyy}`;
|
||||||
|
}
|
||||||
109
tests/Feature/ClientCaseIndexFilterTest.php
Normal file
109
tests/Feature/ClientCaseIndexFilterTest.php
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientCase;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Inertia\Testing\AssertableInertia as Assert;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ClientCaseIndexFilterTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_filters_cases_by_client_and_date(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$clientInclude = Client::factory()->create(['active' => true]);
|
||||||
|
$clientExclude = Client::factory()->create(['active' => true]);
|
||||||
|
|
||||||
|
$recentCase = ClientCase::factory()->create([
|
||||||
|
'client_id' => $clientInclude->id,
|
||||||
|
]);
|
||||||
|
$recentCase->forceFill([
|
||||||
|
'created_at' => now()->subDays(2),
|
||||||
|
'updated_at' => now()->subDays(2),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$otherClientCase = ClientCase::factory()->create([
|
||||||
|
'client_id' => $clientExclude->id,
|
||||||
|
]);
|
||||||
|
$otherClientCase->forceFill([
|
||||||
|
'created_at' => now()->subDays(2),
|
||||||
|
'updated_at' => now()->subDays(2),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$outOfRangeCase = ClientCase::factory()->create([
|
||||||
|
'client_id' => $clientInclude->id,
|
||||||
|
]);
|
||||||
|
$outOfRangeCase->forceFill([
|
||||||
|
'created_at' => now()->subDays(15),
|
||||||
|
'updated_at' => now()->subDays(15),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$response = $this->get(route('clientCase', [
|
||||||
|
'clients' => (string) $clientInclude->id,
|
||||||
|
'from' => now()->subDays(5)->toDateString(),
|
||||||
|
'to' => now()->toDateString(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertInertia(function (Assert $page) use ($recentCase, $otherClientCase, $outOfRangeCase, $clientInclude) {
|
||||||
|
$page->component('Cases/Index');
|
||||||
|
|
||||||
|
$payload = $page->toArray()['props'];
|
||||||
|
$uuids = collect($payload['client_cases']['data'])->pluck('uuid')->all();
|
||||||
|
|
||||||
|
$this->assertContains($recentCase->uuid, $uuids);
|
||||||
|
$this->assertNotContains($otherClientCase->uuid, $uuids);
|
||||||
|
$this->assertNotContains($outOfRangeCase->uuid, $uuids);
|
||||||
|
$this->assertEquals([(string) $clientInclude->id], $payload['filters']['clients']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_filters_cases_by_search_term(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$targetCase = ClientCase::factory()->create();
|
||||||
|
$targetCase->person()->update(['full_name' => 'Special Target']);
|
||||||
|
|
||||||
|
$otherCase = ClientCase::factory()->create();
|
||||||
|
$otherCase->person()->update(['full_name' => 'Another Person']);
|
||||||
|
|
||||||
|
$response = $this->get(route('clientCase', ['search' => 'Target']));
|
||||||
|
|
||||||
|
$response->assertInertia(function (Assert $page) use ($targetCase, $otherCase) {
|
||||||
|
$page->component('Cases/Index');
|
||||||
|
|
||||||
|
$uuids = collect($page->toArray()['props']['client_cases']['data'])->pluck('uuid')->all();
|
||||||
|
|
||||||
|
$this->assertContains($targetCase->uuid, $uuids);
|
||||||
|
$this->assertNotContains($otherCase->uuid, $uuids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_respects_custom_per_page_selection(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
ClientCase::factory()->count(30)->create();
|
||||||
|
|
||||||
|
$response = $this->get(route('clientCase', ['perPage' => 10]));
|
||||||
|
|
||||||
|
$response->assertInertia(function (Assert $page) {
|
||||||
|
$page->component('Cases/Index');
|
||||||
|
|
||||||
|
$payload = $page->toArray()['props'];
|
||||||
|
|
||||||
|
$this->assertCount(10, $payload['client_cases']['data']);
|
||||||
|
$this->assertSame(10, $payload['client_cases']['per_page']);
|
||||||
|
$this->assertSame(10, $payload['filters']['perPage']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user