update case index page segment index and show page
This commit is contained in:
parent
a6ec92ec6b
commit
80948d2944
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\UpdateContractRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use App\Models\Segment;
|
||||
use App\Services\Documents\DocumentStreamService;
|
||||
use App\Services\ReferenceDataCache;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Exception;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
|
@ -30,6 +33,16 @@ public function __construct(
|
|||
public function index(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$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()
|
||||
->select('client_cases.*')
|
||||
|
|
@ -39,7 +52,6 @@ public function index(ClientCase $clientCase, Request $request)
|
|||
->groupBy('client_cases.id');
|
||||
})
|
||||
->where('client_cases.active', 1)
|
||||
// Use LEFT JOINs for aggregated data to avoid subqueries
|
||||
->leftJoin('contracts', function ($join) {
|
||||
$join->on('contracts.client_case_id', '=', 'client_cases.id')
|
||||
->whereNull('contracts.deleted_at');
|
||||
|
|
@ -49,11 +61,18 @@ public function index(ClientCase $clientCase, Request $request)
|
|||
->where('contract_segment.active', true);
|
||||
})
|
||||
->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')
|
||||
->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'),
|
||||
// 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'),
|
||||
])
|
||||
->with(['person.client', 'client.person'])
|
||||
|
|
@ -61,12 +80,49 @@ public function index(ClientCase $clientCase, Request $request)
|
|||
|
||||
return Inertia::render('Cases/Index', [
|
||||
'client_cases' => $query
|
||||
->paginate($request->integer('perPage', 15), ['*'], 'clientCasesPage')
|
||||
->paginate($perPage, ['*'], 'clientCasesPage')
|
||||
->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.
|
||||
*/
|
||||
|
|
@ -1031,7 +1087,7 @@ public function emergencyCreatePerson(ClientCase $clientCase, Request $request)
|
|||
if ($existing && ! $existing->trashed()) {
|
||||
return back()->with('flash', [
|
||||
'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'])) {
|
||||
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
||||
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) {
|
||||
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
||||
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
||||
}
|
||||
}
|
||||
if (! $profile) {
|
||||
|
|
@ -1182,7 +1238,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
|||
}
|
||||
|
||||
// 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 = [
|
||||
'note' => $activityNote,
|
||||
'user_id' => optional($request->user())->id,
|
||||
|
|
@ -1220,7 +1276,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
|||
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) {
|
||||
\Log::warning('SMS enqueue failed', [
|
||||
'error' => $e->getMessage(),
|
||||
|
|
@ -1228,7 +1284,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
|||
'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": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@internationalized/date": "^3.9.0",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unovis/ts": "^1.6.2",
|
||||
"@unovis/vue": "^1.6.2",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"apexcharts": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^11.0.3",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
|
|
@ -28,37 +28,37 @@
|
|||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"preline": "^2.7.0",
|
||||
"quill": "^1.3.7",
|
||||
"reka-ui": "^2.6.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"reka-ui": "^2.6.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue-currency-input": "^3.2.1",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
"vue-search-input": "^1.1.16",
|
||||
"vue-multiselect": "^3.4.0",
|
||||
"vue-search-input": "^1.1.19",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"vue3-apexcharts": "^1.7.0",
|
||||
"vue3-apexcharts": "^1.10.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inertiajs/vue3": "2.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.7.4",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.3",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"axios": "^1.13.2",
|
||||
"laravel-vite-plugin": "^2.0.1",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.2.7",
|
||||
"vue": "^3.3.13",
|
||||
"vue-tsc": "^3.1.5"
|
||||
"vue-tsc": "^3.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
@ -1472,9 +1472,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
||||
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1484,37 +1484,37 @@
|
|||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.17"
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
|
||||
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.17",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.17",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
|
||||
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1529,9 +1529,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
|
||||
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1546,9 +1546,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
|
||||
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1563,9 +1563,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
|
||||
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1580,9 +1580,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
|
||||
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -1597,9 +1597,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
|
||||
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1614,9 +1614,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
|
||||
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1631,9 +1631,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
|
||||
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1648,9 +1648,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
|
||||
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1665,9 +1665,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
|
||||
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
|
|
@ -1683,10 +1683,10 @@
|
|||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.6.0",
|
||||
"@emnapi/runtime": "^1.6.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@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",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
|
|
@ -1695,7 +1695,7 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -1706,7 +1706,7 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -1726,14 +1726,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.7",
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -1755,9 +1755,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
|
||||
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1772,9 +1772,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
|
||||
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1789,17 +1789,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
|
||||
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
|
||||
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.17",
|
||||
"@tailwindcss/oxide": "4.1.17",
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.17"
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
|
|
@ -1829,9 +1829,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"version": "3.13.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz",
|
||||
"integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -1858,12 +1858,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/vue-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
|
||||
"version": "3.13.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.13.tgz",
|
||||
"integrity": "sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
"@tanstack/virtual-core": "3.13.13"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -2208,9 +2208,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"version": "24.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz",
|
||||
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2414,30 +2414,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
|
||||
"integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz",
|
||||
"integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "2.4.23"
|
||||
"@volar/source-map": "2.4.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "2.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
|
||||
"integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz",
|
||||
"integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "2.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
|
||||
"integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz",
|
||||
"integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.23",
|
||||
"@volar/language-core": "2.4.26",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
|
|
@ -2526,13 +2526,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.5.tgz",
|
||||
"integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.8.tgz",
|
||||
"integrity": "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.23",
|
||||
"@volar/language-core": "2.4.26",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^3.0.0",
|
||||
|
|
@ -2764,9 +2764,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.32",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz",
|
||||
"integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
|
||||
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -2774,18 +2774,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz",
|
||||
"integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2803,11 +2803,11 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
"electron-to-chromium": "^1.5.249",
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
|
|
@ -2873,9 +2873,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||
"version": "1.0.30001760",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
|
||||
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3627,9 +3627,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.263",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz",
|
||||
"integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==",
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
|
@ -5413,9 +5413,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
|
|
@ -5561,9 +5561,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -5655,9 +5655,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
||||
"version": "7.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -5849,14 +5849,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.5.tgz",
|
||||
"integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.8.tgz",
|
||||
"integrity": "sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "2.4.23",
|
||||
"@vue/language-core": "3.1.5"
|
||||
"@volar/typescript": "2.4.26",
|
||||
"@vue/language-core": "3.1.8"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
|
|
|
|||
50
package.json
50
package.json
|
|
@ -9,37 +9,37 @@
|
|||
"devDependencies": {
|
||||
"@inertiajs/vue3": "2.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.7.4",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.3",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"axios": "^1.13.2",
|
||||
"laravel-vite-plugin": "^2.0.1",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.2.7",
|
||||
"vue": "^3.3.13",
|
||||
"vue-tsc": "^3.1.5"
|
||||
"vue-tsc": "^3.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@internationalized/date": "^3.9.0",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unovis/ts": "^1.6.2",
|
||||
"@unovis/vue": "^1.6.2",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"apexcharts": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^11.0.3",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
|
|
@ -48,17 +48,17 @@
|
|||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"preline": "^2.7.0",
|
||||
"quill": "^1.3.7",
|
||||
"reka-ui": "^2.6.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"reka-ui": "^2.6.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue-currency-input": "^3.2.1",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
"vue-search-input": "^1.1.16",
|
||||
"vue-multiselect": "^3.4.0",
|
||||
"vue-search-input": "^1.1.19",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"vue3-apexcharts": "^1.7.0",
|
||||
"vue3-apexcharts": "^1.10.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,12 +441,18 @@ function doServerRequest(overrides = {}) {
|
|||
});
|
||||
|
||||
const url = route(props.routeName, props.routeParams || {});
|
||||
router.get(url, q, {
|
||||
const onlyProps = Array.isArray(props.onlyProps) ? props.onlyProps : [];
|
||||
const inertiaOptions = {
|
||||
preserveScroll: props.preserveScroll,
|
||||
preserveState: props.preserveState,
|
||||
replace: true,
|
||||
only: props.onlyProps.length ? props.onlyProps : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
if (onlyProps.length > 0) {
|
||||
inertiaOptions.only = onlyProps;
|
||||
}
|
||||
|
||||
router.get(url, q, inertiaOptions);
|
||||
}
|
||||
|
||||
// Row key helper
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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 ApplicationMark from "@/Components/ApplicationMark.vue";
|
||||
import Banner from "@/Components/Banner.vue";
|
||||
|
|
@ -23,6 +23,22 @@ import {
|
|||
faMap,
|
||||
faGear,
|
||||
} 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({
|
||||
title: String,
|
||||
|
|
@ -115,12 +131,14 @@ const rawMenuGroups = [
|
|||
items: [
|
||||
{
|
||||
key: "dashboard",
|
||||
icon: LayoutDashboardIcon,
|
||||
title: "Nadzorna plošča",
|
||||
routeName: "dashboard",
|
||||
active: ["dashboard"],
|
||||
},
|
||||
{
|
||||
key: "reports",
|
||||
icon: FileChartColumn,
|
||||
title: "Poročila",
|
||||
routeName: "reports.index",
|
||||
active: ["reports.index", "reports.show"],
|
||||
|
|
@ -133,18 +151,21 @@ const rawMenuGroups = [
|
|||
items: [
|
||||
{
|
||||
key: "clients",
|
||||
icon: UsersIcon,
|
||||
title: "Naročniki",
|
||||
routeName: "client",
|
||||
active: ["client", "client.*"],
|
||||
},
|
||||
{
|
||||
key: "cases",
|
||||
icon: FoldersIcon,
|
||||
title: "Primeri",
|
||||
routeName: "clientCase",
|
||||
active: ["clientCase", "clientCase.*"],
|
||||
},
|
||||
{
|
||||
key: "segments",
|
||||
icon: LayoutIcon,
|
||||
title: "Segmenti",
|
||||
routeName: "segments.index",
|
||||
active: ["segments.index"],
|
||||
|
|
@ -157,18 +178,21 @@ const rawMenuGroups = [
|
|||
items: [
|
||||
{
|
||||
key: "imports",
|
||||
icon: ImportIcon,
|
||||
title: "Uvozi",
|
||||
routeName: "imports.index",
|
||||
active: ["imports.index", "imports.*"],
|
||||
},
|
||||
{
|
||||
key: "import-templates",
|
||||
icon: ListIndentIncreaseIcon,
|
||||
title: "Uvozne predloge",
|
||||
routeName: "importTemplates.index",
|
||||
active: ["importTemplates.index"],
|
||||
},
|
||||
{
|
||||
key: "import-templates-new",
|
||||
icon: FilePlusIcon,
|
||||
title: "Nova uvozna predloga",
|
||||
routeName: "importTemplates.create",
|
||||
active: ["importTemplates.create"],
|
||||
|
|
@ -181,6 +205,7 @@ const rawMenuGroups = [
|
|||
items: [
|
||||
{
|
||||
key: "fieldjobs",
|
||||
icon: MapIcon,
|
||||
title: "Terenske naloge",
|
||||
routeName: "fieldjobs.index",
|
||||
active: ["fieldjobs.index"],
|
||||
|
|
@ -195,6 +220,7 @@ const rawMenuGroups = [
|
|||
items: [
|
||||
{
|
||||
key: "settings",
|
||||
icon: SettingsIcon,
|
||||
title: "Nastavitve",
|
||||
routeName: "settings",
|
||||
active: ["settings", "settings.*"],
|
||||
|
|
@ -204,6 +230,7 @@ const rawMenuGroups = [
|
|||
// We'll filter it out below if not authorized.
|
||||
{
|
||||
key: "admin-panel",
|
||||
icon: ShieldUserIcon,
|
||||
title: "Administrator",
|
||||
routeName: "admin.index",
|
||||
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) {
|
||||
try {
|
||||
return patterns?.some((p) => route().current(p));
|
||||
|
|
@ -289,7 +301,7 @@ function isActive(patterns) {
|
|||
<aside
|
||||
:class="[
|
||||
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
|
||||
isMobile
|
||||
? 'fixed inset-y-0 left-0 transform shadow-strong ' +
|
||||
|
|
@ -297,7 +309,9 @@ function isActive(patterns) {
|
|||
: '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
|
||||
:href="route('dashboard')"
|
||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
|
|
@ -305,7 +319,7 @@ function isActive(patterns) {
|
|||
<ApplicationMark />
|
||||
<span
|
||||
v-if="!sidebarCollapsed"
|
||||
class="text-sm font-semibold text-gray-900 transition-opacity"
|
||||
class="text-sm font-semibold text-sidebar-foreground transition-opacity"
|
||||
>
|
||||
Teren
|
||||
</span>
|
||||
|
|
@ -316,7 +330,7 @@ function isActive(patterns) {
|
|||
<li v-for="group in menuGroups" :key="group.label">
|
||||
<div
|
||||
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 }}
|
||||
</div>
|
||||
|
|
@ -327,19 +341,15 @@ function isActive(patterns) {
|
|||
:class="[
|
||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||
isActive(item.active)
|
||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
]"
|
||||
:title="item.title"
|
||||
>
|
||||
<!-- Unified FontAwesome icon rendering -->
|
||||
<FontAwesomeIcon
|
||||
v-if="menuIconMap[item.key]"
|
||||
:icon="menuIconMap[item.key]"
|
||||
:class="[
|
||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
||||
isActive(item.active) ? 'text-primary-600' : 'text-gray-500',
|
||||
]"
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="w-5 h-5 shrink-0 transition-colors"
|
||||
/>
|
||||
<!-- Title -->
|
||||
<span
|
||||
|
|
@ -361,7 +371,7 @@ function isActive(patterns) {
|
|||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Top bar -->
|
||||
<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">
|
||||
<!-- Sidebar toggle -->
|
||||
|
|
@ -373,42 +383,11 @@ function isActive(patterns) {
|
|||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<!-- Hamburger (Bars) icon -->
|
||||
<svg
|
||||
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>
|
||||
<MenuIcon />
|
||||
</Button>
|
||||
<!-- Search trigger -->
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
<Button variant="outline" size="default" @click="openSearch" class="gap-2">
|
||||
<SearchIcon />
|
||||
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
||||
<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"
|
||||
|
|
@ -428,7 +407,7 @@ function isActive(patterns) {
|
|||
title="Phone"
|
||||
>
|
||||
<Link :href="route('phone.index')">
|
||||
<FontAwesomeIcon :icon="faMobileScreenButton" class="h-5 w-5" />
|
||||
<TabletSmartphoneIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<div class="ms-3 relative">
|
||||
|
|
@ -446,27 +425,9 @@ function isActive(patterns) {
|
|||
</button>
|
||||
|
||||
<span v-else class="inline-flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
type="button"
|
||||
class="gap-2"
|
||||
>
|
||||
<Button variant="outline" size="default" type="button" class="gap-2">
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg
|
||||
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>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -495,10 +456,7 @@ function isActive(patterns) {
|
|||
</div>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="bg-white border-b border-gray-200 shadow-sm"
|
||||
>
|
||||
<header 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">
|
||||
<Breadcrumbs
|
||||
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ import {
|
|||
faClipboardList,
|
||||
faCircleCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ClipboardCheckIcon,
|
||||
ClipboardListIcon,
|
||||
MenuIcon,
|
||||
MonitorIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
|
|
@ -136,7 +144,9 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
: '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
|
||||
:href="route('phone.index')"
|
||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
|
|
@ -144,7 +154,7 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
<ApplicationMark />
|
||||
<span
|
||||
v-if="showLabels"
|
||||
class="text-sm font-semibold text-gray-900 transition-opacity"
|
||||
class="text-sm font-semibold text-sidebar-foreground transition-opacity"
|
||||
>
|
||||
Teren
|
||||
</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',
|
||||
route().current('phone.index') ||
|
||||
(route().current('phone.case') && !isCompletedMode)
|
||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
]"
|
||||
title="Opravila"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
: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',
|
||||
]"
|
||||
/>
|
||||
<ClipboardListIcon class="w-5 h-5 shrink-0 transition-colors" />
|
||||
<span
|
||||
v-if="showLabels"
|
||||
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',
|
||||
route().current('phone.completed') ||
|
||||
(route().current('phone.case') && isCompletedMode)
|
||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
]"
|
||||
title="Zaključeno danes"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
: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',
|
||||
]"
|
||||
/>
|
||||
<ClipboardCheckIcon class="w-5 h-5 shrink-0 transition-colors" />
|
||||
<span
|
||||
v-if="showLabels"
|
||||
class="truncate transition-opacity"
|
||||
|
|
@ -243,42 +235,11 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<MenuIcon />
|
||||
</Button>
|
||||
<!-- Search trigger -->
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
<Button variant="outline" size="default" @click="openSearch" class="gap-2">
|
||||
<SearchIcon />
|
||||
<span class="hidden sm:inline text-sm font-medium">Globalni iskalnik</span>
|
||||
<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"
|
||||
|
|
@ -288,7 +249,7 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
</div>
|
||||
<!-- Notifications + User drop menu + Desktop switch button -->
|
||||
<div class="flex items-center">
|
||||
<NotificationsBell class="mr-2" />
|
||||
<NotificationsBell />
|
||||
<!-- Desktop page quick access button -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -298,7 +259,7 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
title="Desktop"
|
||||
>
|
||||
<Link :href="route('clientCase')">
|
||||
<FontAwesomeIcon :icon="faDesktop" class="h-5 w-5" />
|
||||
<MonitorIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<div class="ms-3 relative">
|
||||
|
|
@ -316,27 +277,9 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
</button>
|
||||
|
||||
<span v-else class="inline-flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
type="button"
|
||||
class="gap-2"
|
||||
>
|
||||
<Button variant="outline" size="default" type="button" class="gap-2">
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg
|
||||
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>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -365,10 +308,7 @@ const closeSearch = () => (searchOpen.value = false);
|
|||
</div>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="bg-white border-b border-gray-200 shadow-sm"
|
||||
>
|
||||
<header 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">
|
||||
<Breadcrumbs
|
||||
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 { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
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 due = computed(
|
||||
|
|
@ -73,7 +76,7 @@ function markRead(item) {
|
|||
// Rollback on failure
|
||||
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']"
|
||||
>
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faBell" class="w-5 h-5" />
|
||||
<span
|
||||
<Button variant="ghost" size="default" class="relative">
|
||||
<BellIcon />
|
||||
|
||||
<Badge
|
||||
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"
|
||||
>{{ count }}</span
|
||||
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"
|
||||
variant="destructive"
|
||||
>
|
||||
</button>
|
||||
{{ count }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
|
|
|
|||
|
|
@ -2,44 +2,87 @@
|
|||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import Pagination from "@/Components/Pagination.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 { 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({
|
||||
client_cases: Object,
|
||||
filters: Object,
|
||||
clients: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// Initial search for DataTable toolbar
|
||||
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 fmtCurrency = (v) => {
|
||||
const n = Number(v ?? 0);
|
||||
try {
|
||||
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(
|
||||
n
|
||||
);
|
||||
} catch (e) {
|
||||
return `${n.toFixed(2)} €`;
|
||||
const appliedFilterCount = computed(() => {
|
||||
let count = 0;
|
||||
if (search.value?.trim()) count += 1;
|
||||
if (dateRange.value?.start || dateRange.value?.end) count += 1;
|
||||
if (selectedClients.value.length) count += 1;
|
||||
return count;
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (!v) return "-";
|
||||
const d = new Date(v);
|
||||
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}`;
|
||||
};
|
||||
router.get(route("clientCase"), params, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
dateRange.value = { start: null, end: null };
|
||||
selectedClients.value = [];
|
||||
search.value = "";
|
||||
applyFilters();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppLayout title="Client cases">
|
||||
|
|
@ -80,13 +123,99 @@ const fmtDateDMY = (v) => {
|
|||
},
|
||||
]"
|
||||
: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-options="[10, 15, 25, 50, 100]"
|
||||
:show-pagination="false"
|
||||
:show-toolbar="true"
|
||||
:hoverable="true"
|
||||
row-key="uuid"
|
||||
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 }">
|
||||
{{ row.person?.nu || "-" }}
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ const copyToClipboard = async (text) => {
|
|||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[400px]" align="start">
|
||||
<PopoverContent class="w-100" align="start">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-medium text-sm">Filtri aktivnosti</h4>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,17 @@
|
|||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
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({
|
||||
segments: Array,
|
||||
|
|
@ -26,74 +37,111 @@ const filtered = computed(() => {
|
|||
});
|
||||
});
|
||||
|
||||
function formatCurrencyEUR(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return "-";
|
||||
}
|
||||
const n = Number(value);
|
||||
if (isNaN(n)) {
|
||||
return String(value);
|
||||
}
|
||||
return (
|
||||
n.toLocaleString("sl-SI", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
|
||||
" €"
|
||||
);
|
||||
}
|
||||
const totalBalance = computed(() =>
|
||||
filtered.value.reduce((sum, segment) => sum + Number(segment.total_balance ?? 0), 0)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Segmenti">
|
||||
<template #header></template>
|
||||
<div class="pt-12">
|
||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-4 mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
<div class="py-8">
|
||||
<div class="max-w-6xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<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
|
||||
>
|
||||
<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"
|
||||
type="text"
|
||||
class="border rounded px-3 py-2 w-full max-w-xl"
|
||||
placeholder="Išči po nazivu segmenta ali opisu"
|
||||
class="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Aktivni segmenti</h2>
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle>Aktivni segmenti</CardTitle>
|
||||
<CardDescription
|
||||
>Rezultati so razporejeni v kartice. Klik na segment vodi na
|
||||
podrobnosti.</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
v-if="filtered.length"
|
||||
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"
|
||||
:key="s.id"
|
||||
class="border rounded-lg p-4 shadow-sm hover:shadow transition bg-white"
|
||||
>
|
||||
<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"
|
||||
class="border border-border/70 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
|
||||
>
|
||||
<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
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 min-h-[1.25rem]">
|
||||
{{ s.description || "" }}
|
||||
</p>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500">Vsota stanj</div>
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ formatCurrencyEUR(s.total_balance) }}
|
||||
<CardDescription class="min-h-[1.5rem]">{{
|
||||
s.description || ""
|
||||
}}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<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 class="flex items-center justify-between">
|
||||
<dt class="text-muted-foreground">Aktivne pogodbe</dt>
|
||||
<dd class="font-medium">{{ s.contracts_count ?? 0 }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500">Ni aktivnih segmentov.</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">Ni aktivnih segmentov.</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,35 @@
|
|||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
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 { 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({
|
||||
segment: Object,
|
||||
|
|
@ -17,16 +42,18 @@ const urlParams = new URLSearchParams(window.location.search);
|
|||
const search = ref(urlParams.get("search") || "");
|
||||
const initialClient = urlParams.get("client") || urlParams.get("client_id") || "";
|
||||
const selectedClient = ref(initialClient);
|
||||
const filterPopoverOpen = ref(false);
|
||||
const clientComboboxOpen = ref(false);
|
||||
|
||||
// Column definitions for the server-driven table
|
||||
const columns = [
|
||||
{ key: "reference", label: "Pogodba", sortable: true },
|
||||
{ key: "client_case", label: "Primer" },
|
||||
{ key: "client", label: "Stranka" },
|
||||
{ key: "type", label: "Vrsta" },
|
||||
{ key: "start_date", label: "Začetek", sortable: true },
|
||||
{ key: "end_date", label: "Konec", sortable: true },
|
||||
{ key: "account", label: "Stanje", align: "right" },
|
||||
{ key: "reference", label: "Pogodba", sortable: false },
|
||||
{ key: "client_case", label: "Primer", sortable: false },
|
||||
{ key: "client", label: "Stranka", sortable: false },
|
||||
{ key: "type", label: "Vrsta", sortable: false },
|
||||
{ key: "start_date", label: "Začetek", sortable: false },
|
||||
{ key: "end_date", label: "Konec", sortable: false },
|
||||
{ key: "account", label: "Stanje", align: "right", sortable: false },
|
||||
];
|
||||
|
||||
const exportDialogOpen = ref(false);
|
||||
|
|
@ -35,6 +62,21 @@ const exportColumns = ref(columns.map((col) => col.key));
|
|||
const exportError = ref("");
|
||||
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 contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
||||
const totalContracts = computed(
|
||||
|
|
@ -43,12 +85,28 @@ const totalContracts = computed(
|
|||
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
|
||||
|
||||
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) {
|
||||
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() {
|
||||
exportDialogOpen.value = true;
|
||||
exportError.value = "";
|
||||
|
|
@ -126,18 +184,68 @@ const selectedClientName = computed(() => {
|
|||
return match?.label || "";
|
||||
});
|
||||
|
||||
// React to client selection changes by visiting the same route with updated query
|
||||
watch(selectedClient, (val) => {
|
||||
const query = { search: search.value };
|
||||
if (val) {
|
||||
query.client = val;
|
||||
function isClientFilterEmpty() {
|
||||
return !selectedClient.value;
|
||||
}
|
||||
|
||||
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(
|
||||
route("segments.show", { segment: props.segment?.id ?? props.segment }),
|
||||
query,
|
||||
{ preserveState: true, preserveScroll: true, only: ["contracts"], replace: true }
|
||||
);
|
||||
});
|
||||
filterPopoverOpen.value = false;
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
search.value = "";
|
||||
selectedClient.value = "";
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) {
|
||||
|
|
@ -207,64 +315,191 @@ function extractFilenameFromHeaders(headers) {
|
|||
<template>
|
||||
<AppLayout :title="`Segment: ${segment?.name || ''}`">
|
||||
<template #header> </template>
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-4">
|
||||
<h2 class="text-lg">{{ segment.name }}</h2>
|
||||
<div class="text-sm text-gray-600 mb-4">{{ segment?.description }}</div>
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
|
||||
<AppCard
|
||||
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 -->
|
||||
<div class="mb-4 flex flex-col sm:flex-row sm:items-end gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Stranka</label>
|
||||
<AppCard
|
||||
title=""
|
||||
padding="none"
|
||||
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">
|
||||
<select
|
||||
v-model="selectedClient"
|
||||
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>
|
||||
<UsersIcon size="18" />
|
||||
<CardTitle class="uppercase">Pogodbe</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTableServer
|
||||
</template>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="contracts?.data || []"
|
||||
:data="contracts?.data || []"
|
||||
:meta="contracts || {}"
|
||||
v-model:search="search"
|
||||
route-name="segments.show"
|
||||
:route-params="{ segment: segment?.id ?? segment }"
|
||||
:query="{ client: selectedClient || undefined }"
|
||||
:only-props="['contracts']"
|
||||
:page-size-options="[10, 25, 50]"
|
||||
empty-text="Ni pogodb v tem segmentu."
|
||||
:page-size="contracts?.per_page ?? 15"
|
||||
:page-size-options="[10, 15, 25, 50]"
|
||||
row-key="uuid"
|
||||
empty-text="Ni pogodb v tem segmentu."
|
||||
per-page-param-name="per_page"
|
||||
>
|
||||
<template #toolbar-extra>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
|
||||
<template #toolbar-filters>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Popover v-model:open="filterPopoverOpen">
|
||||
<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"
|
||||
>
|
||||
<FileDown class="h-4 w-4" />
|
||||
Izvozi v Excel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Primer (client_case) cell with link when available -->
|
||||
|
||||
<template #cell-client_case="{ row }">
|
||||
<Link
|
||||
v-if="row.client_case?.uuid"
|
||||
|
|
@ -280,120 +515,131 @@ function extractFilenameFromHeaders(headers) {
|
|||
</Link>
|
||||
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Stranka (client) name -->
|
||||
<template #cell-client="{ row }">
|
||||
{{ row.client?.person?.full_name || "-" }}
|
||||
</template>
|
||||
|
||||
<!-- Vrsta (type) -->
|
||||
<template #cell-type="{ row }">
|
||||
{{ row.type?.name || "-" }}
|
||||
</template>
|
||||
|
||||
<!-- Dates formatted -->
|
||||
<template #cell-start_date="{ row }">
|
||||
{{ formatDate(row.start_date) }}
|
||||
</template>
|
||||
<template #cell-end_date="{ row }">
|
||||
{{ formatDate(row.end_date) }}
|
||||
</template>
|
||||
|
||||
<!-- Account balance formatted -->
|
||||
<template #cell-account="{ row }">
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(row.account?.balance_amount) }}
|
||||
</div>
|
||||
</template>
|
||||
</DataTableServer>
|
||||
</DataTable>
|
||||
</AppCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
||||
<template #title>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Izvoz v Excel</h3>
|
||||
<p class="text-sm text-gray-500">Izberi stolpce in obseg podatkov za izvoz.</p>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-lg font-semibold leading-6 text-foreground">Izvoz v Excel</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Izberi stolpce in obseg podatkov za izvoz.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form id="segment-export-form" class="space-y-6" @submit.prevent="submitExport">
|
||||
<div>
|
||||
<span class="text-sm font-semibold text-gray-700">Obseg podatkov</span>
|
||||
<div class="mt-2 space-y-2">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="current"
|
||||
class="text-indigo-600"
|
||||
v-model="exportScope"
|
||||
<form id="segment-export-form" class="space-y-5" @submit.prevent="submitExport">
|
||||
<div class="space-y-3 rounded-lg border bg-muted/40 p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-foreground">Obseg podatkov</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Preklopi, ali izvoziš samo trenutni pogled ali celoten segment.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-md bg-background px-3 py-2 shadow-sm"
|
||||
>
|
||||
<span class="text-xs font-medium text-muted-foreground">Stran</span>
|
||||
<Switch
|
||||
:model-value="exportScope === 'all'"
|
||||
@update:modelValue="setExportScopeFromSwitch"
|
||||
aria-label="Preklopi obseg izvoza"
|
||||
/>
|
||||
Trenutna stran ({{ currentPageCount }} zapisov)
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="all"
|
||||
class="text-indigo-600"
|
||||
v-model="exportScope"
|
||||
/>
|
||||
Celoten segment ({{ totalContracts }} zapisov)
|
||||
</label>
|
||||
<span class="text-xs font-medium text-muted-foreground">Vse</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
||||
<p class="text-sm font-semibold text-foreground">Trenutna stran</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ currentPageCount }} zapisov
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-background p-3 shadow-sm">
|
||||
<p class="text-sm font-semibold text-foreground">Celoten segment</p>
|
||||
<p class="text-xs text-muted-foreground">{{ totalContracts }} zapisov</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-gray-700">Stolpci</span>
|
||||
<label class="flex items-center gap-2 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allColumnsSelected"
|
||||
@change="toggleAllColumns($event.target.checked)"
|
||||
/>
|
||||
Označi vse
|
||||
</label>
|
||||
<div class="space-y-4 rounded-lg border bg-muted/40 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-foreground">Stolpci</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Izberi, katere stolpce želiš vključiti v izvoz.
|
||||
</p>
|
||||
</div>
|
||||
<div class="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
|
||||
v-for="col in columns"
|
||||
: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
|
||||
type="checkbox"
|
||||
name="columns[]"
|
||||
<Checkbox
|
||||
:id="`export-col-${col.key}`"
|
||||
:model-value="exportColumns.includes(col.key)"
|
||||
:value="col.key"
|
||||
v-model="exportColumns"
|
||||
class="text-indigo-600"
|
||||
@update:modelValue="(checked) => handleColumnToggle(col.key, checked)"
|
||||
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>
|
||||
</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>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-gray-600 hover:text-gray-900"
|
||||
@click="closeExportDialog"
|
||||
>
|
||||
<Button type="button" variant="ghost" @click="closeExportDialog">
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
class="gap-2"
|
||||
>
|
||||
<span v-if="!isExporting">Prenesi Excel</span>
|
||||
<span v-else>Pripravljam ...</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
|
|
|||
|
|
@ -17,3 +17,24 @@ export function fmtDateTime(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