Admin panel updated with shadcn-vue components
This commit is contained in:
parent
70a5d015e0
commit
c4d9ecb39e
|
|
@ -25,7 +25,7 @@ public function index(Request $request): Response
|
|||
{
|
||||
$packages = Package::query()
|
||||
->latest('id')
|
||||
->paginate(20);
|
||||
->paginate(25);
|
||||
// Minimal lookups for create form (active only)
|
||||
$profiles = \App\Models\SmsProfile::query()
|
||||
->where('active', true)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -657,6 +657,7 @@ public function applyToImport(Request $request, ImportTemplate $template, Import
|
|||
|
||||
$import->update([
|
||||
'import_template_id' => $template->id,
|
||||
'reactivate' => $template->reactivate,
|
||||
'meta' => $merged,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class Contract extends Model
|
|||
'end_date',
|
||||
'client_case_id',
|
||||
'type_id',
|
||||
'active',
|
||||
'description',
|
||||
'meta',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -46,76 +46,35 @@ public function resolve(array $mapped, array $context = []): mixed
|
|||
|
||||
public function process(Import $import, array $mapped, array $raw, array $context = []): array
|
||||
{
|
||||
// PHASE 4: Check for existing Contract early to prevent duplicate creation
|
||||
$reference = $mapped['reference'] ?? null;
|
||||
|
||||
if ($reference) {
|
||||
$existingContract = $this->resolutionService->getExistingContract(
|
||||
$import->client_id,
|
||||
$reference
|
||||
);
|
||||
|
||||
if ($existingContract) {
|
||||
Log::info('ContractHandler: Found existing Contract by reference', [
|
||||
'contract_id' => $existingContract->id,
|
||||
'reference' => $reference,
|
||||
]);
|
||||
|
||||
$mode = $this->getOption('update_mode', 'update');
|
||||
|
||||
if ($mode === 'skip') {
|
||||
return [
|
||||
'action' => 'skipped',
|
||||
'entity' => $existingContract,
|
||||
'message' => 'Contract already exists (skip mode)',
|
||||
];
|
||||
}
|
||||
|
||||
// Update existing contract
|
||||
$payload = $this->buildPayload($mapped, $existingContract);
|
||||
$payload = $this->mergeJsonFields($payload, $existingContract);
|
||||
$appliedFields = $this->trackAppliedFields($existingContract, $payload);
|
||||
|
||||
if (empty($appliedFields)) {
|
||||
return [
|
||||
'action' => 'skipped',
|
||||
'entity' => $existingContract,
|
||||
'message' => 'No changes detected',
|
||||
];
|
||||
}
|
||||
|
||||
$existingContract->fill($payload);
|
||||
$existingContract->save();
|
||||
|
||||
return [
|
||||
'action' => 'updated',
|
||||
'entity' => $existingContract,
|
||||
'applied_fields' => $appliedFields,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for existing contract (using resolve method which handles client scoping)
|
||||
$existing = $this->resolve($mapped, $context);
|
||||
|
||||
// Check for reactivation request
|
||||
$reactivate = $this->shouldReactivate($context);
|
||||
|
||||
// Handle reactivation if entity is soft-deleted or inactive
|
||||
if ($existing && $reactivate && $this->needsReactivation($existing)) {
|
||||
$reactivated = $this->attemptReactivation($existing, $context);
|
||||
if ($reactivated) {
|
||||
return [
|
||||
'action' => 'reactivated',
|
||||
'entity' => $existing,
|
||||
'message' => 'Contract reactivated',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we should update or skip based on mode
|
||||
$mode = $this->getOption('update_mode', 'update');
|
||||
|
||||
if ($existing) {
|
||||
|
||||
|
||||
// Check for reactivation FIRST (before update_mode check)
|
||||
$reactivate = $this->shouldReactivate($context);
|
||||
|
||||
Log::info('ContractHandler: Found existing Contract', [
|
||||
'contract_id' => $existing->id,
|
||||
'reference' => $mapped['reference'] ?? null,
|
||||
'context' => $context['import']
|
||||
]);
|
||||
|
||||
if ($reactivate && $this->needsReactivation($existing)) {
|
||||
$reactivated = $this->attemptReactivation($existing, $context);
|
||||
Log::info('ContractHandler: Reactivate', ['reactivated' => $reactivated]);
|
||||
if ($reactivated) {
|
||||
return [
|
||||
'action' => 'reactivated',
|
||||
'entity' => $existing,
|
||||
'message' => 'Contract reactivated',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check update mode
|
||||
$mode = $this->getOption('update_mode', 'update');
|
||||
if ($mode === 'skip') {
|
||||
return [
|
||||
'action' => 'skipped',
|
||||
|
|
@ -124,12 +83,9 @@ public function process(Import $import, array $mapped, array $raw, array $contex
|
|||
];
|
||||
}
|
||||
|
||||
// Update
|
||||
// Update existing contract
|
||||
$payload = $this->buildPayload($mapped, $existing);
|
||||
|
||||
// Merge JSON fields instead of overwriting
|
||||
$payload = $this->mergeJsonFields($payload, $existing);
|
||||
|
||||
$appliedFields = $this->trackAppliedFields($existing, $payload);
|
||||
|
||||
if (empty($appliedFields)) {
|
||||
|
|
@ -200,7 +156,6 @@ protected function buildPayload(array $mapped, $model): array
|
|||
// Map fields according to contract schema
|
||||
$fieldMap = [
|
||||
'reference' => 'reference',
|
||||
'title' => 'title',
|
||||
'description' => 'description',
|
||||
'amount' => 'amount',
|
||||
'currency' => 'currency',
|
||||
|
|
@ -286,7 +241,9 @@ protected function attemptReactivation(Contract $contract, array $context): bool
|
|||
$contract->restore();
|
||||
}
|
||||
|
||||
$contract->update(['active' => 1]);
|
||||
$contract->active = 1;
|
||||
|
||||
$contract->save();
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,6 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
$rowNum++;
|
||||
}
|
||||
|
||||
$isPg = DB::connection()->getDriverName() === 'pgsql';
|
||||
|
||||
// If retry mode, only process failed/invalid rows
|
||||
if ($isRetry) {
|
||||
$failedRows = ImportRow::where('import_id', $import->id)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ public function run(): void
|
|||
'key' => 'contracts',
|
||||
'canonical_root' => 'contract',
|
||||
'label' => 'Pogodbe',
|
||||
'fields' => ['reference', 'title', 'description', 'amount', 'currency', 'start_date', 'end_date', 'active'],
|
||||
'fields' => ['reference', 'start_date', 'end_date', 'description', 'type_id', 'client_case_id', 'meta'],
|
||||
'field_aliases' => [],
|
||||
'aliases' => ['contract', 'contracts'],
|
||||
'supports_multiple' => false,
|
||||
'meta' => false,
|
||||
'meta' => true,
|
||||
'rules' => [],
|
||||
'ui' => ['default_field' => 'reference', 'order' => 1],
|
||||
'handler_class' => \App\Services\Import\Handlers\ContractHandler::class,
|
||||
|
|
@ -45,7 +45,7 @@ public function run(): void
|
|||
'key' => 'accounts',
|
||||
'canonical_root' => 'account',
|
||||
'label' => 'Računi',
|
||||
'fields' => ['contract_id', 'reference', 'title', 'description', 'balance_amount', 'currency'],
|
||||
'fields' => ['reference', 'initial_amount', 'balance_amount', 'contract_id', 'contract_reference', 'type_id', 'active', 'description'],
|
||||
'field_aliases' => [],
|
||||
'aliases' => ['account', 'accounts'],
|
||||
'supports_multiple' => false,
|
||||
|
|
@ -75,8 +75,26 @@ public function run(): void
|
|||
'key' => 'payments',
|
||||
'canonical_root' => 'payment',
|
||||
'label' => 'Plačila',
|
||||
'fields' => ['account_id', 'reference', 'amount', 'currency', 'paid_at', 'payment_date'],
|
||||
'field_aliases' => ['payment_date' => 'paid_at'],
|
||||
'fields' => [
|
||||
'reference',
|
||||
'payment_nu',
|
||||
'payment_date',
|
||||
'amount',
|
||||
'type_id',
|
||||
'active',
|
||||
// optional helpers for mapping by related records
|
||||
'debt_id',
|
||||
'account_id',
|
||||
'account_reference',
|
||||
'contract_reference'
|
||||
],
|
||||
'field_aliases' => [
|
||||
'datum' => 'payment_date',
|
||||
'paid_at' => 'payment_date',
|
||||
'number' => 'payment_nu',
|
||||
'znesek' => 'amount',
|
||||
'value' => 'amount'
|
||||
],
|
||||
'aliases' => ['payment', 'payments'],
|
||||
'supports_multiple' => false,
|
||||
'meta' => false,
|
||||
|
|
|
|||
109
package-lock.json
generated
109
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@guolao/vue-monaco-editor": "^1.6.0",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
|
|
@ -27,9 +28,10 @@
|
|||
"lodash": "^4.17.21",
|
||||
"lucide-vue-next": "^0.552.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"preline": "^2.7.0",
|
||||
"quill": "^1.3.7",
|
||||
"reka-ui": "^2.6.1",
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
|
|
@ -879,6 +881,52 @@
|
|||
"vue": ">= 3.0.0 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@guolao/vue-monaco-editor": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.6.0.tgz",
|
||||
"integrity": "sha512-w2IiJ6eJGGeuIgCK6EKZOAfhHTTUB5aZwslzwGbZ5e89Hb4avx6++GkLTW8p84Sng/arFMjLPPxSBI56cFudyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.6.1",
|
||||
"vue-demi": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.2",
|
||||
"monaco-editor": ">=0.43.0",
|
||||
"vue": "^2.6.14 || >=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@guolao/vue-monaco-editor/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/vue": {
|
||||
"version": "1.7.23",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz",
|
||||
|
|
@ -1069,6 +1117,15 @@
|
|||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
||||
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
|
@ -2309,6 +2366,13 @@
|
|||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
|
|
@ -3616,6 +3680,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -4664,6 +4737,18 @@
|
|||
"vt-pbf": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/material-design-icons-iconfont": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz",
|
||||
|
|
@ -4736,6 +4821,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -5074,9 +5169,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/reka-ui": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.1.tgz",
|
||||
"integrity": "sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.7.0.tgz",
|
||||
"integrity": "sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
|
|
@ -5386,6 +5481,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/striptags": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@guolao/vue-monaco-editor": "^1.6.0",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
|
|
@ -47,9 +48,10 @@
|
|||
"lodash": "^4.17.21",
|
||||
"lucide-vue-next": "^0.552.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"preline": "^2.7.0",
|
||||
"quill": "^1.3.7",
|
||||
"reka-ui": "^2.6.1",
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
|
|
|
|||
34
resources/js/Components/ui/progress/Progress.vue
Normal file
34
resources/js/Components/ui/progress/Progress.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ProgressIndicator, ProgressRoot } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Number, null], required: false, default: 0 },
|
||||
max: { type: Number, required: false },
|
||||
getValueLabel: { type: Function, required: false },
|
||||
getValueText: { type: Function, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProgressRoot
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<ProgressIndicator
|
||||
class="h-full w-full flex-1 bg-primary transition-all"
|
||||
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
|
||||
/>
|
||||
</ProgressRoot>
|
||||
</template>
|
||||
1
resources/js/Components/ui/progress/index.js
Normal file
1
resources/js/Components/ui/progress/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Progress } from "./Progress.vue";
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faUserGroup,
|
||||
faShieldHalved,
|
||||
faArrowLeft,
|
||||
faFileWord,
|
||||
faBars,
|
||||
faGears,
|
||||
faKey,
|
||||
faEnvelope,
|
||||
faEnvelopeOpenText,
|
||||
faAt,
|
||||
faInbox,
|
||||
faFileLines,
|
||||
faMessage,
|
||||
faAddressBook,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
MenuIcon,
|
||||
ChevronDownIcon,
|
||||
ShieldCheckIcon,
|
||||
UsersIcon,
|
||||
KeyRoundIcon,
|
||||
Settings2Icon,
|
||||
FileTextIcon,
|
||||
MailOpenIcon,
|
||||
InboxIcon,
|
||||
AtSignIcon,
|
||||
BookUserIcon,
|
||||
MessageSquareIcon,
|
||||
ArrowLeftIcon,
|
||||
} from "lucide-vue-next";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import DropdownLink from "@/Components/DropdownLink.vue";
|
||||
import GlobalSearch from "@/Layouts/Partials/GlobalSearch.vue";
|
||||
|
|
@ -29,25 +29,61 @@ import { Button } from "@/Components/ui/button";
|
|||
|
||||
const props = defineProps({ title: { type: String, default: "Administrator" } });
|
||||
|
||||
// Basic state reused (simplified vs AppLayout)
|
||||
// Collapsible sidebar state (persisted when user explicitly toggles)
|
||||
const sidebarCollapsed = ref(false);
|
||||
const hasSavedSidebarPref = ref(false);
|
||||
// Mobile off-canvas state
|
||||
const isMobile = ref(false);
|
||||
const mobileSidebarOpen = ref(false);
|
||||
function handleResize() {
|
||||
|
||||
function applyAutoCollapse() {
|
||||
if (typeof window === "undefined") return;
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false;
|
||||
sidebarCollapsed.value = isMobile.value; // auto collapse on small
|
||||
isMobile.value = window.innerWidth < 1024; // Tailwind lg breakpoint
|
||||
sidebarCollapsed.value = isMobile.value;
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (typeof window !== "undefined") {
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false; // close drawer when switching to desktop
|
||||
}
|
||||
if (!hasSavedSidebarPref.value) applyAutoCollapse();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
try {
|
||||
const saved = localStorage.getItem("sidebarCollapsed");
|
||||
if (saved !== null) {
|
||||
hasSavedSidebarPref.value = true;
|
||||
sidebarCollapsed.value = saved === "1";
|
||||
} else {
|
||||
applyAutoCollapse();
|
||||
}
|
||||
} catch {}
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => window.removeEventListener("resize", handleResize));
|
||||
|
||||
watch(sidebarCollapsed, (v) => {
|
||||
if (!hasSavedSidebarPref.value) return; // don't persist auto behavior
|
||||
try {
|
||||
localStorage.setItem("sidebarCollapsed", v ? "1" : "0");
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function toggleSidebar() {
|
||||
if (isMobile.value) mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
else sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
hasSavedSidebarPref.value = true; // user explicitly chose
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
function toggleMobileSidebar() {
|
||||
mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
}
|
||||
|
||||
function handleSidebarToggleClick() {
|
||||
if (isMobile.value) toggleMobileSidebar();
|
||||
else toggleSidebar();
|
||||
}
|
||||
|
||||
const logout = () => router.post(route("logout"));
|
||||
|
|
@ -63,7 +99,7 @@ const navGroups = computed(() => [
|
|||
key: "admin.dashboard",
|
||||
label: "Pregled",
|
||||
route: "admin.index",
|
||||
icon: faShieldHalved,
|
||||
icon: ShieldCheckIcon,
|
||||
active: ["admin.index"],
|
||||
},
|
||||
],
|
||||
|
|
@ -76,14 +112,14 @@ const navGroups = computed(() => [
|
|||
key: "admin.users",
|
||||
label: "Uporabniki",
|
||||
route: "admin.users.index",
|
||||
icon: faUserGroup,
|
||||
icon: UsersIcon,
|
||||
active: ["admin.users.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.permissions.index",
|
||||
label: "Dovoljenja",
|
||||
route: "admin.permissions.index",
|
||||
icon: faKey,
|
||||
icon: KeyRoundIcon,
|
||||
active: ["admin.permissions.index", "admin.permissions.create"],
|
||||
},
|
||||
],
|
||||
|
|
@ -96,14 +132,14 @@ const navGroups = computed(() => [
|
|||
key: "admin.document-settings.index",
|
||||
label: "Nastavitve dokumentov",
|
||||
route: "admin.document-settings.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
active: ["admin.document-settings.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.document-templates.index",
|
||||
label: "Predloge dokumentov",
|
||||
route: "admin.document-templates.index",
|
||||
icon: faFileWord,
|
||||
icon: FileTextIcon,
|
||||
active: ["admin.document-templates.index"],
|
||||
},
|
||||
],
|
||||
|
|
@ -116,7 +152,7 @@ const navGroups = computed(() => [
|
|||
key: "admin.email-templates.index",
|
||||
label: "Email predloge",
|
||||
route: "admin.email-templates.index",
|
||||
icon: faEnvelopeOpenText,
|
||||
icon: MailOpenIcon,
|
||||
active: [
|
||||
"admin.email-templates.index",
|
||||
"admin.email-templates.create",
|
||||
|
|
@ -127,14 +163,14 @@ const navGroups = computed(() => [
|
|||
key: "admin.email-logs.index",
|
||||
label: "Email dnevniki",
|
||||
route: "admin.email-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
active: ["admin.email-logs.index", "admin.email-logs.show"],
|
||||
},
|
||||
{
|
||||
key: "admin.mail-profiles.index",
|
||||
label: "Mail profili",
|
||||
route: "admin.mail-profiles.index",
|
||||
icon: faAt,
|
||||
icon: AtSignIcon,
|
||||
active: ["admin.mail-profiles.index"],
|
||||
},
|
||||
],
|
||||
|
|
@ -147,7 +183,7 @@ const navGroups = computed(() => [
|
|||
key: "admin.sms-templates.index",
|
||||
label: "SMS predloge",
|
||||
route: "admin.sms-templates.index",
|
||||
icon: faFileLines,
|
||||
icon: FileTextIcon,
|
||||
active: [
|
||||
"admin.sms-templates.index",
|
||||
"admin.sms-templates.create",
|
||||
|
|
@ -158,28 +194,28 @@ const navGroups = computed(() => [
|
|||
key: "admin.sms-logs.index",
|
||||
label: "SMS dnevniki",
|
||||
route: "admin.sms-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
active: ["admin.sms-logs.index", "admin.sms-logs.show"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-senders.index",
|
||||
label: "SMS pošiljatelji",
|
||||
route: "admin.sms-senders.index",
|
||||
icon: faAddressBook,
|
||||
icon: BookUserIcon,
|
||||
active: ["admin.sms-senders.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-profiles.index",
|
||||
label: "SMS profili",
|
||||
route: "admin.sms-profiles.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
active: ["admin.sms-profiles.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.packages.index",
|
||||
label: "SMS paketi",
|
||||
route: "admin.packages.index",
|
||||
icon: faMessage,
|
||||
icon: MessageSquareIcon,
|
||||
active: ["admin.packages.index", "admin.packages.show"],
|
||||
},
|
||||
],
|
||||
|
|
@ -215,7 +251,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-gray-200 bg-white"
|
||||
>
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
|
|
@ -230,49 +268,49 @@ function isActive(patterns) {
|
|||
</Link>
|
||||
</div>
|
||||
<nav class="py-4 overflow-y-auto">
|
||||
<div v-for="group in navGroups" :key="group.key" class="mt-2 first:mt-0 px-2">
|
||||
<p
|
||||
v-if="!sidebarCollapsed"
|
||||
class="px-4 py-1.5 mb-1 mt-4 first:mt-0 text-[11px] font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
{{ group.label }}
|
||||
</p>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in group.items" :key="item.key">
|
||||
<Link
|
||||
:href="route(item.route)"
|
||||
:title="item.label"
|
||||
: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',
|
||||
]"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="item.icon"
|
||||
<ul class="space-y-4 px-2">
|
||||
<li v-for="group in navGroups" :key="group.label">
|
||||
<div
|
||||
v-if="!sidebarCollapsed"
|
||||
class="px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-sidebar-foreground/60"
|
||||
>
|
||||
{{ group.label }}
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in group.items" :key="item.key">
|
||||
<Link
|
||||
:href="route(item.route)"
|
||||
:title="item.label"
|
||||
:class="[
|
||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
||||
isActive(item.active) ? 'text-primary-600' : 'text-gray-500',
|
||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||
isActive(item.active)
|
||||
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
v-if="!sidebarCollapsed"
|
||||
class="truncate transition-opacity"
|
||||
:class="{ 'font-medium': isActive(item.active) }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="w-5 h-5 shrink-0 transition-colors"
|
||||
/>
|
||||
<span
|
||||
v-if="!sidebarCollapsed"
|
||||
class="truncate transition-opacity"
|
||||
:class="{ 'font-medium': isActive(item.active) }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-6 border-t border-gray-200 pt-4 space-y-2 px-4">
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 hover:underline flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="text-xs hover:underline flex items-center gap-2 px-3 py-2 rounded-lg text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all duration-150"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faArrowLeft" class="w-3.5 h-3.5" />
|
||||
<ArrowLeftIcon size="18" />
|
||||
<span v-if="!sidebarCollapsed">Nazaj na aplikacijo</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -287,10 +325,11 @@ function isActive(patterns) {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="toggleSidebar"
|
||||
@click="handleSidebarToggleClick"
|
||||
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faBars" class="w-5 h-5" />
|
||||
<MenuIcon />
|
||||
</Button>
|
||||
<h1 class="text-base font-semibold text-gray-900 hidden sm:block">
|
||||
{{ title }}
|
||||
|
|
@ -314,27 +353,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>
|
||||
|
|
@ -358,10 +379,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"
|
||||
|
|
@ -376,10 +394,7 @@ function isActive(patterns) {
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<GlobalSearch :open="false" />
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,32 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import {
|
||||
Settings2Icon,
|
||||
SaveIcon,
|
||||
CheckCircle2Icon,
|
||||
AlertCircleIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Switch } from "@/Components/ui/switch";
|
||||
import { Alert, AlertDescription } from "@/Components/ui/alert";
|
||||
|
||||
const props = defineProps({ settings: Object, defaults: Object });
|
||||
const form = useForm({
|
||||
|
|
@ -70,92 +96,147 @@ function submit() {
|
|||
|
||||
<template>
|
||||
<AdminLayout title="Nastavitve dokumentov">
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
<h1 class="text-2xl font-semibold">Nastavitve dokumentov</h1>
|
||||
<form @submit.prevent="submit" class="space-y-6 bg-white p-6 border rounded">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Vzorec imena</span>
|
||||
<input v-model="form.file_name_pattern" class="border rounded px-3 py-2" />
|
||||
<span class="text-xs text-gray-500"
|
||||
>Podprti placeholderji: {slug} {version} {generation.date}
|
||||
{generation.timestamp}</span
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<span v-if="form.errors.file_name_pattern" class="text-xs text-rose-600">{{
|
||||
form.errors.file_name_pattern
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Privzeti datum format</span>
|
||||
<input v-model="form.date_format" class="border rounded px-3 py-2" />
|
||||
<span class="text-xs text-gray-500">npr. Y-m-d ali d.m.Y</span>
|
||||
<span v-if="form.errors.date_format" class="text-xs text-rose-600">{{
|
||||
form.errors.date_format
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Politika nerešenih</span>
|
||||
<select v-model="form.unresolved_policy" class="border rounded px-3 py-2">
|
||||
<option value="fail">Fail</option>
|
||||
<option value="blank">Blank</option>
|
||||
<option value="keep">Keep</option>
|
||||
</select>
|
||||
<span v-if="form.errors.unresolved_policy" class="text-xs text-rose-600">{{
|
||||
form.errors.unresolved_policy
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 mt-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.preview_enabled"
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
/>
|
||||
<span class="text-sm font-medium">Omogoči predoglede</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Whitelist (JSON)</span>
|
||||
<textarea
|
||||
v-model="form.whitelist"
|
||||
rows="8"
|
||||
class="font-mono text-xs border rounded p-2"
|
||||
></textarea>
|
||||
<span v-if="whitelistError" class="text-xs text-rose-600">{{
|
||||
whitelistError
|
||||
}}</span>
|
||||
<span v-else-if="form.errors.whitelist" class="text-xs text-rose-600">{{
|
||||
form.errors.whitelist
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Date formats override (JSON)</span>
|
||||
<textarea
|
||||
v-model="form.date_formats"
|
||||
rows="8"
|
||||
class="font-mono text-xs border rounded p-2"
|
||||
></textarea>
|
||||
<span class="text-xs text-gray-500"
|
||||
>Primer: {"contract.start_date":"d.m.Y"}</span
|
||||
>
|
||||
<span v-if="dateFormatsError" class="text-xs text-rose-600">{{
|
||||
dateFormatsError
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
:disabled="form.processing"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{{ form.processing ? "Shranjevanje..." : "Shrani" }}
|
||||
</button>
|
||||
<span v-if="form.wasSuccessful" class="text-sm text-emerald-600"
|
||||
>Shranjeno</span
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<Settings2Icon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Nastavitve dokumentov</CardTitle>
|
||||
<CardDescription>
|
||||
Konfiguracija generiranja dokumentov, vzorcev imen in formatiranja
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<!-- Basic Settings -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Osnovne nastavitve</h3>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="file_name_pattern">Vzorec imena datoteke</Label>
|
||||
<Input
|
||||
id="file_name_pattern"
|
||||
v-model="form.file_name_pattern"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Podprti placeholderji: {slug} {version} {generation.date}
|
||||
{generation.timestamp}
|
||||
</p>
|
||||
<p
|
||||
v-if="form.errors.file_name_pattern"
|
||||
class="text-sm text-destructive"
|
||||
>
|
||||
{{ form.errors.file_name_pattern }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="date_format">Privzeti datum format</Label>
|
||||
<Input
|
||||
id="date_format"
|
||||
v-model="form.date_format"
|
||||
placeholder="Y-m-d"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">npr. Y-m-d ali d.m.Y</p>
|
||||
<p v-if="form.errors.date_format" class="text-sm text-destructive">
|
||||
{{ form.errors.date_format }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="unresolved_policy">Politika nerešenih spremenljivk</Label>
|
||||
<Select v-model="form.unresolved_policy">
|
||||
<SelectTrigger id="unresolved_policy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fail">Fail (napaka)</SelectItem>
|
||||
<SelectItem value="blank">Blank (prazno)</SelectItem>
|
||||
<SelectItem value="keep">Keep (obdrži)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p
|
||||
v-if="form.errors.unresolved_policy"
|
||||
class="text-sm text-destructive"
|
||||
>
|
||||
{{ form.errors.unresolved_policy }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-8">
|
||||
<Switch id="preview_enabled" v-model="form.preview_enabled" />
|
||||
<Label for="preview_enabled" class="cursor-pointer">
|
||||
Omogoči predoglede dokumentov
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON Configuration -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Napišrana konfiguracija (JSON)</h3>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="whitelist">Whitelist (dovoljeni tokeni)</Label>
|
||||
<Textarea
|
||||
id="whitelist"
|
||||
v-model="form.whitelist"
|
||||
rows="10"
|
||||
class="font-mono text-xs"
|
||||
placeholder='{"entity": ["column1", "column2"]}'
|
||||
/>
|
||||
<Alert v-if="whitelistError" variant="destructive" class="py-2">
|
||||
<AlertCircleIcon class="h-4 w-4" />
|
||||
<AlertDescription>{{ whitelistError }}</AlertDescription>
|
||||
</Alert>
|
||||
<p v-else-if="form.errors.whitelist" class="text-sm text-destructive">
|
||||
{{ form.errors.whitelist }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="date_formats">Date formats override (JSON)</Label>
|
||||
<Textarea
|
||||
id="date_formats"
|
||||
v-model="form.date_formats"
|
||||
rows="10"
|
||||
class="font-mono text-xs"
|
||||
placeholder='{"contract.start_date": "d.m.Y"}'
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Primer: {"contract.start_date":"d.m.Y"}
|
||||
</p>
|
||||
<Alert v-if="dateFormatsError" variant="destructive" class="py-2">
|
||||
<AlertCircleIcon class="h-4 w-4" />
|
||||
<AlertDescription>{{ dateFormatsError }}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-3 pt-4">
|
||||
<Button :disabled="form.processing" type="submit">
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
{{ form.processing ? "Shranjevanje..." : "Shrani nastavitve" }}
|
||||
</Button>
|
||||
<Alert v-if="form.wasSuccessful" class="py-2 px-3 w-auto">
|
||||
<CheckCircle2Icon class="h-4 w-4 text-green-600" />
|
||||
<AlertDescription class="text-green-600">Shranjeno</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
import { Settings2Icon, FileTextIcon, CalendarIcon, AlertTriangleIcon, ListIcon, PencilIcon } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card'
|
||||
import { Badge } from '@/Components/ui/badge'
|
||||
import { Button } from '@/Components/ui/button'
|
||||
import { Separator } from '@/Components/ui/separator'
|
||||
|
||||
const props = defineProps({
|
||||
config: Object,
|
||||
})
|
||||
|
|
@ -7,26 +14,95 @@ const props = defineProps({
|
|||
|
||||
<template>
|
||||
<AdminLayout title="Nastavitve dokumentov">
|
||||
<h1 class="text-2xl font-semibold mb-4">Nastavitve dokumentov</h1>
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-white rounded border">
|
||||
<h2 class="font-medium mb-2">Privzeti vzorci</h2>
|
||||
<p class="text-sm text-gray-600">Ime datoteke: <code class="px-1 bg-gray-100 rounded">{{ config.file_name_pattern }}</code></p>
|
||||
<p class="text-sm text-gray-600">Format datuma: <code class="px-1 bg-gray-100 rounded">{{ config.date_format }}</code></p>
|
||||
<p class="text-sm text-gray-600">Politika nerešenih: <code class="px-1 bg-gray-100 rounded">{{ config.unresolved_policy }}</code></p>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded border">
|
||||
<h2 class="font-medium mb-2">Dovoljeni tokeni (whitelist)</h2>
|
||||
<div v-for="(cols, entity) in config.whitelist" :key="entity" class="mb-3">
|
||||
<div class="text-sm font-semibold">{{ entity }}</div>
|
||||
<div class="text-xs text-gray-600" v-if="cols.length">{{ cols.join(', ') }}</div>
|
||||
<div class="text-xs text-gray-400" v-else>(brez specifičnih stolpcev)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded border">
|
||||
<h2 class="font-medium mb-2">Uredi (prihaja)</h2>
|
||||
<p class="text-xs text-gray-500">Za urejanje bo dodan obrazec. Trenutno spremembe izvedite v <code>config/documents.php</code>.</p>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<Settings2Icon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Nastavitve dokumentov</CardTitle>
|
||||
<CardDescription>
|
||||
Sistemska konfiguracija za generiranje in upravljanje dokumentov
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" as-child>
|
||||
<Link :href="route('admin.document-settings.edit')">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Basic Configuration -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold flex items-center gap-2">
|
||||
<FileTextIcon class="h-4 w-4" />
|
||||
Privzeti vzorci
|
||||
</h3>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start justify-between p-3 rounded-lg border bg-muted/50">
|
||||
<div>
|
||||
<p class="text-sm font-medium">Ime datoteke</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Vzorec za generiranje imen</p>
|
||||
</div>
|
||||
<Badge variant="secondary" class="font-mono text-xs">
|
||||
{{ config.file_name_pattern }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-start justify-between p-3 rounded-lg border bg-muted/50">
|
||||
<div>
|
||||
<p class="text-sm font-medium">Format datuma</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Privzeto formatiranje datumov</p>
|
||||
</div>
|
||||
<Badge variant="secondary" class="font-mono text-xs">
|
||||
{{ config.date_format }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-start justify-between p-3 rounded-lg border bg-muted/50">
|
||||
<div>
|
||||
<p class="text-sm font-medium">Politika nerešenih</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Vedenje pri nerešenih spremenljivkah</p>
|
||||
</div>
|
||||
<Badge variant="secondary" class="font-mono text-xs">
|
||||
{{ config.unresolved_policy }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Whitelist Configuration -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold flex items-center gap-2">
|
||||
<ListIcon class="h-4 w-4" />
|
||||
Dovoljeni tokeni (whitelist)
|
||||
</h3>
|
||||
<div class="grid gap-3">
|
||||
<Card v-for="(cols, entity) in config.whitelist" :key="entity" class="border-muted">
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm font-mono">{{ entity }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="cols.length" class="flex flex-wrap gap-1.5">
|
||||
<Badge v-for="col in cols" :key="col" variant="outline" class="text-xs font-mono">
|
||||
{{ col }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p v-else class="text-xs text-muted-foreground italic">
|
||||
(brez specifičnih stolpcev)
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,327 +1,352 @@
|
|||
<template>
|
||||
<AdminLayout title="Uredi predlogo">
|
||||
<div class="mb-6 flex flex-col lg:flex-row lg:items-start gap-6">
|
||||
<div class="flex-1 min-w-[320px]">
|
||||
<div class="flex items-start justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">{{ template.name }}</h1>
|
||||
<p class="text-xs text-gray-500 mt-1 flex flex-wrap gap-3">
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><span class="text-gray-400">Slug:</span
|
||||
><span class="font-medium">{{ template.slug }}</span></span
|
||||
>
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><span class="text-gray-400">Verzija:</span
|
||||
><span class="font-medium">v{{ template.version }}</span></span
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1"
|
||||
:class="template.active ? 'text-emerald-600' : 'text-gray-400'"
|
||||
><span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="template.active ? 'bg-emerald-500' : 'bg-gray-300'"
|
||||
/>
|
||||
{{ template.active ? "Aktivna" : "Neaktivna" }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="toggleActive" class="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
:class="[btnBase, template.active ? btnWarn : btnOutline]"
|
||||
:disabled="toggleForm.processing"
|
||||
>
|
||||
<span v-if="toggleForm.processing">...</span>
|
||||
<span v-else>{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}</span>
|
||||
</button>
|
||||
<Link
|
||||
:href="route('admin.document-templates.show', template.id)"
|
||||
:class="[btnBase, btnOutline]"
|
||||
>Ogled</Link
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[320px] space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<FileTextIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{{ template.name }}</CardTitle>
|
||||
<CardDescription class="flex flex-wrap gap-3 mt-1">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>Slug:</span>
|
||||
<Badge variant="secondary" class="text-xs">{{ template.slug }}</Badge>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>Verzija:</span>
|
||||
<Badge variant="secondary" class="text-xs">v{{ template.version }}</Badge>
|
||||
</span>
|
||||
<Badge :variant="template.active ? 'default' : 'outline'" class="text-xs">
|
||||
{{ template.active ? "Aktivna" : "Neaktivna" }}
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<form @submit.prevent="toggleActive">
|
||||
<Button
|
||||
type="submit"
|
||||
:variant="template.active ? 'destructive' : 'default'"
|
||||
size="sm"
|
||||
:disabled="toggleForm.processing"
|
||||
>
|
||||
<PowerOffIcon v-if="template.active" class="h-4 w-4 mr-2" />
|
||||
<Power v-else class="h-4 w-4 mr-2" />
|
||||
{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}
|
||||
</Button>
|
||||
</form>
|
||||
<Button size="sm" variant="outline" as-child>
|
||||
<Link :href="route('admin.document-templates.show', template.id)">
|
||||
<EyeIcon class="h-4 w-4 mr-2" />
|
||||
Ogled
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-8">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<!-- Osnovno -->
|
||||
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
||||
Osnovne nastavitve
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600"
|
||||
>Izlazna datoteka (pattern)</span
|
||||
>
|
||||
<input
|
||||
v-model="form.output_filename_pattern"
|
||||
type="text"
|
||||
class="input input-bordered w-full input-sm"
|
||||
placeholder="POVRACILO_{contract.reference}"
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<Settings2Icon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Osnovne nastavitve</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="output_filename_pattern">Izlazna datoteka (pattern)</Label>
|
||||
<Input
|
||||
id="output_filename_pattern"
|
||||
v-model="form.output_filename_pattern"
|
||||
placeholder="POVRACILO_{contract.reference}"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Tokens npr. {contract.reference}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="date_format">Privzeti format datuma</Label>
|
||||
<Input
|
||||
id="date_format"
|
||||
v-model="form.date_format"
|
||||
placeholder="d.m.Y"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="fail_on_unresolved"
|
||||
:default-value="form.fail_on_unresolved"
|
||||
@update:model-value="(val) => (form.fail_on_unresolved = val)"
|
||||
/>
|
||||
<span class="text-[11px] text-gray-500"
|
||||
>Tokens npr. {contract.reference}</span
|
||||
>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600"
|
||||
>Privzeti format datuma</span
|
||||
>
|
||||
<input
|
||||
v-model="form.date_format"
|
||||
type="text"
|
||||
class="input input-bordered w-full input-sm"
|
||||
placeholder="d.m.Y"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-xs font-medium text-gray-600">
|
||||
<input
|
||||
id="fail_on_unresolved"
|
||||
type="checkbox"
|
||||
v-model="form.fail_on_unresolved"
|
||||
class="checkbox checkbox-xs"
|
||||
/>
|
||||
<span>Prekini če token ni rešen (fail on unresolved)</span>
|
||||
</label>
|
||||
</div>
|
||||
<Label for="fail_on_unresolved" class="cursor-pointer font-normal">
|
||||
Prekini če token ni rešen (fail on unresolved)
|
||||
</Label>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Formatiranje -->
|
||||
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
||||
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
||||
Formatiranje
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-3 gap-5">
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Decimalna mesta</span>
|
||||
<input
|
||||
v-model.number="form.number_decimals"
|
||||
type="number"
|
||||
min="0"
|
||||
max="6"
|
||||
class="input input-bordered w-full input-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Decimalni separator</span>
|
||||
<input
|
||||
v-model="form.decimal_separator"
|
||||
type="text"
|
||||
maxlength="2"
|
||||
class="input input-bordered w-full input-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Tisocice separator</span>
|
||||
<input
|
||||
v-model="form.thousands_separator"
|
||||
type="text"
|
||||
maxlength="2"
|
||||
class="input input-bordered w-full input-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Znak valute</span>
|
||||
<input
|
||||
v-model="form.currency_symbol"
|
||||
type="text"
|
||||
maxlength="8"
|
||||
class="input input-bordered w-full input-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Pozicija valute</span>
|
||||
<select
|
||||
v-model="form.currency_position"
|
||||
class="select select-bordered select-sm w-full"
|
||||
>
|
||||
<option :value="null">(privzeto)</option>
|
||||
<option value="before">Pred</option>
|
||||
<option value="after">Za</option>
|
||||
</select>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-center gap-2 space-y-0 pt-6 text-xs font-medium text-gray-600"
|
||||
>
|
||||
<input
|
||||
id="currency_space"
|
||||
type="checkbox"
|
||||
v-model="form.currency_space"
|
||||
class="checkbox checkbox-xs"
|
||||
/>
|
||||
<span>Presledek pred/za valuto</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<FileTextIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Formatiranje</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="number_decimals">Decimalna mesta</Label>
|
||||
<Input
|
||||
id="number_decimals"
|
||||
v-model.number="form.number_decimals"
|
||||
type="number"
|
||||
min="0"
|
||||
max="6"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="decimal_separator">Decimalni separator</Label>
|
||||
<Input
|
||||
id="decimal_separator"
|
||||
v-model="form.decimal_separator"
|
||||
maxlength="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="thousands_separator">Tisočice separator</Label>
|
||||
<Input
|
||||
id="thousands_separator"
|
||||
v-model="form.thousands_separator"
|
||||
maxlength="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="currency_symbol">Znak valute</Label>
|
||||
<Input
|
||||
id="currency_symbol"
|
||||
v-model="form.currency_symbol"
|
||||
maxlength="8"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="currency_position">Pozicija valute</Label>
|
||||
<Select v-model="form.currency_position">
|
||||
<SelectTrigger id="currency_position">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">(privzeto)</SelectItem>
|
||||
<SelectItem value="before">Pred</SelectItem>
|
||||
<SelectItem value="after">Za</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pt-8">
|
||||
<Checkbox
|
||||
id="currency_space"
|
||||
:default-value="form.currency_space"
|
||||
@update:model-value="(val) => (form.currency_space = val)"
|
||||
/>
|
||||
<Label for="currency_space" class="cursor-pointer font-normal">
|
||||
Presledek pred/za valuto
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Aktivnost -->
|
||||
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
||||
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
||||
Aktivnost
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Akcija</span>
|
||||
<select
|
||||
v-model="form.action_id"
|
||||
class="select select-bordered select-sm w-full"
|
||||
@change="handleActionChange"
|
||||
>
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="a in actions" :key="a.id" :value="a.id">
|
||||
{{ a.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1 block">
|
||||
<span class="text-xs font-medium text-gray-600">Odločitev</span>
|
||||
<select
|
||||
v-model="form.decision_id"
|
||||
class="select select-bordered select-sm w-full"
|
||||
:disabled="!currentActionDecisions.length"
|
||||
>
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="d in currentActionDecisions" :key="d.id" :value="d.id">
|
||||
{{ d.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1 md:col-span-2 block">
|
||||
<span class="text-xs font-medium text-gray-600"
|
||||
>Predloga opombe aktivnosti</span
|
||||
>
|
||||
<textarea
|
||||
v-model="form.activity_note_template"
|
||||
rows="3"
|
||||
class="textarea textarea-bordered w-full text-xs"
|
||||
placeholder="Besedilo aktivnosti..."
|
||||
/>
|
||||
<span class="text-[11px] text-gray-500"
|
||||
>Tokeni npr. {contract.reference}</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<ActivityIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Aktivnost</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="action_id">Akcija</Label>
|
||||
<Select v-model="form.action_id" @update:model-value="handleActionChange">
|
||||
<SelectTrigger id="action_id">
|
||||
<SelectValue placeholder="(brez)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">(brez)</SelectItem>
|
||||
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">
|
||||
{{ a.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="decision_id">Odločitev</Label>
|
||||
<Select v-model="form.decision_id" :disabled="!currentActionDecisions.length">
|
||||
<SelectTrigger id="decision_id">
|
||||
<SelectValue placeholder="(brez)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">(brez)</SelectItem>
|
||||
<SelectItem v-for="d in currentActionDecisions" :key="d.id" :value="d.id">
|
||||
{{ d.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<Label for="activity_note_template">Predloga opombe aktivnosti</Label>
|
||||
<Textarea
|
||||
id="activity_note_template"
|
||||
v-model="form.activity_note_template"
|
||||
rows="3"
|
||||
placeholder="Besedilo aktivnosti..."
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Tokeni npr. {contract.reference}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Custom tokens defaults -->
|
||||
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
||||
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
||||
Custom tokens (privzete vrednosti)
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" :class="[btnBase, btnOutline]" @click="addCustomDefault">
|
||||
Dodaj vrstico
|
||||
</button>
|
||||
<CodeIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Custom tokens (privzete vrednosti)</CardTitle>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<Button type="button" variant="outline" size="sm" @click="addCustomDefault">
|
||||
Dodaj vrstico
|
||||
</Button>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div
|
||||
v-for="(row, idx) in customRows"
|
||||
:key="idx"
|
||||
class="grid grid-cols-12 items-center gap-2"
|
||||
class="grid grid-cols-12 items-start gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.key"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full col-span-4"
|
||||
placeholder="custom ključ (npr. order_id)"
|
||||
/>
|
||||
<template v-if="row.type === 'text'">
|
||||
<textarea
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Input
|
||||
v-model="row.key"
|
||||
placeholder="custom ključ (npr. order_id)"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-5 space-y-1">
|
||||
<Textarea
|
||||
v-if="row.type === 'text'"
|
||||
v-model="row.value"
|
||||
rows="3"
|
||||
class="textarea textarea-bordered w-full text-xs col-span-5"
|
||||
placeholder="privzeta vrednost"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
<Input
|
||||
v-else
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full col-span-5"
|
||||
placeholder="privzeta vrednost"
|
||||
/>
|
||||
</template>
|
||||
<select v-model="row.type" class="select select-bordered select-sm w-full col-span-2">
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="text">text</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-ghost btn-xs col-span-1" @click="removeCustomDefault(idx)">✕</button>
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Select v-model="row.type">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">string</SelectItem>
|
||||
<SelectItem value="number">number</SelectItem>
|
||||
<SelectItem value="date">date</SelectItem>
|
||||
<SelectItem value="text">text</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="col-span-1 flex items-center pt-2">
|
||||
<Button type="button" variant="ghost" size="icon" @click="removeCustomDefault(idx)">
|
||||
<XIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11px] text-gray-500">
|
||||
Uporabite v predlogi kot <code v-pre>{{custom.your_key}}</code>. Manjkajoče vrednosti se privzeto izpraznijo.
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Uporabite v predlogi kot <code class="px-1 py-0.5 bg-muted rounded text-xs" v-pre>{{custom.your_key}}</code>. Manjkajoče vrednosti se privzeto izpraznijo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
:class="[btnBase, btnPrimary]"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
<span v-if="form.processing">Shranjevanje…</span>
|
||||
<span v-else>Shrani spremembe</span>
|
||||
</button>
|
||||
<Link
|
||||
:href="route('admin.document-templates.show', template.id)"
|
||||
:class="[btnBase, btnOutline]"
|
||||
>Prekliči</Link
|
||||
>
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
{{ form.processing ? "Shranjevanje…" : "Shrani spremembe" }}
|
||||
</Button>
|
||||
<Button variant="outline" as-child>
|
||||
<Link :href="route('admin.document-templates.show', template.id)">
|
||||
<XIcon class="h-4 w-4 mr-2" />
|
||||
Prekliči
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Side meta panel -->
|
||||
<aside class="w-full lg:w-72 space-y-6">
|
||||
<div class="bg-white border rounded-lg shadow-sm p-4 space-y-3">
|
||||
<h3 class="text-xs font-semibold tracking-wide text-gray-600 uppercase">
|
||||
Meta
|
||||
</h3>
|
||||
<ul class="text-xs text-gray-600 space-y-1">
|
||||
<li>
|
||||
<span class="text-gray-400">Velikost:</span>
|
||||
<span class="font-medium"
|
||||
>{{ (template.file_size / 1024).toFixed(1) }} KB</span
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Meta podatki</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Velikost:</span>
|
||||
<Badge variant="secondary">{{ (template.file_size / 1024).toFixed(1) }} KB</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Hash:</span>
|
||||
<code class="text-xs">{{ template.file_hash?.substring(0, 12) }}…</code>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Engine:</span>
|
||||
<Badge variant="outline">{{ template.engine }}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Button variant="outline" size="sm" class="w-full" as-child>
|
||||
<a :href="'/storage/' + template.file_path" target="_blank">
|
||||
Prenesi izvorni DOCX
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card v-if="template.tokens?.length">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Tokens</CardTitle>
|
||||
<CardDescription>{{ template.tokens.length }} tokenov</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-wrap gap-1.5 max-h-48 overflow-auto">
|
||||
<Badge
|
||||
v-for="t in template.tokens"
|
||||
:key="t"
|
||||
variant="secondary"
|
||||
class="font-mono text-xs"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Hash:</span>
|
||||
<span class="font-mono">{{ template.file_hash?.substring(0, 12) }}…</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Engine:</span>
|
||||
<span class="font-medium">{{ template.engine }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
:href="'/storage/' + template.file_path"
|
||||
target="_blank"
|
||||
class="text-[11px] inline-flex items-center gap-1 text-indigo-600 hover:underline"
|
||||
>Prenesi izvorni DOCX →</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="template.tokens?.length"
|
||||
class="bg-white border rounded-lg shadow-sm p-4"
|
||||
>
|
||||
<h3 class="text-xs font-semibold tracking-wide text-gray-600 uppercase mb-2">
|
||||
Tokens ({{ template.tokens.length }})
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-1.5 max-h-48 overflow-auto pr-1">
|
||||
<span
|
||||
v-for="t in template.tokens"
|
||||
:key="t"
|
||||
class="px-1.5 py-0.5 bg-gray-100 rounded text-[11px] font-mono"
|
||||
>{{ t }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{{ t }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
|
|
@ -331,6 +356,16 @@
|
|||
import { computed, reactive } from "vue";
|
||||
import { useForm, Link, router } from "@inertiajs/vue3";
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Settings2Icon, FileTextIcon, ActivityIcon, CodeIcon, SaveIcon, XIcon, EyeIcon, Power, PowerOffIcon } from "lucide-vue-next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
// Button style utility classes
|
||||
const btnBase =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Link, useForm } from "@inertiajs/vue3";
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
UploadIcon,
|
||||
FileTextIcon,
|
||||
Power,
|
||||
PowerOffIcon,
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Progress } from "@/Components/ui/progress";
|
||||
|
||||
const props = defineProps({
|
||||
templates: { type: Array, default: () => [] },
|
||||
|
|
@ -9,7 +37,7 @@ const props = defineProps({
|
|||
|
||||
// Upload form state
|
||||
const uploadForm = useForm({ name: "", slug: "", file: null });
|
||||
const selectedSlug = ref("");
|
||||
const selectedSlug = ref(null);
|
||||
const uniqueSlugs = computed(() => {
|
||||
const s = new Set(props.templates.map((t) => t.slug));
|
||||
return Array.from(s).sort();
|
||||
|
|
@ -60,186 +88,164 @@ const groups = computed(() => {
|
|||
|
||||
<template>
|
||||
<AdminLayout title="Dokumentne predloge">
|
||||
<div class="mb-8 space-y-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Header & Upload -->
|
||||
<div class="flex flex-col xl:flex-row xl:items-start gap-6">
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<h1 class="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<span>Dokumentne predloge</span>
|
||||
<span
|
||||
class="text-xs font-medium bg-gray-200 text-gray-600 px-2 py-0.5 rounded"
|
||||
>{{ groups.length }} skupin</span
|
||||
>
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 mt-1 max-w-prose">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Dokumentne predloge</h1>
|
||||
<Badge variant="secondary">{{ groups.length }} skupin</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground max-w-prose">
|
||||
Upravljaj verzije DOCX predlog. Naloži novo verzijo obstoječega sluga ali
|
||||
ustvari popolnoma novo predlogo.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
@submit.prevent="submitUpload"
|
||||
class="flex-1 bg-white/70 backdrop-blur border rounded-lg shadow-sm p-4 flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<span class="i-lucide-upload-cloud w-4 h-4" /> Nova / nova verzija
|
||||
</h2>
|
||||
<div
|
||||
v-if="uploadForm.progress"
|
||||
class="w-40 h-1 bg-gray-200 rounded overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-indigo-500 transition-all"
|
||||
:style="{ width: uploadForm.progress.percentage + '%' }"
|
||||
<Card class="flex-1">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<UploadIcon class="h-4 w-4" />
|
||||
Nova / nova verzija
|
||||
</CardTitle>
|
||||
<Progress
|
||||
v-if="uploadForm.progress"
|
||||
:model-value="uploadForm.progress.percentage"
|
||||
class="w-40 h-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-5 gap-3 text-xs">
|
||||
<div class="md:col-span-1">
|
||||
<label class="block font-medium mb-1">Obstoječi slug</label>
|
||||
<select
|
||||
v-model="selectedSlug"
|
||||
class="select select-bordered select-sm w-full"
|
||||
>
|
||||
<option value="">(nov)</option>
|
||||
<option v-for="s in uniqueSlugs" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-1">
|
||||
<label class="block font-medium mb-1">Nov slug</label>
|
||||
<input
|
||||
v-model="uploadForm.slug"
|
||||
:disabled="!!selectedSlug"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="opomin"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-1">
|
||||
<label class="block font-medium mb-1">Naziv</label>
|
||||
<input
|
||||
v-model="uploadForm.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Ime predloge"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2 flex items-end">
|
||||
<label class="w-full">
|
||||
<input
|
||||
id="docx-upload-input"
|
||||
@change="handleFile"
|
||||
type="file"
|
||||
accept=".docx"
|
||||
class="file-input file-input-bordered file-input-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span class="text-[11px] text-gray-500" v-if="!uploadForm.file"
|
||||
>Izberi DOCX datoteko…</span
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-primary"
|
||||
:disabled="
|
||||
uploadForm.processing ||
|
||||
!uploadForm.file ||
|
||||
(!uploadForm.slug && !selectedSlug)
|
||||
"
|
||||
>
|
||||
<span v-if="uploadForm.processing">Nalaganje…</span>
|
||||
<span v-else>Shrani verzijo</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="uploadForm.errors.file" class="text-rose-600 text-xs">
|
||||
{{ uploadForm.errors.file }}
|
||||
</div>
|
||||
</form>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="submitUpload" class="space-y-4">
|
||||
<div class="grid md:grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="existing_slug">Obstoječi slug</Label>
|
||||
<Select v-model="selectedSlug">
|
||||
<SelectTrigger id="existing_slug">
|
||||
<SelectValue placeholder="(nov)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="s in uniqueSlugs" :key="s" :value="s">{{
|
||||
s
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="new_slug">Nov slug</Label>
|
||||
<Input
|
||||
id="new_slug"
|
||||
v-model="uploadForm.slug"
|
||||
:disabled="!!selectedSlug"
|
||||
placeholder="opomin"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="template_name">Naziv</Label>
|
||||
<Input
|
||||
id="template_name"
|
||||
v-model="uploadForm.name"
|
||||
placeholder="Ime predloge"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2 space-y-2">
|
||||
<Label for="docx-upload-input">DOCX datoteka</Label>
|
||||
<Input
|
||||
id="docx-upload-input"
|
||||
@change="handleFile"
|
||||
type="file"
|
||||
accept=".docx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<span class="text-xs text-muted-foreground" v-if="!uploadForm.file">
|
||||
Izberi DOCX datoteko…
|
||||
</span>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
:disabled="
|
||||
uploadForm.processing ||
|
||||
!uploadForm.file ||
|
||||
(!uploadForm.slug && !selectedSlug)
|
||||
"
|
||||
>
|
||||
<UploadIcon class="h-4 w-4 mr-2" />
|
||||
{{ uploadForm.processing ? "Nalaganje…" : "Shrani verzijo" }}
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="uploadForm.errors.file" class="text-sm text-destructive">
|
||||
{{ uploadForm.errors.file }}
|
||||
</p>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div v-if="groups.length" class="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div
|
||||
v-for="g in groups"
|
||||
:key="g.slug"
|
||||
class="group relative flex flex-col bg-white border rounded-lg shadow-sm overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="px-4 py-3 border-b bg-gradient-to-r from-gray-50 to-white flex items-start justify-between gap-3"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<h3 class="font-medium text-sm leading-5 truncate">{{ g.name }}</h3>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 mt-1 text-[11px] text-gray-500"
|
||||
>
|
||||
<span class="px-1.5 py-0.5 bg-gray-100 rounded">{{ g.slug }}</span>
|
||||
<span>Zadnja: v{{ g.versions[0].version }}</span>
|
||||
<span
|
||||
class="flex items-center gap-1"
|
||||
:class="
|
||||
g.versions.filter((v) => v.active).length
|
||||
? 'text-emerald-600'
|
||||
: 'text-gray-400'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="
|
||||
g.versions.filter((v) => v.active).length
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-gray-300'
|
||||
<Card v-for="g in groups" :key="g.slug">
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-base truncate">{{ g.name }}</CardTitle>
|
||||
<CardDescription class="flex flex-wrap items-center gap-2 mt-1">
|
||||
<Badge variant="secondary" class="text-xs">{{ g.slug }}</Badge>
|
||||
<span class="text-xs">Zadnja: v{{ g.versions[0].version }}</span>
|
||||
<Badge
|
||||
:variant="
|
||||
g.versions.filter((v) => v.active).length ? 'default' : 'outline'
|
||||
"
|
||||
/>
|
||||
{{ g.versions.filter((v) => v.active).length }} aktivnih
|
||||
</span>
|
||||
class="text-xs"
|
||||
>
|
||||
{{ g.versions.filter((v) => v.active).length }} aktivnih
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" as-child>
|
||||
<Link :href="route('admin.document-templates.show', g.versions[0].id)">
|
||||
Detalji
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Link
|
||||
:href="route('admin.document-templates.show', g.versions[0].id)"
|
||||
class="text-xs text-indigo-600 hover:underline whitespace-nowrap mt-1"
|
||||
>Detalji</Link
|
||||
>
|
||||
</div>
|
||||
<div class="p-3 flex-1 flex flex-col gap-2">
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="v in g.versions" :key="v.id" class="flex items-center gap-1">
|
||||
<Link
|
||||
:href="route('admin.document-templates.edit', v.id)"
|
||||
class="px-2 py-0.5 rounded-md border text-[11px] font-medium transition-colors"
|
||||
:class="
|
||||
v.active
|
||||
? 'border-emerald-500/60 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
: 'border-gray-300 bg-white text-gray-600 hover:bg-gray-50'
|
||||
"
|
||||
>v{{ v.version }}</Link
|
||||
<Button
|
||||
size="sm"
|
||||
:variant="v.active ? 'default' : 'outline'"
|
||||
class="h-7 px-2 text-xs"
|
||||
as-child
|
||||
>
|
||||
<button
|
||||
<Link :href="route('admin.document-templates.edit', v.id)">
|
||||
v{{ v.version }}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
:variant="v.active ? 'destructive' : 'outline'"
|
||||
class="h-7 w-7"
|
||||
@click="toggle(v.id)"
|
||||
class="rounded-md border px-1.5 py-0.5 text-[10px] font-medium transition-colors"
|
||||
:class="
|
||||
v.active
|
||||
? 'bg-amber-500 border-amber-500 text-white hover:bg-amber-600'
|
||||
: 'bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200'
|
||||
"
|
||||
>
|
||||
{{ v.active ? "✕" : "✓" }}
|
||||
</button>
|
||||
<XIcon v-if="v.active" class="h-3 w-3" />
|
||||
<CheckIcon v-else class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto pt-2 border-t flex justify-end">
|
||||
<Link
|
||||
:href="route('admin.document-templates.edit', g.versions[0].id)"
|
||||
class="text-[11px] text-indigo-600 hover:underline"
|
||||
>Uredi zadnjo verzijo →</Link
|
||||
>
|
||||
<div class="pt-2 border-t flex justify-end">
|
||||
<Button size="sm" variant="link" class="h-auto p-0" as-child>
|
||||
<Link :href="route('admin.document-templates.edit', g.versions[0].id)">
|
||||
Uredi zadnjo verziju →
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<p v-else class="text-sm text-gray-500">Ni predlog.</p>
|
||||
<p v-else class="text-sm text-muted-foreground">Ni predlog.</p>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,231 +2,294 @@
|
|||
<AdminLayout title="Predloga">
|
||||
<div class="flex flex-col lg:flex-row gap-6 items-start">
|
||||
<div class="flex-1 min-w-[320px] space-y-6">
|
||||
<div class="bg-white border rounded-lg shadow-sm p-5 flex flex-col gap-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">{{ template.name }}</h1>
|
||||
<p class="text-xs text-gray-500 mt-1 flex flex-wrap gap-3">
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><span class="text-gray-400">Slug:</span
|
||||
><span class="font-medium">{{ template.slug }}</span></span
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><span class="text-gray-400">Verzija:</span
|
||||
><span class="font-medium">v{{ template.version }}</span></span
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1"
|
||||
:class="template.active ? 'text-emerald-600' : 'text-gray-400'"
|
||||
>
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="template.active ? 'bg-emerald-500' : 'bg-gray-300'"
|
||||
/>
|
||||
{{ template.active ? "Aktivna" : "Neaktivna" }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="toggleActive" class="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
:class="[btnBase, template.active ? btnWarn : btnOutline]"
|
||||
:disabled="toggleForm.processing"
|
||||
>
|
||||
<span v-if="toggleForm.processing">...</span>
|
||||
<span v-else>{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}</span>
|
||||
</button>
|
||||
<Link
|
||||
:href="route('admin.document-templates.edit', template.id)"
|
||||
:class="[btnBase, btnPrimary]"
|
||||
>Uredi</Link
|
||||
>
|
||||
<Link
|
||||
:href="route('admin.document-templates.index')"
|
||||
:class="[btnBase, btnOutline]"
|
||||
>Nazaj</Link
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-3 gap-6 text-xs">
|
||||
<div class="space-y-2">
|
||||
<h3 class="uppercase font-semibold tracking-wide text-gray-600">
|
||||
Datoteka
|
||||
</h3>
|
||||
<ul class="space-y-1 text-gray-600">
|
||||
<li>
|
||||
<span class="text-gray-400">Velikost:</span>
|
||||
<span class="font-medium"
|
||||
>{{ (template.file_size / 1024).toFixed(1) }} KB</span
|
||||
<FileTextIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{{ template.name }}</CardTitle>
|
||||
<CardDescription class="flex flex-wrap gap-3 mt-1">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>Slug:</span>
|
||||
<Badge variant="secondary" class="text-xs">{{
|
||||
template.slug
|
||||
}}</Badge>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>Verzija:</span>
|
||||
<Badge variant="secondary" class="text-xs"
|
||||
>v{{ template.version }}</Badge
|
||||
>
|
||||
</span>
|
||||
<Badge
|
||||
:variant="template.active ? 'default' : 'outline'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ template.active ? "Aktivna" : "Neaktivna" }}
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<form @submit.prevent="toggleActive">
|
||||
<Button
|
||||
type="submit"
|
||||
:variant="template.active ? 'destructive' : 'default'"
|
||||
size="sm"
|
||||
:disabled="toggleForm.processing"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Hash:</span>
|
||||
<span class="font-mono"
|
||||
>{{ template.file_hash?.substring(0, 12) }}…</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Engine:</span>
|
||||
<span class="font-medium">{{ template.engine }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
:href="'/storage/' + template.file_path"
|
||||
target="_blank"
|
||||
class="text-[11px] inline-flex items-center gap-1 text-indigo-600 hover:underline"
|
||||
>Prenesi DOCX →</a
|
||||
<PowerOffIcon v-if="template.active" class="h-4 w-4 mr-2" />
|
||||
<Power v-else class="h-4 w-4 mr-2" />
|
||||
{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}
|
||||
</Button>
|
||||
</form>
|
||||
<Button size="sm" as-child>
|
||||
<Link :href="route('admin.document-templates.edit', template.id)">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" as-child>
|
||||
<Link :href="route('admin.document-templates.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid md:grid-cols-3 gap-6 text-sm">
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-semibold flex items-center gap-2">
|
||||
<FileTextIcon class="h-4 w-4" />
|
||||
Datoteka
|
||||
</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Velikost:</span>
|
||||
<Badge variant="secondary"
|
||||
>{{ (template.file_size / 1024).toFixed(1) }} KB</Badge
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Hash:</span>
|
||||
<code class="text-xs"
|
||||
>{{ template.file_hash?.substring(0, 12) }}…</code
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Engine:</span>
|
||||
<Badge variant="outline">{{ template.engine }}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" class="w-full" as-child>
|
||||
<a :href="'/storage/' + template.file_path" target="_blank">
|
||||
<DownloadIcon class="h-4 w-4 mr-2" />
|
||||
Prenesi DOCX
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-semibold flex items-center gap-2">
|
||||
<HashIcon class="h-4 w-4" />
|
||||
Formatiranje
|
||||
</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Datum:</span>
|
||||
<code class="text-xs">{{
|
||||
template.settings?.date_format || "d.m.Y"
|
||||
}}</code>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Decimalna mesta:</span>
|
||||
<span>{{ template.settings?.number_decimals ?? "-" }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Separators:</span>
|
||||
<code class="text-xs">
|
||||
{{ template.settings?.decimal_separator || "." }} /
|
||||
{{ template.settings?.thousands_separator || " " }}
|
||||
</code>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Valuta:</span>
|
||||
<span class="text-xs">
|
||||
{{ template.settings?.currency_symbol || "€" }} ({{
|
||||
template.settings?.currency_position || "before"
|
||||
}})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-semibold">Aktivnost</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Akcija:</span>
|
||||
<Badge variant="outline">{{ template.action?.name || "-" }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Odločitev:</span>
|
||||
<Badge variant="outline">{{ template.decision?.name || "-" }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Fail unresolved:</span>
|
||||
<Badge
|
||||
:variant="
|
||||
template.settings?.fail_on_unresolved
|
||||
? 'destructive'
|
||||
: 'secondary'
|
||||
"
|
||||
>
|
||||
{{ template.settings?.fail_on_unresolved ? "DA" : "NE" }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="template.settings?.activity_note_template">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Predloga opombe aktivnosti</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre
|
||||
class="bg-muted p-3 rounded border text-xs leading-relaxed whitespace-pre-wrap font-mono"
|
||||
>{{ template.settings.activity_note_template }}</pre
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="template.tokens?.length">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle class="text-base">Tokens</CardTitle>
|
||||
<CardDescription>{{ template.tokens.length }} tokenov</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="expandedTokens = !expandedTokens"
|
||||
>
|
||||
{{ expandedTokens ? "Skrij" : "Prikaži vse" }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="uppercase font-semibold tracking-wide text-gray-600">
|
||||
Formatiranje
|
||||
</h3>
|
||||
<ul class="space-y-1 text-gray-600">
|
||||
<li>
|
||||
<span class="text-gray-400">Datum:</span>
|
||||
{{ template.settings?.date_format || "d.m.Y" }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Decimalna mesta:</span>
|
||||
{{ template.settings?.number_decimals ?? "-" }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Separators:</span>
|
||||
{{ template.settings?.decimal_separator || "." }} /
|
||||
{{ template.settings?.thousands_separator || " " }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Valuta:</span>
|
||||
{{ template.settings?.currency_symbol || "€" }} ({{
|
||||
template.settings?.currency_position || "before"
|
||||
}})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="uppercase font-semibold tracking-wide text-gray-600">
|
||||
Aktivnost
|
||||
</h3>
|
||||
<ul class="space-y-1 text-gray-600">
|
||||
<li>
|
||||
<span class="text-gray-400">Akcija:</span>
|
||||
{{ template.action?.name || "-" }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Odločitev:</span>
|
||||
{{ template.decision?.name || "-" }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-400">Fail unresolved:</span>
|
||||
{{ template.settings?.fail_on_unresolved ? "DA" : "NE" }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="template.settings?.activity_note_template"
|
||||
class="bg-white border rounded-lg shadow-sm p-5 space-y-2 text-xs"
|
||||
>
|
||||
<h2 class="uppercase font-semibold tracking-wide text-gray-600">
|
||||
Predloga opombe aktivnosti
|
||||
</h2>
|
||||
<pre
|
||||
class="bg-gray-50 p-3 rounded border text-[11px] leading-relaxed whitespace-pre-wrap"
|
||||
>{{ template.settings.activity_note_template }}</pre
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="template.tokens?.length"
|
||||
class="bg-white border rounded-lg shadow-sm p-5"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="uppercase font-semibold tracking-wide text-gray-600 text-xs">
|
||||
Tokens ({{ template.tokens.length }})
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
@click="expandedTokens = !expandedTokens"
|
||||
class="text-[11px] text-indigo-600 hover:underline"
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
class="flex flex-wrap gap-1.5 overflow-auto"
|
||||
:class="!expandedTokens && 'max-h-32'"
|
||||
>
|
||||
{{ expandedTokens ? "Skrij" : "Prikaži vse" }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-1.5 max-h-56 overflow-auto pr-1"
|
||||
:class="!expandedTokens && 'max-h-32'"
|
||||
>
|
||||
<span
|
||||
v-for="t in template.tokens"
|
||||
:key="t"
|
||||
class="px-1.5 py-0.5 bg-gray-100 rounded text-[11px] font-mono"
|
||||
>{{ t }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
v-for="t in template.tokens"
|
||||
:key="t"
|
||||
variant="secondary"
|
||||
class="font-mono text-xs"
|
||||
>
|
||||
{{ t }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<aside class="w-full lg:w-72 space-y-6">
|
||||
<div class="bg-white border rounded-lg shadow-sm p-4 space-y-3 text-xs">
|
||||
<h3 class="uppercase font-semibold tracking-wide text-gray-600">
|
||||
Hitra dejanja
|
||||
</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Link
|
||||
:href="route('admin.document-templates.edit', template.id)"
|
||||
:class="[btnBase, btnPrimary]"
|
||||
>Uredi nastavitve</Link
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Hitra dejanja</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-2">
|
||||
<Button size="sm" as-child>
|
||||
<Link :href="route('admin.document-templates.edit', template.id)">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi nastavitve
|
||||
</Link>
|
||||
</Button>
|
||||
<form @submit.prevent="toggleActive">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
:class="[btnBase, template.active ? btnWarn : btnOutline]"
|
||||
size="sm"
|
||||
:variant="template.active ? 'destructive' : 'default'"
|
||||
:disabled="toggleForm.processing"
|
||||
class="w-full"
|
||||
>
|
||||
<PowerOffIcon v-if="template.active" class="h-4 w-4 mr-2" />
|
||||
<Power v-else class="h-4 w-4 mr-2" />
|
||||
{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
<form @submit.prevent="rescan">
|
||||
<button type="submit" :class="[btnBase, btnOutline]" :disabled="rescanForm.processing">
|
||||
<span v-if="rescanForm.processing">Pregledujem…</span>
|
||||
<span v-else>Ponovno preglej tokene</span>
|
||||
</button>
|
||||
</form>
|
||||
<Link
|
||||
:href="route('admin.document-templates.index')"
|
||||
:class="[btnBase, btnOutline]"
|
||||
>Vse predloge</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white border rounded-lg shadow-sm p-4 space-y-2 text-[11px] text-gray-600"
|
||||
>
|
||||
<h3 class="uppercase font-semibold tracking-wide text-gray-600 text-xs">
|
||||
Opombe
|
||||
</h3>
|
||||
<p>
|
||||
Uporabi to stran za hiter pregled meta podatkov predloge ter njenih tokenov.
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="rescan">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="rescanForm.processing"
|
||||
class="w-full"
|
||||
>
|
||||
<RefreshCwIcon class="h-4 w-4 mr-2" />
|
||||
{{ rescanForm.processing ? "Pregledujem…" : "Ponovno preglej tokene" }}
|
||||
</Button>
|
||||
</form>
|
||||
<Button size="sm" variant="outline" as-child>
|
||||
<Link :href="route('admin.document-templates.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Vse predloge
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Opombe</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
<p>
|
||||
Uporabi to stran za hiter pregled meta podatkov predloge ter njenih tokenov.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { Link, useForm } from "@inertiajs/vue3";
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import {
|
||||
FileTextIcon,
|
||||
Power,
|
||||
PowerOffIcon,
|
||||
PencilIcon,
|
||||
ArrowLeftIcon,
|
||||
RefreshCwIcon,
|
||||
DownloadIcon,
|
||||
HashIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
// Button style utility classes
|
||||
const btnBase =
|
||||
"inline-flex items-center justify-center gap-1 rounded-md border text-xs font-medium px-3 py-1.5 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
const btnPrimary = "bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500";
|
||||
const btnOutline = "bg-white text-gray-700 border-gray-300 hover:bg-gray-50";
|
||||
const btnWarn = "bg-amber-500 border-amber-500 text-white hover:bg-amber-400";
|
||||
const expandedTokens = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
template: Object,
|
||||
|
|
|
|||
|
|
@ -1,104 +1,259 @@
|
|||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Head, Link, router } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, router } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { MailIcon, FilterIcon, ExternalLinkIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
logs: Object,
|
||||
templates: Array,
|
||||
filters: Object,
|
||||
})
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
status: props.filters?.status || '',
|
||||
to: props.filters?.to || '',
|
||||
subject: props.filters?.subject || '',
|
||||
template_id: props.filters?.template_id || '',
|
||||
date_from: props.filters?.date_from || '',
|
||||
date_to: props.filters?.date_to || '',
|
||||
})
|
||||
status: props.filters?.status || "",
|
||||
to: props.filters?.to || "",
|
||||
subject: props.filters?.subject || "",
|
||||
template_id: props.filters?.template_id || "",
|
||||
date_from: props.filters?.date_from || "",
|
||||
date_to: props.filters?.date_to || "",
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
router.get(route('admin.email-logs.index'), {
|
||||
...form.value,
|
||||
}, { preserveState: true, preserveScroll: true })
|
||||
router.get(
|
||||
route("admin.email-logs.index"),
|
||||
{
|
||||
...form.value,
|
||||
},
|
||||
{ preserveState: true, preserveScroll: true }
|
||||
);
|
||||
}
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (status === "sent") return "default";
|
||||
if (status === "queued" || status === "sending") return "secondary";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: "Datum",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "to_email",
|
||||
label: "Prejemnik",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "subject",
|
||||
label: "Zadeva",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "template",
|
||||
label: "Predloga",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "duration_ms",
|
||||
label: "Trajanje",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Dejanja",
|
||||
sortable: false,
|
||||
align: "right",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Email Logs">
|
||||
<Head title="Email Logs" />
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl font-semibold text-gray-800">Email Logs</h1>
|
||||
<div class="mt-3 grid grid-cols-1 md:grid-cols-6 gap-2">
|
||||
<select v-model="form.status" class="input">
|
||||
<option value="">All statuses</option>
|
||||
<option value="queued">Queued</option>
|
||||
<option value="sending">Sending</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
<option value="deferred">Deferred</option>
|
||||
</select>
|
||||
<input v-model="form.to" placeholder="To email" class="input" />
|
||||
<input v-model="form.subject" placeholder="Subject" class="input" />
|
||||
<select v-model="form.template_id" class="input">
|
||||
<option value="">All templates</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<input v-model="form.date_from" type="date" class="input" />
|
||||
<input v-model="form.date_to" type="date" class="input" />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button @click="applyFilters" class="px-3 py-1.5 text-xs rounded border bg-gray-50 hover:bg-gray-100">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<MailIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Email Logs</CardTitle>
|
||||
<CardDescription>
|
||||
Pregled vseh poslanih in čakajočih e-poštnih sporočil
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="applyFilters" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="status">Status</Label>
|
||||
<Select v-model="form.status">
|
||||
<SelectTrigger id="status">
|
||||
<SelectValue placeholder="All statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">All statuses</SelectItem>
|
||||
<SelectItem value="queued">Queued</SelectItem>
|
||||
<SelectItem value="sending">Sending</SelectItem>
|
||||
<SelectItem value="sent">Sent</SelectItem>
|
||||
<SelectItem value="failed">Failed</SelectItem>
|
||||
<SelectItem value="bounced">Bounced</SelectItem>
|
||||
<SelectItem value="deferred">Deferred</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="to">To email</Label>
|
||||
<Input id="to" v-model="form.to" placeholder="prejemnik@email.com" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="subject">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
v-model="form.subject"
|
||||
placeholder="Iskanje po zadevi"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="template">Template</Label>
|
||||
<Select v-model="form.template_id">
|
||||
<SelectTrigger id="template">
|
||||
<SelectValue placeholder="All templates" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">All templates</SelectItem>
|
||||
<SelectItem v-for="t in templates" :key="t.id" :value="t.id">{{
|
||||
t.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="date_from">Od datuma</Label>
|
||||
<Input id="date_from" v-model="form.date_from" type="date" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="date_to">Do datuma</Label>
|
||||
<Input id="date_to" v-model="form.date_to" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" size="sm">
|
||||
<FilterIcon class="h-4 w-4 mr-2" />
|
||||
Filtriraj
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm">
|
||||
<div class="overflow-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600">
|
||||
<tr>
|
||||
<th class="text-left p-2">Date</th>
|
||||
<th class="text-left p-2">Status</th>
|
||||
<th class="text-left p-2">To</th>
|
||||
<th class="text-left p-2">Subject</th>
|
||||
<th class="text-left p-2">Template</th>
|
||||
<th class="text-left p-2">Duration</th>
|
||||
<th class="text-left p-2">\#</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs.data" :key="log.id" class="border-t">
|
||||
<td class="p-2 whitespace-nowrap">{{ new Date(log.created_at).toLocaleString() }}</td>
|
||||
<td class="p-2"><span class="inline-flex items-center px-2 py-0.5 rounded text-xs border" :class="{
|
||||
'bg-green-50 text-green-700 border-green-200': log.status === 'sent',
|
||||
'bg-amber-50 text-amber-700 border-amber-200': log.status === 'queued' || log.status === 'sending',
|
||||
'bg-red-50 text-red-700 border-red-200': log.status === 'failed',
|
||||
}">{{ log.status }}</span></td>
|
||||
<td class="p-2 truncate max-w-[220px]">
|
||||
{{ log.to_email || (Array.isArray(log.to_recipients) && log.to_recipients.length ? log.to_recipients.join(', ') : '-') }}
|
||||
</td>
|
||||
<td class="p-2 truncate max-w-[320px]">{{ log.subject }}</td>
|
||||
<td class="p-2 truncate max-w-[220px]">{{ log.template?.name || '-' }}</td>
|
||||
<td class="p-2">{{ log.duration_ms ? log.duration_ms + ' ms' : '-' }}</td>
|
||||
<td class="p-2"><Link :href="route('admin.email-logs.show', log.id)" class="text-indigo-600 hover:underline">Open</Link></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-2 border-t text-xs text-gray-600 flex items-center justify-between">
|
||||
<div>Showing {{ logs.from }}-{{ logs.to }} of {{ logs.total }}</div>
|
||||
<div class="flex gap-2">
|
||||
<Link v-for="link in logs.links" :key="link.url || link.label" :href="link.url || '#'" :class="['px-2 py-1 rounded border text-xs', { 'bg-indigo-600 text-white border-indigo-600': link.active, 'pointer-events-none opacity-50': !link.url } ]" v-html="link.label" />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<MailIcon size="18" />
|
||||
<CardTitle class="uppercase">Uvozi</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="logs.data"
|
||||
:meta="logs"
|
||||
:show-toolbar="false"
|
||||
:show-pagination="true"
|
||||
route-name="admin.email-logs.index"
|
||||
:preserve-state="true"
|
||||
:preserve-scroll="true"
|
||||
empty-text="Ni e-poštnih dnevnikov"
|
||||
empty-description="Začnite z ustvarjanjem e-poštnih sporočil"
|
||||
>
|
||||
<template #cell-created_at="{ value }">
|
||||
<div class="whitespace-nowrap text-sm">
|
||||
{{ new Date(value).toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ value }">
|
||||
<Badge :variant="getStatusVariant(value)">
|
||||
{{ value }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-to_email="{ row }">
|
||||
<div class="max-w-55 truncate">
|
||||
{{
|
||||
row.to_email ||
|
||||
(Array.isArray(row.to_recipients) && row.to_recipients.length
|
||||
? row.to_recipients.join(", ")
|
||||
: "-")
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-subject="{ value }">
|
||||
<div class="max-w-[320px] truncate">{{ value }}</div>
|
||||
</template>
|
||||
|
||||
<template #cell-template="{ row }">
|
||||
<div class="max-w-55 truncate">{{ row.template?.name || "-" }}</div>
|
||||
</template>
|
||||
|
||||
<template #cell-duration_ms="{ value }">
|
||||
<span v-if="value" class="text-xs text-muted-foreground">
|
||||
{{ value }} ms
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<Button size="sm" variant="ghost" as-child>
|
||||
<Link :href="route('admin.email-logs.show', row.id)">
|
||||
<ExternalLinkIcon class="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input { width: 100%; border-radius: 0.375rem; border: 1px solid #d1d5db; padding: 0.5rem 0.75rem; font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.input:focus { outline: 2px solid transparent; outline-offset: 2px; border-color: #6366f1; box-shadow: 0 0 0 1px #6366f1; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Head, Link } from '@inertiajs/vue3'
|
||||
import { MailIcon, ArrowLeftIcon, ClockIcon, UserIcon, AlertCircleIcon, FileTextIcon, CodeIcon } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card'
|
||||
import { Button } from '@/Components/ui/button'
|
||||
import { Badge } from '@/Components/ui/badge'
|
||||
import { Separator } from '@/Components/ui/separator'
|
||||
|
||||
const props = defineProps({
|
||||
log: Object,
|
||||
|
|
@ -11,50 +16,124 @@ const props = defineProps({
|
|||
<AdminLayout title="Email Log">
|
||||
<Head title="Email Log" />
|
||||
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Link :href="route('admin.email-logs.index')" class="text-sm text-gray-600 hover:text-gray-800">Back</Link>
|
||||
<h1 class="text-xl font-semibold text-gray-800">Email Log #{{ props.log.id }}</h1>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Created: {{ new Date(props.log.created_at).toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<MailIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Email Log #{{ props.log.id }}</CardTitle>
|
||||
<CardDescription>
|
||||
Ustvarjeno: {{ new Date(props.log.created_at).toLocaleString() }}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="route('admin.email-logs.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4 space-y-2">
|
||||
<div class="text-sm"><span class="font-semibold">Status:</span> {{ props.log.status }}</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold">To:</span>
|
||||
<template v-if="props.log.to_email">
|
||||
{{ props.log.to_email }} {{ props.log.to_name ? '(' + props.log.to_name + ')' : '' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="Array.isArray(props.log.to_recipients) && props.log.to_recipients.length">
|
||||
{{ props.log.to_recipients.join(', ') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-sm"><span class="font-semibold">Subject:</span> {{ props.log.subject }}</div>
|
||||
<div class="text-sm"><span class="font-semibold">Template:</span> {{ props.log.template?.name || '-' }}</div>
|
||||
<!-- Message ID removed per request -->
|
||||
<div class="text-sm"><span class="font-semibold">Attempts:</span> {{ props.log.attempt }}</div>
|
||||
<div class="text-sm"><span class="font-semibold">Duration:</span> {{ props.log.duration_ms ? props.log.duration_ms + ' ms' : '-' }}</div>
|
||||
<div v-if="props.log.error_message" class="text-sm text-red-700"><span class="font-semibold">Error:</span> {{ props.log.error_message }}</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Podrobnosti</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Status:</span>
|
||||
<Badge
|
||||
:variant="
|
||||
props.log.status === 'sent' ? 'default' :
|
||||
props.log.status === 'queued' || props.log.status === 'sending' ? 'secondary' :
|
||||
props.log.status === 'failed' ? 'destructive' : 'outline'
|
||||
"
|
||||
>
|
||||
{{ props.log.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium flex items-center gap-2">
|
||||
<UserIcon class="h-4 w-4" />
|
||||
Prejemnik
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<template v-if="props.log.to_email">
|
||||
{{ props.log.to_email }} {{ props.log.to_name ? '(' + props.log.to_name + ')' : '' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="Array.isArray(props.log.to_recipients) && props.log.to_recipients.length">
|
||||
{{ props.log.to_recipients.join(', ') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium">Zadeva</div>
|
||||
<div class="text-sm text-muted-foreground">{{ props.log.subject }}</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Predloga:</span>
|
||||
<Badge variant="outline">{{ props.log.template?.name || '-' }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Poskusi:</span>
|
||||
<Badge variant="secondary">{{ props.log.attempt }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<ClockIcon class="h-4 w-4" />
|
||||
Trajanje:
|
||||
</span>
|
||||
<span class="text-sm">{{ props.log.duration_ms ? props.log.duration_ms + ' ms' : '-' }}</span>
|
||||
</div>
|
||||
<div v-if="props.log.error_message" class="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertCircleIcon class="h-4 w-4 text-destructive mt-0.5" />
|
||||
<div>
|
||||
<div class="text-sm font-medium text-destructive">Napaka</div>
|
||||
<div class="text-xs text-destructive/80 mt-1">{{ props.log.error_message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<FileTextIcon class="h-4 w-4" />
|
||||
Besedilo
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre class="text-xs whitespace-pre-wrap break-words bg-muted p-3 rounded-md">{{ props.log.body?.body_text || '' }}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4">
|
||||
<div class="label">Text</div>
|
||||
<pre class="text-xs whitespace-pre-wrap break-words">{{ props.log.body?.body_text || '' }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4">
|
||||
<div class="label">HTML</div>
|
||||
<iframe :srcdoc="props.log.body?.body_html || ''" class="w-full h-[480px] border rounded bg-white"></iframe>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<CodeIcon class="h-4 w-4" />
|
||||
HTML vsebina
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<iframe :srcdoc="props.log.body?.body_html || ''" class="w-full h-[480px] border rounded-md bg-white"></iframe>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label { display:block; font-size: 0.7rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:#6b7280; margin-bottom:0.25rem; }
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,11 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPenToSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PlusIcon, PencilIcon, Trash2Icon, MailIcon } from 'lucide-vue-next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
|
||||
const props = defineProps({
|
||||
templates: { type: Array, default: () => [] },
|
||||
|
|
@ -19,50 +22,84 @@ function destroyTemplate(tpl) {
|
|||
<template>
|
||||
<AdminLayout title="Email predloge">
|
||||
<Head title="Email predloge" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">Email predloge</h1>
|
||||
<Link
|
||||
:href="route('admin.email-templates.create')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nova predloga
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Ime</th>
|
||||
<th class="px-3 py-2 text-left">Ključ</th>
|
||||
<th class="px-3 py-2 text-left">Entities</th>
|
||||
<th class="px-3 py-2 text-left">Aktivno</th>
|
||||
<th class="px-3 py-2 text-left">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in templates" :key="t.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ t.name }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ t.key }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ (t.entity_types || []).join(', ') }}</td>
|
||||
<td class="px-3 py-2">{{ t.active ? 'da' : 'ne' }}</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<Link
|
||||
:href="route('admin.email-templates.edit', t.id)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-600 border-indigo-300 bg-indigo-50 hover:bg-indigo-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPenToSquare" class="w-3.5 h-3.5" /> Uredi
|
||||
</Link>
|
||||
<button
|
||||
@click="destroyTemplate(t)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-rose-700 border-rose-300 bg-rose-50 hover:bg-rose-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTrash" class="w-3.5 h-3.5" /> Izbriši
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<MailIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Email predloge</CardTitle>
|
||||
<CardDescription>
|
||||
Upravljanje predlog za e-poštna sporočila
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button as-child>
|
||||
<Link :href="route('admin.email-templates.create')">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nova predloga
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ime</TableHead>
|
||||
<TableHead>Ključ</TableHead>
|
||||
<TableHead>Entitete</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="t in templates" :key="t.id">
|
||||
<TableCell class="font-medium">{{ t.name }}</TableCell>
|
||||
<TableCell>
|
||||
<code class="text-xs bg-muted px-1.5 py-0.5 rounded">{{ t.key }}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Badge v-for="entity in (t.entity_types || [])"
|
||||
:key="entity"
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ entity }}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="t.active ? 'default' : 'secondary'">
|
||||
{{ t.active ? 'Aktivno' : 'Neaktivno' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button size="sm" variant="outline" as-child>
|
||||
<Link :href="route('admin.email-templates.edit', t.id)">
|
||||
<PencilIcon class="h-4 w-4 mr-1" />
|
||||
Uredi
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
@click="destroyTemplate(t)"
|
||||
>
|
||||
<Trash2Icon class="h-4 w-4 mr-1" />
|
||||
Izbriši
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faUserGroup,
|
||||
faKey,
|
||||
faGears,
|
||||
faFileWord,
|
||||
faEnvelopeOpenText,
|
||||
faInbox,
|
||||
faAt,
|
||||
faAddressBook,
|
||||
faFileLines,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
UsersIcon,
|
||||
KeyRoundIcon,
|
||||
Settings2Icon,
|
||||
FileTextIcon,
|
||||
MailOpenIcon,
|
||||
InboxIcon,
|
||||
AtSignIcon,
|
||||
BookUserIcon,
|
||||
MessageSquareIcon,
|
||||
ArrowRightIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
const cards = [
|
||||
{
|
||||
|
|
@ -22,13 +30,13 @@ const cards = [
|
|||
title: "Uporabniki",
|
||||
description: "Upravljanje uporabnikov in njihovih vlog",
|
||||
route: "admin.users.index",
|
||||
icon: faUserGroup,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
title: "Novo dovoljenje",
|
||||
description: "Dodaj in konfiguriraj novo dovoljenje",
|
||||
route: "admin.permissions.create",
|
||||
icon: faKey,
|
||||
icon: KeyRoundIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -39,13 +47,13 @@ const cards = [
|
|||
title: "Nastavitve dokumentov",
|
||||
description: "Privzete sistemske nastavitve za dokumente",
|
||||
route: "admin.document-settings.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
},
|
||||
{
|
||||
title: "Predloge dokumentov",
|
||||
description: "Upravljanje in verzioniranje DOCX predlog",
|
||||
route: "admin.document-templates.index",
|
||||
icon: faFileWord,
|
||||
icon: FileTextIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -56,19 +64,19 @@ const cards = [
|
|||
title: "Email predloge",
|
||||
description: "Upravljanje HTML / tekst email predlog",
|
||||
route: "admin.email-templates.index",
|
||||
icon: faEnvelopeOpenText,
|
||||
icon: MailOpenIcon,
|
||||
},
|
||||
{
|
||||
title: "Email dnevniki",
|
||||
description: "Pregled poslanih emailov in statusov",
|
||||
route: "admin.email-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
},
|
||||
{
|
||||
title: "Mail profili",
|
||||
description: "SMTP profili, nastavitve in testiranje povezave",
|
||||
route: "admin.mail-profiles.index",
|
||||
icon: faAt,
|
||||
icon: AtSignIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -79,31 +87,31 @@ const cards = [
|
|||
title: "SMS profili",
|
||||
description: "Nastavitve SMS profilov, testno pošiljanje in stanje kreditov",
|
||||
route: "admin.sms-profiles.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
},
|
||||
{
|
||||
title: "SMS pošiljatelji",
|
||||
description: "Upravljanje nazivov pošiljateljev (Sender ID) za SMS profile",
|
||||
route: "admin.sms-senders.index",
|
||||
icon: faAddressBook,
|
||||
icon: BookUserIcon,
|
||||
},
|
||||
{
|
||||
title: "SMS predloge",
|
||||
description: "Tekstovne predloge za SMS obvestila in opomnike",
|
||||
route: "admin.sms-templates.index",
|
||||
icon: faFileLines,
|
||||
icon: FileTextIcon,
|
||||
},
|
||||
{
|
||||
title: "SMS dnevniki",
|
||||
description: "Pregled poslanih SMSov in statusov",
|
||||
route: "admin.sms-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
},
|
||||
{
|
||||
title: "SMS paketi",
|
||||
description: "Kreiranje in pošiljanje serijskih SMS paketov",
|
||||
route: "admin.packages.index",
|
||||
icon: faInbox,
|
||||
icon: MessageSquareIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -112,44 +120,50 @@ const cards = [
|
|||
|
||||
<template>
|
||||
<AdminLayout title="Administrator">
|
||||
<div class="space-y-14">
|
||||
<section
|
||||
v-for="(group, i) in cards"
|
||||
:key="group.category"
|
||||
:class="[i > 0 ? 'pt-6 border-t border-gray-200/70' : '']"
|
||||
>
|
||||
<h2 class="text-xs font-semibold tracking-wider uppercase text-gray-500 mb-4">
|
||||
{{ group.category }}
|
||||
</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="space-y-8">
|
||||
<div v-for="(group, i) in cards" :key="group.category">
|
||||
<Separator v-if="i > 0" class="my-8" />
|
||||
<div class="mb-6">
|
||||
<h2
|
||||
class="text-xs font-semibold tracking-wider uppercase text-muted-foreground"
|
||||
>
|
||||
{{ group.category }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Link
|
||||
v-for="item in group.items"
|
||||
:key="item.title"
|
||||
:href="route(item.route)"
|
||||
class="group relative overflow-hidden p-5 rounded-lg border bg-white hover:border-indigo-300 hover:shadow transition focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
class="group"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<span
|
||||
class="inline-flex items-center justify-center w-10 h-10 rounded-md bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="item.icon" class="w-5 h-5" />
|
||||
</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-sm mb-1 flex items-center gap-2">
|
||||
{{ item.title }}
|
||||
<span
|
||||
class="opacity-0 group-hover:opacity-100 transition text-indigo-500 text-[10px] font-medium"
|
||||
>→</span
|
||||
<Card class="h-full transition-all hover:border-primary hover:shadow-md">
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 text-primary group-hover:bg-primary/20 transition-colors shrink-0"
|
||||
>
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 leading-relaxed line-clamp-3">
|
||||
<component :is="item.icon" class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<CardTitle class="text-base flex items-center gap-2 group">
|
||||
{{ item.title }}
|
||||
<ArrowRightIcon
|
||||
class="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity text-primary"
|
||||
/>
|
||||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription class="text-sm leading-relaxed line-clamp-2">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,58 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
|
||||
import { Head, Link, useForm } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faPlus,
|
||||
faFlask,
|
||||
faBolt,
|
||||
faArrowsRotate,
|
||||
faToggleOn,
|
||||
faToggleOff,
|
||||
faPaperPlane,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
PlusIcon,
|
||||
FlaskConicalIcon,
|
||||
MailIcon,
|
||||
PencilIcon,
|
||||
SendIcon,
|
||||
MoreVerticalIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Switch } from "@/Components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/Components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
|
||||
const props = defineProps({
|
||||
profiles: { type: Array, default: () => [] },
|
||||
|
|
@ -93,13 +132,15 @@ function submitEdit() {
|
|||
if (form.password && form.password.trim() !== "") {
|
||||
payload.password = form.password.trim();
|
||||
}
|
||||
form.transform(() => payload).put(route("admin.mail-profiles.update", editTarget.value.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
editOpen.value = false;
|
||||
editTarget.value = null;
|
||||
},
|
||||
});
|
||||
form
|
||||
.transform(() => payload)
|
||||
.put(route("admin.mail-profiles.update", editTarget.value.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
editOpen.value = false;
|
||||
editTarget.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toggleActive(p) {
|
||||
|
|
@ -131,251 +172,266 @@ const statusClass = (p) => {
|
|||
<template>
|
||||
<AdminLayout title="Mail profili">
|
||||
<Head title="Mail profili" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
|
||||
Mail profili
|
||||
<span class="text-xs font-medium text-gray-400">({{ profiles.length }})</span>
|
||||
</h1>
|
||||
<button
|
||||
@click="openCreate"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nov profil
|
||||
</button>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Ime</th>
|
||||
<th class="px-3 py-2 text-left">Host</th>
|
||||
<th class="px-3 py-2">Port</th>
|
||||
<th class="px-3 py-2">Enc</th>
|
||||
<th class="px-3 py-2">Aktivno</th>
|
||||
<th class="px-3 py-2">Test</th>
|
||||
<th class="px-3 py-2">Zadnji uspeh</th>
|
||||
<th class="px-3 py-2">Napaka</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="p in profiles"
|
||||
:key="p.id"
|
||||
class="border-t last:border-b hover:bg-gray-50"
|
||||
>
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ p.name }}</td>
|
||||
<td class="px-3 py-2">{{ p.host }}</td>
|
||||
<td class="px-3 py-2 text-center">{{ p.port }}</td>
|
||||
<td class="px-3 py-2 text-center">{{ p.encryption || "—" }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button
|
||||
@click="toggleActive(p)"
|
||||
class="text-indigo-600 hover:text-indigo-800"
|
||||
:title="p.active ? 'Onemogoči' : 'Omogoči'"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="p.active ? faToggleOn : faToggleOff"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="['font-medium', statusClass(p)]">{{
|
||||
p.test_status || "—"
|
||||
}}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-500">
|
||||
{{ p.last_success_at ? new Date(p.last_success_at).toLocaleString() : "—" }}
|
||||
</td>
|
||||
<td
|
||||
class="px-3 py-2 text-xs text-rose-600 max-w-[160px] truncate"
|
||||
:title="p.last_error_message"
|
||||
>
|
||||
{{ p.last_error_message || "—" }}
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<button
|
||||
@click="testConnection(p)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-600 border-amber-300 bg-amber-50 hover:bg-amber-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faFlask" class="w-3.5 h-3.5" /> Test
|
||||
</button>
|
||||
<button
|
||||
@click="sendTestEmail(p)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-emerald-700 border-emerald-300 bg-emerald-50 hover:bg-emerald-100"
|
||||
title="Pošlji testni email"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Pošlji test
|
||||
</button>
|
||||
<button
|
||||
@click="openEdit(p)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-600 border-indigo-300 bg-indigo-50 hover:bg-indigo-100"
|
||||
title="Uredi profil"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faArrowsRotate" class="w-3.5 h-3.5" /> Uredi
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
:show="createOpen"
|
||||
title="Nov Mail profil"
|
||||
max-width="2xl"
|
||||
confirm-text="Shrani"
|
||||
:processing="form.processing"
|
||||
@close="closeCreate"
|
||||
@confirm="submitCreate"
|
||||
>
|
||||
<form @submit.prevent="submitCreate" id="create-mail-profile" class="space-y-5">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<MailIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Mail profili</CardTitle>
|
||||
<CardDescription
|
||||
>Upravljajte SMTP profile za pošiljanje e-pošte ({{
|
||||
profiles.length
|
||||
}})</CardDescription
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="openCreate">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nov profil
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ime</TableHead>
|
||||
<TableHead>Host</TableHead>
|
||||
<TableHead class="text-center">Port</TableHead>
|
||||
<TableHead class="text-center">Enc</TableHead>
|
||||
<TableHead class="text-center">Aktivno</TableHead>
|
||||
<TableHead class="text-center">Status</TableHead>
|
||||
<TableHead>Zadnji uspeh</TableHead>
|
||||
<TableHead>Napaka</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="p in profiles" :key="p.id">
|
||||
<TableCell class="font-medium">{{ p.name }}</TableCell>
|
||||
<TableCell>{{ p.host }}</TableCell>
|
||||
<TableCell class="text-center">{{ p.port }}</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge v-if="p.encryption" variant="outline">{{
|
||||
p.encryption.toUpperCase()
|
||||
}}</Badge>
|
||||
<span v-else class="text-muted-foreground">—</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Switch
|
||||
:default-value="p.active"
|
||||
@update:model-value="() => toggleActive(p)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge
|
||||
v-if="p.test_status === 'success'"
|
||||
variant="default"
|
||||
class="bg-green-100 text-green-800 hover:bg-green-100"
|
||||
>{{ p.test_status }}</Badge
|
||||
>
|
||||
<Badge v-else-if="p.test_status === 'failed'" variant="destructive">{{
|
||||
p.test_status
|
||||
}}</Badge>
|
||||
<Badge
|
||||
v-else-if="p.test_status === 'queued'"
|
||||
variant="secondary"
|
||||
class="bg-amber-100 text-amber-800 hover:bg-amber-100"
|
||||
>{{ p.test_status }}</Badge
|
||||
>
|
||||
<span v-else class="text-muted-foreground">—</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-sm text-muted-foreground">
|
||||
{{
|
||||
p.last_success_at ? new Date(p.last_success_at).toLocaleString() : "—"
|
||||
}}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class="text-sm text-destructive max-w-[200px] truncate"
|
||||
:title="p.last_error_message"
|
||||
>
|
||||
{{ p.last_error_message || "—" }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVerticalIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Akcije</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="testConnection(p)">
|
||||
<FlaskConicalIcon class="h-4 w-4 mr-2" />
|
||||
Test povezavo
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="sendTestEmail(p)">
|
||||
<SendIcon class="h-4 w-4 mr-2" />
|
||||
Pošlji testni email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="openEdit(p)">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi profil
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog :open="createOpen" @update:open="(val) => (createOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nov Mail profil</DialogTitle>
|
||||
<DialogDescription
|
||||
>Ustvarite nov SMTP profil za pošiljanje e-pošte</DialogDescription
|
||||
>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="submitCreate" class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="col-span-1">
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="form.name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-name">Ime</Label>
|
||||
<Input id="create-name" v-model="form.name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Host</label>
|
||||
<input v-model="form.host" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-host">Host</Label>
|
||||
<Input id="create-host" v-model="form.host" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Port</label>
|
||||
<input v-model="form.port" type="number" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-port">Port</Label>
|
||||
<Input id="create-port" v-model.number="form.port" type="number" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Encryption</label>
|
||||
<select v-model="form.encryption" class="input">
|
||||
<option value="">(None)</option>
|
||||
<option value="tls">TLS</option>
|
||||
<option value="ssl">SSL</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label for="create-encryption">Encryption</Label>
|
||||
<Select v-model="form.encryption">
|
||||
<SelectTrigger id="create-encryption">
|
||||
<SelectValue placeholder="Izberi..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">None</SelectItem>
|
||||
<SelectItem value="tls">TLS</SelectItem>
|
||||
<SelectItem value="ssl">SSL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Username</label>
|
||||
<input v-model="form.username" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-username">Username</Label>
|
||||
<Input id="create-username" v-model="form.username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Password</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="create-password">Password</Label>
|
||||
<Input
|
||||
id="create-password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="input"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">From naslov</label>
|
||||
<input v-model="form.from_address" type="email" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-from-address">From naslov</Label>
|
||||
<Input id="create-from-address" v-model="form.from_address" type="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">From ime</label>
|
||||
<input v-model="form.from_name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-from-name">From ime</Label>
|
||||
<Input id="create-from-name" v-model="form.from_name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Prioriteta</label>
|
||||
<input v-model="form.priority" type="number" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="create-priority">Prioriteta</Label>
|
||||
<Input id="create-priority" v-model.number="form.priority" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CreateDialog>
|
||||
<!-- Edit Modal -->
|
||||
<UpdateDialog
|
||||
:show="editOpen"
|
||||
title="Uredi Mail profil"
|
||||
max-width="2xl"
|
||||
confirm-text="Shrani"
|
||||
:processing="form.processing"
|
||||
@close="closeEdit"
|
||||
@confirm="submitEdit"
|
||||
>
|
||||
<form @submit.prevent="submitEdit" id="edit-mail-profile" class="space-y-5">
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="closeCreate" :disabled="form.processing"
|
||||
>Prekliči</Button
|
||||
>
|
||||
<Button @click="submitCreate" :disabled="form.processing">Shrani</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog :open="editOpen" @update:open="(val) => (editOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Uredi Mail profil</DialogTitle>
|
||||
<DialogDescription>Posodobite nastavitve SMTP profila</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="submitEdit" class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="form.name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-name">Ime</Label>
|
||||
<Input id="edit-name" v-model="form.name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Host</label>
|
||||
<input v-model="form.host" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-host">Host</Label>
|
||||
<Input id="edit-host" v-model="form.host" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Port</label>
|
||||
<input v-model="form.port" type="number" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-port">Port</Label>
|
||||
<Input id="edit-port" v-model.number="form.port" type="number" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Encryption</label>
|
||||
<select v-model="form.encryption" class="input">
|
||||
<option value="">(None)</option>
|
||||
<option value="tls">TLS</option>
|
||||
<option value="ssl">SSL</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-encryption">Encryption</Label>
|
||||
<Select v-model="form.encryption">
|
||||
<SelectTrigger id="edit-encryption">
|
||||
<SelectValue placeholder="Izberi..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">None</SelectItem>
|
||||
<SelectItem value="tls">TLS</SelectItem>
|
||||
<SelectItem value="ssl">SSL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Username</label>
|
||||
<input v-model="form.username" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-username">Username</Label>
|
||||
<Input id="edit-username" v-model="form.username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Password (nova, če spreminjaš)</label>
|
||||
<input v-model="form.password" type="password" class="input" autocomplete="new-password" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-password">Password (nova, če spreminjaš)</Label>
|
||||
<Input
|
||||
id="edit-password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">From naslov</label>
|
||||
<input v-model="form.from_address" type="email" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-from-address">From naslov</Label>
|
||||
<Input id="edit-from-address" v-model="form.from_address" type="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">From ime</label>
|
||||
<input v-model="form.from_name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-from-name">From ime</Label>
|
||||
<Input id="edit-from-name" v-model="form.from_name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Prioriteta</label>
|
||||
<input v-model="form.priority" type="number" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-priority">Prioriteta</Label>
|
||||
<Input id="edit-priority" v-model.number="form.priority" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Pusti geslo prazno, če želiš obdržati obstoječe.</p>
|
||||
</form>
|
||||
</UpdateDialog>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Pusti geslo prazno, če želiš obdržati obstoječe.
|
||||
</p>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="closeEdit" :disabled="form.processing"
|
||||
>Prekliči</Button
|
||||
>
|
||||
<Button @click="submitEdit" :disabled="form.processing">Shrani</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Utility replacements for @apply not processed in SFC scope build pipeline */
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--tw-color-gray-300, #d1d5db);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,6 +2,27 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import {
|
||||
PackageIcon,
|
||||
ArrowLeftIcon,
|
||||
PlayIcon,
|
||||
XCircleIcon,
|
||||
RefreshCwIcon,
|
||||
CopyIcon,
|
||||
} from "lucide-vue-next";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
package: { type: Object, required: true },
|
||||
|
|
@ -9,6 +30,24 @@ const props = defineProps({
|
|||
preview: { type: [Object, null], default: null },
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ accessorKey: "id", header: "ID" },
|
||||
{ accessorKey: "target", header: "Prejemnik" },
|
||||
{ accessorKey: "message", header: "Sporočilo" },
|
||||
{ accessorKey: "status", header: "Status" },
|
||||
{ accessorKey: "last_error", header: "Napaka" },
|
||||
{ accessorKey: "provider_message_id", header: "Provider ID" },
|
||||
{ accessorKey: "cost", header: "Cena" },
|
||||
{ accessorKey: "currency", header: "Valuta" },
|
||||
];
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (["queued", "processing"].includes(status)) return "secondary";
|
||||
if (status === "sent") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
const refreshing = ref(false);
|
||||
let timer = null;
|
||||
|
||||
|
|
@ -16,11 +55,15 @@ const isRunning = computed(() => ["queued", "running"].includes(props.package.st
|
|||
|
||||
// Derive a summary of payload/template/body from the first item on the page.
|
||||
// Assumption: payload is the same across items in a package (both flows use a common payload).
|
||||
const firstItem = computed(() => (props.items?.data && props.items.data.length ? props.items.data[0] : null));
|
||||
const firstPayload = computed(() => (firstItem.value ? firstItem.value.payload_json || {} : {}));
|
||||
const firstItem = computed(() =>
|
||||
props.items?.data && props.items.data.length ? props.items.data[0] : null
|
||||
);
|
||||
const firstPayload = computed(() =>
|
||||
firstItem.value ? firstItem.value.payload_json || {} : {}
|
||||
);
|
||||
const messageBody = computed(() => {
|
||||
const b = firstPayload.value?.body;
|
||||
if (typeof b === 'string') {
|
||||
if (typeof b === "string") {
|
||||
const t = b.trim();
|
||||
return t.length ? t : null;
|
||||
}
|
||||
|
|
@ -73,14 +116,16 @@ async function copyText(text) {
|
|||
await navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
// Fallback for older browsers
|
||||
const ta = document.createElement('textarea');
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (_) {}
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
} catch (_) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,195 +133,205 @@ async function copyText(text) {
|
|||
|
||||
<template>
|
||||
<AdminLayout :title="`Paket #${package.id}`">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-gray-800">Paket #{{ package.id }}</h2>
|
||||
<p class="text-xs text-gray-500">
|
||||
UUID: <span class="font-mono">{{ package.uuid }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Link
|
||||
:href="route('admin.packages.index')"
|
||||
class="text-sm text-gray-600 hover:underline"
|
||||
>← Nazaj</Link
|
||||
>
|
||||
<button
|
||||
v-if="['draft', 'failed'].includes(package.status)"
|
||||
@click="dispatchPkg"
|
||||
class="px-3 py-1.5 rounded bg-indigo-600 text-white text-sm"
|
||||
>
|
||||
Zaženi
|
||||
</button>
|
||||
<button
|
||||
v-if="isRunning"
|
||||
@click="cancelPkg"
|
||||
class="px-3 py-1.5 rounded bg-rose-600 text-white text-sm"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
v-if="!isRunning"
|
||||
@click="reload"
|
||||
class="px-3 py-1.5 rounded border text-sm"
|
||||
>
|
||||
Osveži
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<PackageIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle>Paket #{{ package.id }}</CardTitle>
|
||||
<CardDescription class="font-mono"
|
||||
>UUID: {{ package.uuid }}</CardDescription
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('admin.packages.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="['draft', 'failed'].includes(package.status)"
|
||||
@click="dispatchPkg"
|
||||
size="sm"
|
||||
>
|
||||
<PlayIcon class="h-4 w-4 mr-2" />
|
||||
Zaženi
|
||||
</Button>
|
||||
<Button v-if="isRunning" @click="cancelPkg" variant="destructive" size="sm">
|
||||
<XCircleIcon class="h-4 w-4 mr-2" />
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button v-if="!isRunning" @click="reload" variant="outline" size="sm">
|
||||
<RefreshCwIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid sm:grid-cols-4 gap-3 mb-4">
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500">Status</p>
|
||||
<p class="text-sm font-semibold mt-1 uppercase">{{ package.status }}</p>
|
||||
</div>
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500">Skupaj</p>
|
||||
<p class="text-sm font-semibold mt-1">{{ package.total_items }}</p>
|
||||
</div>
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500">Poslano</p>
|
||||
<p class="text-sm font-semibold mt-1 text-emerald-700">
|
||||
{{ package.sent_count }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500">Neuspešno</p>
|
||||
<p class="text-sm font-semibold mt-1 text-rose-700">{{ package.failed_count }}</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Status</CardDescription>
|
||||
<CardTitle class="text-xl uppercase">{{ package.status }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Skupaj</CardDescription>
|
||||
<CardTitle class="text-xl">{{ package.total_items }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Poslano</CardDescription>
|
||||
<CardTitle class="text-xl text-emerald-700">{{ package.sent_count }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Neuspešno</CardDescription>
|
||||
<CardTitle class="text-xl text-rose-700">{{ package.failed_count }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Payload / Message preview -->
|
||||
<div class="mb-4 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Sporočilo</p>
|
||||
<template v-if="preview && preview.content">
|
||||
<div class="text-sm whitespace-pre-wrap text-gray-800">{{ preview.content }}</div>
|
||||
<div class="mt-2">
|
||||
<button @click="copyText(preview.content)" class="px-2 py-1 rounded border text-xs">Kopiraj</button>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Sporočilo</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<template v-if="preview && preview.content">
|
||||
<div class="text-sm whitespace-pre-wrap mb-3">{{ preview.content }}</div>
|
||||
<Button @click="copyText(preview.content)" size="sm" variant="outline">
|
||||
<CopyIcon class="h-3.5 w-3.5 mr-2" />
|
||||
Kopiraj
|
||||
</Button>
|
||||
<p
|
||||
v-if="preview.source === 'template' && preview.template"
|
||||
class="mt-3 text-xs text-muted-foreground"
|
||||
>
|
||||
Predloga: {{ preview.template.name }} (#{{ preview.template.id }})
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="messageBody" class="text-sm whitespace-pre-wrap">
|
||||
{{ messageBody }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">
|
||||
<template v-if="payloadSummary.template_id">
|
||||
Uporabljena bo predloga #{{ payloadSummary.template_id }}.
|
||||
</template>
|
||||
<template v-else> Vsebina sporočila ni določena. </template>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Meta / Nastavitve pošiljanja</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="text-sm grid grid-cols-3 gap-y-2">
|
||||
<dt class="col-span-1 text-muted-foreground">Profil</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.profile_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Pošiljatelj</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.sender_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Predloga</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.template_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Delivery report</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.delivery_report ? "da" : "ne" }}</dd>
|
||||
</dl>
|
||||
<div
|
||||
v-if="
|
||||
package.meta && (package.meta.source || package.meta.skipped !== undefined)
|
||||
"
|
||||
class="mt-3 pt-3 border-t text-xs text-muted-foreground"
|
||||
>
|
||||
<span v-if="package.meta.source" class="mr-3"
|
||||
>Vir: {{ package.meta.source }}</span
|
||||
>
|
||||
<span v-if="package.meta.skipped !== undefined"
|
||||
>Preskočeno: {{ package.meta.skipped }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="preview.source === 'template' && preview.template" class="mt-2 text-xs text-gray-500">
|
||||
Predloga: {{ preview.template.name }} (#{{ preview.template.id }})
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="messageBody" class="text-sm whitespace-pre-wrap text-gray-800">
|
||||
{{ messageBody }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-gray-600">
|
||||
<template v-if="payloadSummary.template_id">
|
||||
Uporabljena bo predloga #{{ payloadSummary.template_id }}.
|
||||
</template>
|
||||
<template v-else>
|
||||
Vsebina sporočila ni določena.
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="rounded border bg-white p-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Meta / Nastavitve pošiljanja</p>
|
||||
<dl class="text-sm text-gray-700 grid grid-cols-3 gap-y-1">
|
||||
<dt class="col-span-1 text-gray-500">Profil</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.profile_id ?? '—' }}</dd>
|
||||
<dt class="col-span-1 text-gray-500">Pošiljatelj</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.sender_id ?? '—' }}</dd>
|
||||
<dt class="col-span-1 text-gray-500">Predloga</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.template_id ?? '—' }}</dd>
|
||||
<dt class="col-span-1 text-gray-500">Delivery report</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.delivery_report ? 'da' : 'ne' }}</dd>
|
||||
</dl>
|
||||
<div v-if="package.meta && (package.meta.source || package.meta.skipped !== undefined)" class="mt-2 text-xs text-gray-500">
|
||||
<span v-if="package.meta.source" class="mr-3">Vir: {{ package.meta.source }}</span>
|
||||
<span v-if="package.meta.skipped !== undefined">Preskočeno: {{ package.meta.skipped }}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<PackageIcon size="18" />
|
||||
<CardTitle class="uppercase">Uvozi</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="items.data"
|
||||
:meta="items"
|
||||
route-name="admin.packages.show"
|
||||
:route-params="{ id: package.id }"
|
||||
>
|
||||
<template #cell-target="{ row }">
|
||||
<span class="text-sm">{{
|
||||
(row.target_json && row.target_json.number) || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<div class="overflow-hidden rounded-md border bg-white">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr class="text-xs text-gray-500">
|
||||
<th class="px-3 py-2 text-left font-medium">ID</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Prejemnik</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Sporočilo</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Status</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Napaka</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Provider ID</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Cena</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Valuta</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr v-for="it in items.data" :key="it.id">
|
||||
<td class="px-3 py-2">{{ it.id }}</td>
|
||||
<td class="px-3 py-2">
|
||||
{{ (it.target_json && it.target_json.number) || "—" }}
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xs text-gray-800 max-w-[420px] line-clamp-2 whitespace-pre-wrap">{{ it.rendered_preview || '—' }}</div>
|
||||
<button v-if="it.rendered_preview" @click="copyText(it.rendered_preview)" class="px-2 py-0.5 rounded border text-xs">Kopiraj</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
|
||||
:class="{
|
||||
'bg-yellow-50 text-yellow-700': ['queued', 'processing'].includes(
|
||||
it.status
|
||||
),
|
||||
'bg-emerald-50 text-emerald-700': it.status === 'sent',
|
||||
'bg-rose-50 text-rose-700': it.status === 'failed',
|
||||
'bg-gray-100 text-gray-600':
|
||||
it.status === 'canceled' || it.status === 'skipped',
|
||||
}"
|
||||
>{{ it.status }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-rose-700">{{ it.last_error ?? "—" }}</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-500 font-mono">
|
||||
{{ it.provider_message_id ?? "—" }}
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ it.cost ?? "—" }}</td>
|
||||
<td class="px-3 py-2">{{ it.currency ?? "—" }}</td>
|
||||
</tr>
|
||||
<tr v-if="!items.data?.length">
|
||||
<td colspan="8" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||
Ni elementov za prikaz.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<template #cell-message="{ row }">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xs max-w-[420px] line-clamp-2 whitespace-pre-wrap">
|
||||
{{ row.rendered_preview || "—" }}
|
||||
</div>
|
||||
<Button
|
||||
v-if="row.rendered_preview"
|
||||
@click="copyText(row.rendered_preview)"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm">
|
||||
<div class="text-gray-600">
|
||||
Prikazano {{ items.from || 0 }}–{{ items.to || 0 }} od {{ items.total || 0 }}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Link
|
||||
:href="items.prev_page_url || '#'"
|
||||
:class="[
|
||||
'px-3 py-1.5 rounded border',
|
||||
items.prev_page_url
|
||||
? 'text-gray-700 hover:bg-gray-50'
|
||||
: 'text-gray-400 cursor-not-allowed',
|
||||
]"
|
||||
>Nazaj</Link
|
||||
>
|
||||
<Link
|
||||
:href="items.next_page_url || '#'"
|
||||
:class="[
|
||||
'px-3 py-1.5 rounded border',
|
||||
items.next_page_url
|
||||
? 'text-gray-700 hover:bg-gray-50'
|
||||
: 'text-gray-400 cursor-not-allowed',
|
||||
]"
|
||||
>Naprej</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template #cell-status="{ row }">
|
||||
<Badge :variant="getStatusVariant(row.status)">{{ row.status }}</Badge>
|
||||
</template>
|
||||
|
||||
<div v-if="refreshing" class="mt-2 text-xs text-gray-500">Osveževanje ...</div>
|
||||
<template #cell-last_error="{ row }">
|
||||
<span class="text-xs text-rose-700">{{ row.last_error ?? "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-provider_message_id="{ row }">
|
||||
<span class="font-mono text-xs text-muted-foreground">{{
|
||||
row.provider_message_id ?? "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-cost="{ row }">
|
||||
<span class="text-sm">{{ row.cost ?? "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-currency="{ row }">
|
||||
<span class="text-sm">{{ row.currency ?? "—" }}</span>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
|
||||
<div v-if="refreshing" class="mt-2 text-xs text-muted-foreground">
|
||||
Osveževanje ...
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,134 @@
|
|||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faKey, faArrowLeft, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link } from "@inertiajs/vue3";
|
||||
import { KeyRoundIcon, ArrowLeftIcon, SaveIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
roles: Array,
|
||||
})
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
roles: [],
|
||||
})
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('admin.permissions.store'), {
|
||||
form.post(route("admin.permissions.store"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => form.reset('name','slug','description','roles')
|
||||
})
|
||||
onSuccess: () => form.reset("name", "slug", "description", "roles"),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Novo dovoljenje">
|
||||
<div class="max-w-2xl mx-auto bg-white border rounded-xl shadow-sm p-6 space-y-8">
|
||||
<header class="flex items-start justify-between gap-6">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center h-9 w-9 rounded-md bg-indigo-50 text-indigo-600"><FontAwesomeIcon :icon="faKey" /></span>
|
||||
Novo dovoljenje
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">Ustvari sistemsko dovoljenje za uporabo pri vlogah.</p>
|
||||
</div>
|
||||
<Link :href="route('admin.permissions.index')" class="inline-flex items-center gap-1 text-xs font-medium text-gray-500 hover:text-gray-700">
|
||||
<FontAwesomeIcon :icon="faArrowLeft" class="w-4 h-4" /> Nazaj
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600">Ime</label>
|
||||
<input v-model="form.name" type="text" class="w-full border rounded-md px-3 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
<p v-if="form.errors.name" class="text-xs text-red-600 mt-1">{{ form.errors.name }}</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600">Slug</label>
|
||||
<input v-model="form.slug" type="text" class="w-full border rounded-md px-3 py-2 text-sm font-mono focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
<p v-if="form.errors.slug" class="text-xs text-red-600 mt-1">{{ form.errors.slug }}</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600">Opis</label>
|
||||
<textarea v-model="form.description" rows="3" class="w-full border rounded-md px-3 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">{{ form.errors.description }}</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600">Veži na vloge</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<label v-for="r in props.roles" :key="r.id" class="inline-flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" :value="r.id" v-model="form.roles" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"/>
|
||||
<span><span class="font-medium">{{ r.name }}</span> <span class="text-xs text-gray-500">({{ r.slug }})</span></span>
|
||||
</label>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<KeyRoundIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Novo dovoljenje</CardTitle>
|
||||
<CardDescription
|
||||
>Ustvari sistemsko dovoljenje za uporabo pri vlogah.</CardDescription
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="form.errors.roles" class="text-xs text-red-600 mt-1">{{ form.errors.roles }}</p>
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('admin.permissions.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Ime</Label>
|
||||
<Input id="name" v-model="form.name" type="text" />
|
||||
<p v-if="form.errors.name" class="text-sm text-destructive">
|
||||
{{ form.errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input id="slug" v-model="form.slug" type="text" class="font-mono" />
|
||||
<p v-if="form.errors.slug" class="text-sm text-destructive">
|
||||
{{ form.errors.slug }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-2">
|
||||
<Label for="description">Opis</Label>
|
||||
<Textarea id="description" v-model="form.description" rows="3" />
|
||||
<p v-if="form.errors.description" class="text-sm text-destructive">
|
||||
{{ form.errors.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-2">
|
||||
<Label>Veži na vloge</Label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<label
|
||||
v-for="r in props.roles"
|
||||
:key="r.id"
|
||||
class="flex items-center gap-2 text-sm cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
:value="r.id"
|
||||
:checked="form.roles.includes(r.id)"
|
||||
@update:checked="
|
||||
(checked) => {
|
||||
if (checked) form.roles.push(r.id);
|
||||
else form.roles = form.roles.filter((id) => id !== r.id);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<span
|
||||
><span class="font-medium">{{ r.name }}</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>({{ r.slug }})</span
|
||||
></span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="form.errors.roles" class="text-sm text-destructive">
|
||||
{{ form.errors.roles }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button :disabled="form.processing" type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50">
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Shrani
|
||||
</button>
|
||||
<Link :href="route('admin.permissions.index')" class="text-sm text-gray-500 hover:text-gray-700">Prekliči</Link>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<Button :disabled="form.processing" type="submit">
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
Shrani
|
||||
</Button>
|
||||
<Button variant="ghost" as-child>
|
||||
<Link :href="route('admin.permissions.index')">Prekliči</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faKey, faArrowLeft, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { KeyRoundIcon, ArrowLeftIcon, SaveIcon } from "lucide-vue-next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
permission: Object,
|
||||
|
|
@ -26,112 +31,92 @@ function submit() {
|
|||
|
||||
<template>
|
||||
<AdminLayout :title="`Uredi dovoljenje — ${props.permission.name}`">
|
||||
<div class="max-w-2xl mx-auto bg-white border rounded-xl shadow-sm p-6 space-y-8">
|
||||
<header class="flex items-start justify-between gap-6">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-9 w-9 rounded-md bg-indigo-50 text-indigo-600"
|
||||
><FontAwesomeIcon :icon="faKey"
|
||||
/></span>
|
||||
Uredi dovoljenje
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
Posodobi sistemsko dovoljenje in pripete vloge.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
:href="route('admin.permissions.index')"
|
||||
class="inline-flex items-center gap-1 text-xs font-medium text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faArrowLeft" class="w-4 h-4" /> Nazaj
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600"
|
||||
>Ime</label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="w-full border rounded-md px-3 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
<p v-if="form.errors.name" class="text-xs text-red-600 mt-1">
|
||||
{{ form.errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600"
|
||||
>Slug</label
|
||||
>
|
||||
<input
|
||||
v-model="form.slug"
|
||||
type="text"
|
||||
class="w-full border rounded-md px-3 py-2 text-sm font-mono focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
<p v-if="form.errors.slug" class="text-xs text-red-600 mt-1">
|
||||
{{ form.errors.slug }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600"
|
||||
>Opis</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="w-full border rounded-md px-3 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">
|
||||
{{ form.errors.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<label class="block text-xs font-medium uppercase tracking-wide text-gray-600"
|
||||
>Veži na vloge</label
|
||||
>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<label
|
||||
v-for="r in props.roles"
|
||||
:key="r.id"
|
||||
class="inline-flex items-center gap-2 text-sm"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="r.id"
|
||||
v-model="form.roles"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span
|
||||
><span class="font-medium">{{ r.name }}</span>
|
||||
<span class="text-xs text-gray-500">({{ r.slug }})</span></span
|
||||
>
|
||||
</label>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<KeyRoundIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Uredi dovoljenje</CardTitle>
|
||||
<CardDescription>Posodobi sistemsko dovoljenje in pripete vloge.</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="form.errors.roles" class="text-xs text-red-600 mt-1">
|
||||
{{ form.errors.roles }}
|
||||
</p>
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('admin.permissions.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
:disabled="form.processing"
|
||||
type="submit"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faSave" class="w-4 h-4" /> Shrani
|
||||
</button>
|
||||
<Link
|
||||
:href="route('admin.permissions.index')"
|
||||
class="text-sm text-gray-500 hover:text-gray-700"
|
||||
>Prekliči</Link
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Ime</Label>
|
||||
<Input id="name" v-model="form.name" type="text" />
|
||||
<p v-if="form.errors.name" class="text-sm text-destructive">
|
||||
{{ form.errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input id="slug" v-model="form.slug" type="text" class="font-mono" />
|
||||
<p v-if="form.errors.slug" class="text-sm text-destructive">
|
||||
{{ form.errors.slug }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-2">
|
||||
<Label for="description">Opis</Label>
|
||||
<Textarea id="description" v-model="form.description" rows="3" />
|
||||
<p v-if="form.errors.description" class="text-sm text-destructive">
|
||||
{{ form.errors.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sm:col-span-2 space-y-2">
|
||||
<Label>Veži na vloge</Label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<label
|
||||
v-for="r in props.roles"
|
||||
:key="r.id"
|
||||
class="flex items-center gap-2 text-sm cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
:value="r.id"
|
||||
:checked="form.roles.includes(r.id)"
|
||||
@update:checked="(checked) => {
|
||||
if (checked) form.roles.push(r.id)
|
||||
else form.roles = form.roles.filter(id => id !== r.id)
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
><span class="font-medium">{{ r.name }}</span>
|
||||
<span class="text-xs text-muted-foreground">({{ r.slug }})</span></span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="form.errors.roles" class="text-sm text-destructive">
|
||||
{{ form.errors.roles }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<Button :disabled="form.processing" type="submit">
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
Shrani
|
||||
</Button>
|
||||
<Button variant="ghost" as-child>
|
||||
<Link :href="route('admin.permissions.index')">Prekliči</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Link, usePage } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faKey,
|
||||
faPen,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { SearchIcon, PlusIcon, KeyRoundIcon, PencilIcon } from "lucide-vue-next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
|
||||
|
||||
const props = defineProps({
|
||||
permissions: Array,
|
||||
|
|
@ -29,103 +28,92 @@ const filtered = computed(() => {
|
|||
|
||||
<template>
|
||||
<AdminLayout title="Dovoljenja">
|
||||
<div class="max-w-5xl mx-auto space-y-8">
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6 space-y-6">
|
||||
<header class="flex flex-col sm:flex-row sm:items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold tracking-tight">Dovoljenja</h1>
|
||||
<p class="text-sm text-gray-500">Pregled vseh sistemskih dovoljenj.</p>
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-4 justify-between">
|
||||
<div>
|
||||
<CardTitle>Dovoljenja</CardTitle>
|
||||
<CardDescription>Pregled vseh sistemskih dovoljenj.</CardDescription>
|
||||
</div>
|
||||
<Button as-child>
|
||||
<Link :href="route('admin.permissions.create')">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Novo
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="relative w-full sm:max-w-xs">
|
||||
<SearchIcon class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="q"
|
||||
type="text"
|
||||
placeholder="Išči..."
|
||||
class="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ filtered.length }} / {{ props.permissions.length }} rezultatov
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
:href="route('admin.permissions.create')"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-md text-xs font-medium bg-indigo-600 text-white hover:bg-indigo-500"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Novo
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="relative w-full sm:max-w-xs">
|
||||
<span class="absolute left-2 top-2 text-gray-400">
|
||||
<FontAwesomeIcon :icon="faMagnifyingGlass" class="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
v-model="q"
|
||||
type="text"
|
||||
placeholder="Išči..."
|
||||
class="pl-8 pr-3 py-1.5 text-sm rounded-md border border-gray-300 focus:ring-indigo-500 focus:border-indigo-500 w-full"
|
||||
/>
|
||||
<div class="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="text-left">Ime</TableHead>
|
||||
<TableHead class="text-left">Slug</TableHead>
|
||||
<TableHead class="text-left">Opis</TableHead>
|
||||
<TableHead class="text-left">Ustvarjeno</TableHead>
|
||||
<TableHead class="text-left">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="p in filtered" :key="p.id">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="inline-flex items-center justify-center h-8 w-8 rounded-lg bg-primary/10 text-primary">
|
||||
<KeyRoundIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<Link
|
||||
:href="route('admin.permissions.edit', p.id)"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ p.name }}
|
||||
</Link>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="font-mono text-xs">
|
||||
<Badge variant="secondary">{{ p.slug }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-sm max-w-md">
|
||||
{{ p.description || "—" }}
|
||||
</TableCell>
|
||||
<TableCell class="text-sm text-muted-foreground">
|
||||
{{ new Date(p.created_at).toLocaleDateString() }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="route('admin.permissions.edit', p.id)">
|
||||
<PencilIcon class="h-3.5 w-3.5 mr-2" />
|
||||
Uredi
|
||||
</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!filtered.length">
|
||||
<TableCell colspan="5" class="text-center text-sm text-muted-foreground py-8">
|
||||
Ni rezultatov
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ filtered.length }} / {{ props.permissions.length }} rezultatov
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-slate-200">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th class="p-2 text-left text-[11px] uppercase tracking-wide font-medium">
|
||||
Ime
|
||||
</th>
|
||||
<th class="p-2 text-left text-[11px] uppercase tracking-wide font-medium">
|
||||
Slug
|
||||
</th>
|
||||
<th class="p-2 text-left text-[11px] uppercase tracking-wide font-medium">
|
||||
Opis
|
||||
</th>
|
||||
<th class="p-2 text-left text-[11px] uppercase tracking-wide font-medium">
|
||||
Ustvarjeno
|
||||
</th>
|
||||
<th class="p-2 text-left text-[11px] uppercase tracking-wide font-medium">
|
||||
Akcije
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="p in filtered"
|
||||
:key="p.id"
|
||||
class="border-t border-slate-100 hover:bg-slate-50/60"
|
||||
>
|
||||
<td class="p-2 whitespace-nowrap font-medium flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-7 w-7 rounded-md bg-indigo-50 text-indigo-600"
|
||||
><FontAwesomeIcon :icon="faKey"
|
||||
/></span>
|
||||
<Link
|
||||
:href="route('admin.permissions.edit', p.id)"
|
||||
class="hover:underline"
|
||||
>{{ p.name }}</Link
|
||||
>
|
||||
</td>
|
||||
<td class="p-2 whitespace-nowrap font-mono text-xs text-gray-600">
|
||||
{{ p.slug }}
|
||||
</td>
|
||||
<td class="p-2 text-xs text-gray-600 max-w-md">
|
||||
{{ p.description || "—" }}
|
||||
</td>
|
||||
<td class="p-2 whitespace-nowrap text-xs text-gray-500">
|
||||
{{ new Date(p.created_at).toLocaleDateString() }}
|
||||
</td>
|
||||
<td class="p-2 whitespace-nowrap text-xs">
|
||||
<Link
|
||||
:href="route('admin.permissions.edit', p.id)"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-slate-200 text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPen" class="w-3.5 h-3.5" /> Uredi
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!filtered.length">
|
||||
<td colspan="5" class="p-6 text-center text-sm text-gray-500">
|
||||
Ni rezultatov
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,35 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, router } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import {
|
||||
MessageSquareIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
EyeIcon,
|
||||
MessageSquareTextIcon,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
logs: { type: Object, required: true },
|
||||
|
|
@ -23,11 +51,20 @@ function reload() {
|
|||
const query = Object.fromEntries(
|
||||
Object.entries(f.value).filter(([_, v]) => v !== null && v !== undefined && v !== "")
|
||||
);
|
||||
router.get(route("admin.sms-logs.index"), query, { preserveScroll: true, preserveState: true });
|
||||
router.get(route("admin.sms-logs.index"), query, {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [f.value.status, f.value.profile_id, f.value.template_id, f.value.from, f.value.to],
|
||||
() => [
|
||||
f.value.status,
|
||||
f.value.profile_id,
|
||||
f.value.template_id,
|
||||
f.value.from,
|
||||
f.value.to,
|
||||
],
|
||||
() => reload()
|
||||
);
|
||||
|
||||
|
|
@ -35,130 +72,222 @@ function clearFilters() {
|
|||
f.value = { status: "", profile_id: "", template_id: "", search: "", from: "", to: "" };
|
||||
reload();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ key: "created_at", label: "Čas", sortable: false },
|
||||
{ key: "to_number", label: "Prejemnik", sortable: false },
|
||||
{ key: "sender", label: "Sender", sortable: false },
|
||||
{ key: "profile", label: "Profil", sortable: false },
|
||||
{ key: "template", label: "Predloga", sortable: false },
|
||||
{ key: "status", label: "Status", sortable: false },
|
||||
{ key: "cost", label: "Cena", sortable: false },
|
||||
{ key: "provider_message_id", label: "Provider ID", sortable: false },
|
||||
{ key: "actions", label: "", sortable: false },
|
||||
];
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (status === "queued") return "secondary";
|
||||
if (status === "sent") return "default";
|
||||
if (status === "delivered") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (status === "queued") return "bg-amber-100 text-amber-800 hover:bg-amber-100";
|
||||
if (status === "sent") return "bg-sky-100 text-sky-800 hover:bg-sky-100";
|
||||
if (status === "delivered") return "bg-green-100 text-green-800 hover:bg-green-100";
|
||||
if (status === "failed") return "bg-red-100 text-red-800 hover:bg-red-100";
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="SMS dnevniki">
|
||||
<Head title="SMS dnevniki" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">SMS dnevniki</h1>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white p-4 shadow-sm mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-3">
|
||||
<div>
|
||||
<label class="label">Status</label>
|
||||
<select v-model="f.status" class="input">
|
||||
<option value="">Vsi</option>
|
||||
<option value="queued">queued</option>
|
||||
<option value="sent">sent</option>
|
||||
<option value="delivered">delivered</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<MessageSquareIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>SMS dnevniki</CardTitle>
|
||||
<CardDescription>Pregled poslanih SMS sporočil</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="f.profile_id" class="input">
|
||||
<option value="">Vsi</option>
|
||||
<option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Predloga</label>
|
||||
<select v-model="f.template_id" class="input">
|
||||
<option value="">Vse</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Od</label>
|
||||
<input type="date" v-model="f.from" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Do</label>
|
||||
<input type="date" v-model="f.to" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Iskanje</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="f.search"
|
||||
class="input"
|
||||
placeholder="to, sender, provider id, message"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<button type="button" class="px-3 py-1.5 rounded border text-sm bg-gray-50 hover:bg-gray-100" @click="reload">Filtriraj</button>
|
||||
<button type="button" class="px-3 py-1.5 rounded border text-sm bg-white hover:bg-gray-50" @click="clearFilters">Počisti</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Čas</th>
|
||||
<th class="px-3 py-2 text-left">Prejemnik</th>
|
||||
<th class="px-3 py-2 text-left">Sender</th>
|
||||
<th class="px-3 py-2 text-left">Profil</th>
|
||||
<th class="px-3 py-2 text-left">Predloga</th>
|
||||
<th class="px-3 py-2 text-left">Status</th>
|
||||
<th class="px-3 py-2 text-left">Cena</th>
|
||||
<th class="px-3 py-2 text-left">Provider ID</th>
|
||||
<th class="px-3 py-2"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs.data" :key="log.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2">{{ new Date(log.created_at).toLocaleString() }}</td>
|
||||
<td class="px-3 py-2">{{ log.to_number }}</td>
|
||||
<td class="px-3 py-2">{{ log.sender || '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.profile?.name || '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.template?.slug || log.template?.name || '—' }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<span :class="{
|
||||
'text-amber-600': log.status === 'queued',
|
||||
'text-sky-700': log.status === 'sent',
|
||||
'text-emerald-700': log.status === 'delivered',
|
||||
'text-rose-700': log.status === 'failed',
|
||||
}">{{ log.status }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ log.cost != null ? (Number(log.cost).toFixed(2) + ' ' + (log.currency || '')) : '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.provider_message_id || '—' }}</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
<Link :href="route('admin.sms-logs.show', log.id)" class="text-xs px-2 py-1 rounded border text-gray-700 bg-gray-50 hover:bg-gray-100">Ogled</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!logs.data || logs.data.length === 0">
|
||||
<td colspan="9" class="px-3 py-6 text-center text-sm text-gray-500">Ni vnosov.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="px-3 py-2 border-t flex items-center justify-between text-xs text-gray-600">
|
||||
<div>
|
||||
Prikaz {{ logs.from || 0 }}–{{ logs.to || 0 }} od {{ logs.total || 0 }}
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Filtri</CardTitle>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Link
|
||||
v-for="l in logs.links"
|
||||
:key="l.label + l.url"
|
||||
:href="l.url || '#'"
|
||||
v-html="l.label"
|
||||
class="px-2 py-1 rounded border"
|
||||
:class="[l.active ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white hover:bg-gray-50']"
|
||||
preserve-scroll
|
||||
preserve-state
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-status">Status</Label>
|
||||
<Select v-model="f.status">
|
||||
<SelectTrigger id="filter-status">
|
||||
<SelectValue placeholder="Vsi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vsi</SelectItem>
|
||||
<SelectItem value="queued">queued</SelectItem>
|
||||
<SelectItem value="sent">sent</SelectItem>
|
||||
<SelectItem value="delivered">delivered</SelectItem>
|
||||
<SelectItem value="failed">failed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-profile">Profil</Label>
|
||||
<Select v-model="f.profile_id">
|
||||
<SelectTrigger id="filter-profile">
|
||||
<SelectValue placeholder="Vsi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vsi</SelectItem>
|
||||
<SelectItem v-for="p in profiles" :key="p.id" :value="p.id">{{
|
||||
p.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-template">Predloga</Label>
|
||||
<Select v-model="f.template_id">
|
||||
<SelectTrigger id="filter-template">
|
||||
<SelectValue placeholder="Vse" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vse</SelectItem>
|
||||
<SelectItem v-for="t in templates" :key="t.id" :value="t.id">{{
|
||||
t.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-from">Od</Label>
|
||||
<Input id="filter-from" type="date" v-model="f.from" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-to">Do</Label>
|
||||
<Input id="filter-to" type="date" v-model="f.to" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-search">Iskanje</Label>
|
||||
<Input
|
||||
id="filter-search"
|
||||
type="text"
|
||||
v-model="f.search"
|
||||
placeholder="to, sender, provider id"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<Button size="sm" @click="reload">
|
||||
<FilterIcon class="h-4 w-4" />
|
||||
Filtriraj
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" @click="clearFilters">
|
||||
<XIcon class="h-4 w-4" />
|
||||
Počisti
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
<MessageSquareTextIcon size="18" />
|
||||
<CardTitle class="uppercase">Poslani</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="logs.data"
|
||||
:meta="logs"
|
||||
:page-size="25"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
route-name="admin.sms-logs.index"
|
||||
>
|
||||
<template #cell-created_at="{ row }">
|
||||
<span class="text-sm">{{ new Date(row.created_at).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-to_number="{ row }">
|
||||
<span class="text-sm">{{ row.to_number }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-sender="{ row }">
|
||||
<span class="text-sm text-muted-foreground">{{ row.sender || "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-profile="{ row }">
|
||||
<span class="text-sm">{{ row.profile?.name || "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-template="{ row }">
|
||||
<Badge v-if="row.template" variant="outline">{{
|
||||
row.template.slug || row.template.name
|
||||
}}</Badge>
|
||||
<span v-else class="text-sm text-muted-foreground">—</span>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<Badge
|
||||
:variant="getStatusVariant(row.status)"
|
||||
:class="getStatusClass(row.status)"
|
||||
>
|
||||
{{ row.status }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-cost="{ row }">
|
||||
<span class="text-sm">
|
||||
{{
|
||||
row.cost != null
|
||||
? Number(row.cost).toFixed(2) + " " + (row.currency || "")
|
||||
: "—"
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-provider_message_id="{ row }">
|
||||
<span class="text-sm text-muted-foreground truncate max-w-[150px] block">{{
|
||||
row.provider_message_id || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex justify-end">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('admin.sms-logs.show', row.id)">
|
||||
<EyeIcon class="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input { width: 100%; border-radius: 0.375rem; border: 1px solid #d1d5db; padding: 0.5rem 0.75rem; font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.input:focus { outline: 2px solid transparent; outline-offset: 2px; border-color: #6366f1; box-shadow: 0 0 0 1px #6366f1; }
|
||||
.label { display: block; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; margin-bottom: 0.25rem; }
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,222 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
import { ArrowLeftIcon, MessageSquareIcon, InfoIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
const props = defineProps({ log: { type: Object, required: true } });
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (status === "queued") return "secondary";
|
||||
if (status === "sent") return "default";
|
||||
if (status === "delivered") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (status === "queued") return "bg-amber-100 text-amber-800 hover:bg-amber-100";
|
||||
if (status === "sent") return "bg-sky-100 text-sky-800 hover:bg-sky-100";
|
||||
if (status === "delivered") return "bg-green-100 text-green-800 hover:bg-green-100";
|
||||
if (status === "failed") return "bg-red-100 text-red-800 hover:bg-red-100";
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="SMS log">
|
||||
<Head title="SMS log" />
|
||||
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('admin.sms-logs.index')" class="text-sm text-indigo-600 hover:underline">← Nazaj na dnevnike</Link>
|
||||
<div class="text-gray-700 text-sm">#{{ log.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<MessageSquareIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>SMS log</CardTitle>
|
||||
<CardDescription>#{{ log.id }}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="route('admin.sms-logs.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<div class="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div class="font-semibold text-gray-800 mb-2">Sporočilo</div>
|
||||
<pre class="text-sm whitespace-pre-wrap">{{ log.message }}</pre>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div class="font-semibold text-gray-800 mb-2">Meta</div>
|
||||
<pre class="text-xs whitespace-pre-wrap">{{ JSON.stringify(log.meta || {}, null, 2) }}</pre>
|
||||
</div>
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Sporočilo</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre class="text-sm whitespace-pre-wrap bg-muted p-4 rounded-md">{{
|
||||
log.message
|
||||
}}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Meta podatki</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre class="text-xs whitespace-pre-wrap bg-muted p-4 rounded-md font-mono">{{
|
||||
JSON.stringify(log.meta || {}, null, 2)
|
||||
}}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||
<div class="text-gray-500">Prejemnik</div>
|
||||
<div class="text-gray-800">{{ log.to_number }}</div>
|
||||
|
||||
<div class="text-gray-500">Sender</div>
|
||||
<div class="text-gray-800">{{ log.sender || '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Profil</div>
|
||||
<div class="text-gray-800">{{ log.profile?.name || '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Predloga</div>
|
||||
<div class="text-gray-800">{{ log.template?.slug || log.template?.name || '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Status</div>
|
||||
<div class="text-gray-800">{{ log.status }}</div>
|
||||
|
||||
<div class="text-gray-500">Cena</div>
|
||||
<div class="text-gray-800">{{ log.cost != null ? (Number(log.cost).toFixed(2) + ' ' + (log.currency || '')) : '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Provider ID</div>
|
||||
<div class="text-gray-800">{{ log.provider_message_id || '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Čas</div>
|
||||
<div class="text-gray-800">{{ new Date(log.created_at).toLocaleString() }}</div>
|
||||
|
||||
<div class="text-gray-500">Sent</div>
|
||||
<div class="text-gray-800">{{ log.sent_at ? new Date(log.sent_at).toLocaleString() : '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Delivered</div>
|
||||
<div class="text-gray-800">{{ log.delivered_at ? new Date(log.delivered_at).toLocaleString() : '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Failed</div>
|
||||
<div class="text-gray-800">{{ log.failed_at ? new Date(log.failed_at).toLocaleString() : '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Napaka (koda)</div>
|
||||
<div class="text-gray-800">{{ log.error_code || '—' }}</div>
|
||||
|
||||
<div class="text-gray-500">Napaka (opis)</div>
|
||||
<div class="text-gray-800">{{ log.error_message || '—' }}</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<InfoIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Informacije</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Prejemnik</div>
|
||||
<div class="text-sm font-medium">{{ log.to_number }}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Sender</div>
|
||||
<div class="text-sm">{{ log.sender || "—" }}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Profil</div>
|
||||
<div class="text-sm">{{ log.profile?.name || "—" }}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Predloga</div>
|
||||
<Badge v-if="log.template" variant="outline">{{
|
||||
log.template.slug || log.template.name
|
||||
}}</Badge>
|
||||
<span v-else class="text-sm text-muted-foreground">—</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Status</div>
|
||||
<Badge
|
||||
:variant="getStatusVariant(log.status)"
|
||||
:class="getStatusClass(log.status)"
|
||||
>
|
||||
{{ log.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Cena</div>
|
||||
<div class="text-sm">
|
||||
{{
|
||||
log.cost != null
|
||||
? Number(log.cost).toFixed(2) + " " + (log.currency || "")
|
||||
: "—"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||
Provider ID
|
||||
</div>
|
||||
<div class="text-sm font-mono text-xs break-all">
|
||||
{{ log.provider_message_id || "—" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Čas</div>
|
||||
<div class="text-sm">{{ new Date(log.created_at).toLocaleString() }}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Poslano</div>
|
||||
<div class="text-sm">
|
||||
{{ log.sent_at ? new Date(log.sent_at).toLocaleString() : "—" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||
Dostavljeno
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ log.delivered_at ? new Date(log.delivered_at).toLocaleString() : "—" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">Neuspešno</div>
|
||||
<div class="text-sm">
|
||||
{{ log.failed_at ? new Date(log.failed_at).toLocaleString() : "—" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||
Napaka (koda)
|
||||
</div>
|
||||
<div class="text-sm">{{ log.error_code || "—" }}</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||
Napaka (opis)
|
||||
</div>
|
||||
<div class="text-sm text-destructive">{{ log.error_message || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label { display: block; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; margin-bottom: 0.25rem; }
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,42 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import { Head, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPaperPlane, faCoins, faTags, faFlask } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
MessageSquareIcon,
|
||||
PlusIcon,
|
||||
SendIcon,
|
||||
CoinsIcon,
|
||||
TagIcon,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
initialProfiles: { type: Array, default: () => [] },
|
||||
|
|
@ -85,13 +117,17 @@ async function submitTest() {
|
|||
delivery_report: !!testForm.delivery_report,
|
||||
country_code: testForm.country_code,
|
||||
};
|
||||
await router.post(route("admin.sms-profiles.test-send", testTarget.value.id), payload, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
testOpen.value = false;
|
||||
},
|
||||
});
|
||||
await router.post(
|
||||
route("admin.sms-profiles.test-send", testTarget.value.id),
|
||||
payload,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
testOpen.value = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
testForm.processing = false;
|
||||
}
|
||||
|
|
@ -119,175 +155,188 @@ const formatDateTime = (s) => (s ? new Date(s).toLocaleString() : "—");
|
|||
<template>
|
||||
<AdminLayout title="SMS profili">
|
||||
<Head title="SMS profili" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
|
||||
SMS profili
|
||||
<span class="text-xs font-medium text-gray-400">({{ profiles.length }})</span>
|
||||
</h1>
|
||||
<button
|
||||
@click="openCreate"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nov profil
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Ime</th>
|
||||
<th class="px-3 py-2 text-left">Uporabnik</th>
|
||||
<th class="px-3 py-2">Aktiven</th>
|
||||
<th class="px-3 py-2">Pošiljatelji</th>
|
||||
<th class="px-3 py-2">Bilanca</th>
|
||||
<th class="px-3 py-2">Cena</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in profiles" :key="p.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ p.name }}</td>
|
||||
<td class="px-3 py-2">{{ p.api_username }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="p.active ? 'text-emerald-600' : 'text-rose-600'">{{ p.active ? 'Da' : 'Ne' }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-600">
|
||||
<span v-if="(p.senders||[]).length === 0">—</span>
|
||||
<span v-else>
|
||||
{{ p.senders.map(s => s.sname).join(', ') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchBalance(p)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-700 border-amber-300 bg-amber-50 hover:bg-amber-100">
|
||||
<FontAwesomeIcon :icon="faCoins" class="w-3.5 h-3.5" /> Pridobi
|
||||
</button>
|
||||
<span class="text-xs text-gray-600">{{ balances[p.id] ?? '—' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchPrice(p)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-700 border-indigo-300 bg-indigo-50 hover:bg-indigo-100">
|
||||
<FontAwesomeIcon :icon="faTags" class="w-3.5 h-3.5" /> Cene
|
||||
</button>
|
||||
<span class="text-xs text-gray-600 truncate max-w-[200px]" :title="(quotes[p.id]||[]).join(', ')">
|
||||
{{ (quotes[p.id] || []).join(', ') || '—' }}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<MessageSquareIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>
|
||||
SMS profili
|
||||
<Badge variant="secondary" class="ml-2">{{ profiles.length }}</Badge>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button @click="openCreate">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nov profil
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ime</TableHead>
|
||||
<TableHead>Uporabnik</TableHead>
|
||||
<TableHead>Aktiven</TableHead>
|
||||
<TableHead>Pošiljatelji</TableHead>
|
||||
<TableHead>Bilanca</TableHead>
|
||||
<TableHead>Cena</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="p in profiles" :key="p.id">
|
||||
<TableCell class="font-medium">{{ p.name }}</TableCell>
|
||||
<TableCell>{{ p.api_username }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="p.active ? 'default' : 'secondary'">
|
||||
{{ p.active ? "Da" : "Ne" }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground text-xs">
|
||||
<span v-if="(p.senders || []).length === 0">—</span>
|
||||
<span v-else>
|
||||
{{ p.senders.map((s) => s.sname).join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<button
|
||||
@click="openTest(p)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-emerald-700 border-emerald-300 bg-emerald-50 hover:bg-emerald-100"
|
||||
title="Pošlji test SMS"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Test SMS
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="fetchBalance(p)" variant="outline" size="sm">
|
||||
<CoinsIcon class="h-3.5 w-3.5 mr-1" /> Pridobi
|
||||
</Button>
|
||||
<span class="text-xs text-muted-foreground">{{
|
||||
balances[p.id] ?? "—"
|
||||
}}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="fetchPrice(p)" variant="outline" size="sm">
|
||||
<TagIcon class="h-3.5 w-3.5 mr-1" /> Cene
|
||||
</Button>
|
||||
<span
|
||||
class="text-xs text-muted-foreground truncate max-w-[200px]"
|
||||
:title="(quotes[p.id] || []).join(', ')"
|
||||
>
|
||||
{{ (quotes[p.id] || []).join(", ") || "—" }}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button
|
||||
@click="openTest(p)"
|
||||
variant="default"
|
||||
size="sm"
|
||||
title="Pošlji test SMS"
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5 mr-1" /> Test SMS
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Create Profile Modal -->
|
||||
<CreateDialog
|
||||
:show="createOpen"
|
||||
title="Nov SMS profil"
|
||||
max-width="2xl"
|
||||
confirm-text="Shrani"
|
||||
:processing="createForm.processing"
|
||||
@close="() => (createOpen = false)"
|
||||
@confirm="submitCreate"
|
||||
>
|
||||
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-5">
|
||||
<Dialog :open="createOpen" @update:open="(val) => (createOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nov SMS profil</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="createForm.name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label>Ime</Label>
|
||||
<Input v-model="createForm.name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="createForm.active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Aktivno</Label>
|
||||
<Select v-model="createForm.active">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API uporabnik</label>
|
||||
<input v-model="createForm.api_username" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label>API uporabnik</Label>
|
||||
<Input v-model="createForm.api_username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API geslo</label>
|
||||
<input v-model="createForm.api_password" type="password" class="input" autocomplete="new-password" />
|
||||
<div class="space-y-2">
|
||||
<Label>API geslo</Label>
|
||||
<Input
|
||||
v-model="createForm.api_password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CreateDialog>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="() => (createOpen = false)"
|
||||
>Prekliči</Button
|
||||
>
|
||||
<Button
|
||||
form="create-sms-profile"
|
||||
type="submit"
|
||||
:disabled="createForm.processing"
|
||||
>Shrani</Button
|
||||
>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Test Send Modal -->
|
||||
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
|
||||
<template #title> Testni SMS </template>
|
||||
<template #content>
|
||||
<Dialog :open="testOpen" @update:open="(val) => (testOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Testni SMS</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input v-model="testForm.to" type="text" class="input" placeholder="+386..." />
|
||||
<div class="space-y-2">
|
||||
<Label>Prejemnik (E.164)</Label>
|
||||
<Input v-model="testForm.to" type="text" placeholder="+386..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda (opcijsko)</label>
|
||||
<input v-model="testForm.country_code" type="text" class="input" placeholder="SI" />
|
||||
<div class="space-y-2">
|
||||
<Label>Državna koda (opcijsko)</Label>
|
||||
<Input v-model="testForm.country_code" type="text" placeholder="SI" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="label">Sporočilo</label>
|
||||
<textarea v-model="testForm.message" class="input" rows="4"></textarea>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label>Sporočilo</Label>
|
||||
<Textarea v-model="testForm.message" rows="4" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Dostavna poročila</Label>
|
||||
<Select v-model="testForm.delivery_report">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result details removed; rely on flash message after redirect -->
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" @click="() => (testOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Zapri</button>
|
||||
<button type="button" @click="submitTest" :disabled="testForm.processing || !testTarget" class="px-4 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50">
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5 mr-1" /> Pošlji test
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="() => (testOpen = false)"
|
||||
>Zapri</Button
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !testTarget"
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5 mr-2" /> Pošlji test
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--tw-color-gray-300, #d1d5db);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,47 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Head, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPen, faTrash, faToggleOn, faToggleOff } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/Components/ui/dropdown-menu";
|
||||
import { Switch } from "@/Components/ui/switch";
|
||||
import {
|
||||
PhoneIcon,
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
Trash2Icon,
|
||||
MoreVerticalIcon,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
initialSenders: { type: Array, default: () => [] },
|
||||
|
|
@ -13,8 +50,9 @@ const props = defineProps({
|
|||
|
||||
// Use props directly so Inertia navigations refresh the list automatically
|
||||
const senders = computed(() => props.initialSenders || []);
|
||||
const profileById = computed(() => Object.fromEntries((props.profiles || []).map(p => [p.id, p])));
|
||||
|
||||
const profileById = computed(() =>
|
||||
Object.fromEntries((props.profiles || []).map((p) => [p.id, p]))
|
||||
);
|
||||
|
||||
// Create/Edit modal
|
||||
const editOpen = ref(false);
|
||||
|
|
@ -71,7 +109,11 @@ async function submitEdit() {
|
|||
}
|
||||
|
||||
async function toggleActive(s) {
|
||||
await router.post(route("admin.sms-senders.toggle", s.id), {}, { preserveScroll: true });
|
||||
await router.post(
|
||||
route("admin.sms-senders.toggle", s.id),
|
||||
{},
|
||||
{ preserveScroll: true }
|
||||
);
|
||||
}
|
||||
|
||||
async function destroySender(s) {
|
||||
|
|
@ -83,117 +125,131 @@ async function destroySender(s) {
|
|||
<template>
|
||||
<AdminLayout title="SMS pošiljatelji">
|
||||
<Head title="SMS pošiljatelji" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">SMS pošiljatelji</h1>
|
||||
<button @click="openCreate" class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow">
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nov pošiljatelj
|
||||
</button>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<PhoneIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>SMS pošiljatelji</CardTitle>
|
||||
</div>
|
||||
<Button @click="openCreate">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nov pošiljatelj
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Pošiljatelj</TableHead>
|
||||
<TableHead>Številka</TableHead>
|
||||
<TableHead>Profil</TableHead>
|
||||
<TableHead>Aktiven</TableHead>
|
||||
<TableHead>Opis</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="s in senders" :key="s.id">
|
||||
<TableCell class="font-medium">{{ s.sname }}</TableCell>
|
||||
<TableCell>{{ s.phone_number || "—" }}</TableCell>
|
||||
<TableCell>{{ profileById[s.profile_id]?.name || "—" }}</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
:default-value="s.active"
|
||||
@update:model-value="() => toggleActive(s)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{
|
||||
s.description || "—"
|
||||
}}</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVerticalIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="openEdit(s)">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="destroySender(s)" class="text-destructive">
|
||||
<Trash2Icon class="h-4 w-4 mr-2" />
|
||||
Izbriši
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Pošiljatelj</th>
|
||||
<th class="px-3 py-2 text-left">Številka</th>
|
||||
<th class="px-3 py-2 text-left">Profil</th>
|
||||
<th class="px-3 py-2">Aktiven</th>
|
||||
<th class="px-3 py-2 text-left">Opis</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="s in senders" :key="s.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ s.sname }}</td>
|
||||
<td class="px-3 py-2 text-gray-700">{{ s.phone_number || '—' }}</td>
|
||||
<td class="px-3 py-2">{{ profileById[s.profile_id]?.name || '—' }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="s.active ? 'text-emerald-600' : 'text-rose-600'">{{ s.active ? 'Da' : 'Ne' }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ s.description || '—' }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="openEdit(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-700 border-indigo-300 bg-indigo-50 hover:bg-indigo-100">
|
||||
<FontAwesomeIcon :icon="faPen" class="w-3.5 h-3.5" /> Uredi
|
||||
</button>
|
||||
<button @click="toggleActive(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-700 border-amber-300 bg-amber-50 hover:bg-amber-100">
|
||||
<FontAwesomeIcon :icon="s.active ? faToggleOn : faToggleOff" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button @click="destroySender(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-rose-700 border-rose-300 bg-rose-50 hover:bg-rose-100">
|
||||
<FontAwesomeIcon :icon="faTrash" class="w-3.5 h-3.5" /> Izbriši
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<DialogModal :show="editOpen" max-width="2xl" @close="() => (editOpen = false)">
|
||||
<template #title> {{ editing ? 'Uredi pošiljatelja' : 'Nov pošiljatelj' }} </template>
|
||||
<template #content>
|
||||
<form @submit.prevent="submitEdit" id="edit-sms-sender" class="space-y-5">
|
||||
<Dialog :open="editOpen" @update:open="(val) => (editOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{
|
||||
editing ? "Uredi pošiljatelja" : "Nov pošiljatelj"
|
||||
}}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="submitEdit" id="edit-sms-sender" class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="form.profile_id" class="input">
|
||||
<option :value="null" disabled>Izberi profil…</option>
|
||||
<option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Profil</Label>
|
||||
<Select v-model="form.profile_id">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Izberi profil…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="p in profiles" :key="p.id" :value="p.id">
|
||||
{{ p.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Pošiljatelj (Sender ID) — opcijsko</label>
|
||||
<input v-model="form.sname" type="text" class="input" placeholder="npr. TEREN" />
|
||||
<div class="space-y-2">
|
||||
<Label>Pošiljatelj (Sender ID) — opcijsko</Label>
|
||||
<Input v-model="form.sname" type="text" placeholder="npr. TEREN" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Številka pošiljatelja (opcijsko)</label>
|
||||
<input v-model="form.phone_number" type="text" class="input" placeholder="npr. +38640123456" />
|
||||
<div class="space-y-2">
|
||||
<Label>Številka pošiljatelja (opcijsko)</Label>
|
||||
<Input
|
||||
v-model="form.phone_number"
|
||||
type="text"
|
||||
placeholder="npr. +38640123456"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="label">Opis (opcijsko)</label>
|
||||
<input v-model="form.description" type="text" class="input" />
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label>Opis (opcijsko)</Label>
|
||||
<Input v-model="form.description" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="form.active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Aktivno</Label>
|
||||
<Select v-model="form.active">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" @click="() => (editOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Prekliči</button>
|
||||
<button form="edit-sms-sender" type="submit" :disabled="form.processing" class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50">Shrani</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="() => (editOpen = false)"
|
||||
>Prekliči</Button
|
||||
>
|
||||
<Button form="edit-sms-sender" type="submit" :disabled="form.processing"
|
||||
>Shrani</Button
|
||||
>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--tw-color-gray-300, #d1d5db);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,27 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ArrowLeftIcon, SaveIcon, MessageSquareIcon, SendIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Switch } from "@/Components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
const props = defineProps({
|
||||
template: { type: Object, default: null },
|
||||
|
|
@ -104,160 +125,270 @@ async function submitTest() {
|
|||
<AdminLayout :title="props.template ? 'Uredi SMS predlogo' : 'Nova SMS predloga'">
|
||||
<Head :title="props.template ? 'Uredi SMS predlogo' : 'Nova SMS predloga'" />
|
||||
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('admin.sms-templates.index')" class="text-sm text-indigo-600 hover:underline">← Nazaj na seznam</Link>
|
||||
<div class="text-gray-700 text-sm" v-if="props.template">#{{ props.template.id }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="submit"
|
||||
:disabled="form.processing"
|
||||
class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
Shrani
|
||||
</button>
|
||||
</div>
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<MessageSquareIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{{
|
||||
props.template ? "Uredi SMS predlogo" : "Nova SMS predloga"
|
||||
}}</CardTitle>
|
||||
<CardDescription v-if="props.template"
|
||||
>#{{ props.template.id }}</CardDescription
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="route('admin.sms-templates.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="sm" @click="submit" :disabled="form.processing">
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
Shrani
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Form -->
|
||||
<div class="lg:col-span-2 rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-5 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="form.name" type="text" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Slug</label>
|
||||
<input v-model="form.slug" type="text" class="input" placeholder="npr. payment_reminder" />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="label">Vsebina</label>
|
||||
<textarea v-model="form.content" rows="8" class="input" placeholder="Pozdravljen {{ person.first_name }}, ..."></textarea>
|
||||
<div class="text-[11px] text-gray-500 mt-1">
|
||||
Uporabite placeholderje npr. <code>{first_name}</code> ali
|
||||
<code v-pre>{{ person.first_name }}</code> – ob pošiljanju se vrednosti nadomestijo.
|
||||
<Card class="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Nastavitve predloge</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Ime</Label>
|
||||
<Input id="name" v-model="form.name" type="text" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
v-model="form.slug"
|
||||
type="text"
|
||||
placeholder="npr. payment_reminder"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2 space-y-2">
|
||||
<Label for="content">Vsebina</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
v-model="form.content"
|
||||
rows="8"
|
||||
placeholder="Pozdravljen {{ person.first_name }}, ..."
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Uporabite placeholderje npr.
|
||||
<code class="bg-muted px-1 py-0.5 rounded">{first_name}</code> ali
|
||||
<code class="bg-muted px-1 py-0.5 rounded" v-pre>{{
|
||||
person.first_name
|
||||
}}</code>
|
||||
– ob pošiljanju se vrednosti nadomestijo.
|
||||
</p>
|
||||
</div>
|
||||
<div class="md:col-span-2 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="allow-custom"
|
||||
:default-value="form.allow_custom_body"
|
||||
@update:model-value="(val) => (form.allow_custom_body = val)"
|
||||
/>
|
||||
<Label for="allow-custom" class="font-normal cursor-pointer"
|
||||
>Dovoli lastno besedilo</Label
|
||||
>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Če je omogočeno, lahko pošiljatelj namesto vsebine predloge napiše
|
||||
poljubno besedilo.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="profile">Privzet profil</Label>
|
||||
<Select v-model="form.default_profile_id">
|
||||
<SelectTrigger id="profile">
|
||||
<SelectValue placeholder="Izberi profil" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">—</SelectItem>
|
||||
<SelectItem v-for="p in props.profiles" :key="p.id" :value="p.id">{{
|
||||
p.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="sender">Privzet sender</Label>
|
||||
<Select
|
||||
v-model="form.default_sender_id"
|
||||
:disabled="!form.default_profile_id"
|
||||
>
|
||||
<SelectTrigger id="sender">
|
||||
<SelectValue placeholder="Izberi sender" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">—</SelectItem>
|
||||
<SelectItem
|
||||
v-for="s in currentSendersForProfile(form.default_profile_id)"
|
||||
:key="s.id"
|
||||
:value="s.id"
|
||||
>
|
||||
{{ s.sname }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="active"
|
||||
:default-value="form.is_active"
|
||||
@update:model-value="(val) => (form.is_active = val)"
|
||||
/>
|
||||
<Label for="active" class="font-normal cursor-pointer">Aktivno</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="label">Dovoli lastno besedilo</label>
|
||||
<select v-model="form.allow_custom_body" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="text-[11px] text-gray-500 mt-1">Če je omogočeno, lahko pošiljatelj namesto vsebine predloge napiše poljubno besedilo.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Privzet profil</label>
|
||||
<select v-model="form.default_profile_id" class="input">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Privzet sender</label>
|
||||
<select v-model="form.default_sender_id" class="input" :disabled="!form.default_profile_id">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="s in currentSendersForProfile(form.default_profile_id)" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="form.is_active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Akcija po pošiljanju</label>
|
||||
<select v-model="form.action_id" class="input">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="a in props.actions" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Odločitev</label>
|
||||
<select v-model="form.decision_id" class="input" :disabled="!form.action_id">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="d in (props.actions.find(x => x.id === form.action_id)?.decisions || [])" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test send -->
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-5 space-y-4">
|
||||
<div class="font-semibold text-gray-800">Testno pošiljanje</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input v-model="testForm.to" type="text" class="input" placeholder="+386..." />
|
||||
<Separator />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="action">Akcija po pošiljanju</Label>
|
||||
<Select v-model="form.action_id">
|
||||
<SelectTrigger id="action">
|
||||
<SelectValue placeholder="Brez" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Brez</SelectItem>
|
||||
<SelectItem v-for="a in props.actions" :key="a.id" :value="a.id">{{
|
||||
a.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="decision">Odločitev</Label>
|
||||
<Select v-model="form.decision_id" :disabled="!form.action_id">
|
||||
<SelectTrigger id="decision">
|
||||
<SelectValue placeholder="Brez" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Brez</SelectItem>
|
||||
<SelectItem
|
||||
v-for="d in props.actions.find((x) => x.id === form.action_id)
|
||||
?.decisions || []"
|
||||
:key="d.id"
|
||||
:value="d.id"
|
||||
>{{ d.name }}</SelectItem
|
||||
>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda</label>
|
||||
<input v-model="testForm.country_code" type="text" class="input" placeholder="SI" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Testno pošiljanje</CardTitle>
|
||||
<CardDescription>Pošljite testno SMS sporočilo</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="test-to">Prejemnik (E.164)</Label>
|
||||
<Input
|
||||
id="test-to"
|
||||
v-model="testForm.to"
|
||||
type="text"
|
||||
placeholder="+386..."
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-country">Državna koda</Label>
|
||||
<Input
|
||||
id="test-country"
|
||||
v-model="testForm.country_code"
|
||||
type="text"
|
||||
placeholder="SI"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-profile">Profil</Label>
|
||||
<Select v-model="testForm.profile_id">
|
||||
<SelectTrigger id="test-profile">
|
||||
<SelectValue placeholder="Privzeti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Privzeti</SelectItem>
|
||||
<SelectItem v-for="p in props.profiles" :key="p.id" :value="p.id">{{
|
||||
p.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-sender">Sender</Label>
|
||||
<Select v-model="testForm.sender_id" :disabled="!testForm.profile_id">
|
||||
<SelectTrigger id="test-sender">
|
||||
<SelectValue placeholder="Privzeti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Privzeti</SelectItem>
|
||||
<SelectItem
|
||||
v-for="s in currentSendersForProfile(testForm.profile_id)"
|
||||
:key="s.id"
|
||||
:value="s.id"
|
||||
>{{ s.sname }}</SelectItem
|
||||
>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-delivery">Dostavna poročila</Label>
|
||||
<Select v-model="testForm.delivery_report">
|
||||
<SelectTrigger id="test-delivery">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="form.allow_custom_body" class="space-y-2">
|
||||
<Label for="test-custom">Lastno besedilo (opcijsko)</Label>
|
||||
<Textarea
|
||||
id="test-custom"
|
||||
v-model="testForm.custom_content"
|
||||
rows="4"
|
||||
placeholder="Če je izpolnjeno, bo namesto predloge poslano to besedilo."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="testForm.profile_id" class="input">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
<div class="flex items-center justify-end">
|
||||
<Button
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !props.template"
|
||||
>
|
||||
<SendIcon class="h-4 w-4 mr-2" />
|
||||
Pošlji test
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Sender</label>
|
||||
<select v-model="testForm.sender_id" class="input" :disabled="!testForm.profile_id">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="s in currentSendersForProfile(testForm.profile_id)" :key="s.id" :value="s.id">{{ s.sname }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="form.allow_custom_body" class="md:col-span-2">
|
||||
<label class="label">Lastno besedilo (opcijsko)</label>
|
||||
<textarea v-model="testForm.custom_content" rows="4" class="input" placeholder="Če je izpolnjeno, bo namesto predloge poslano to besedilo."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button type="button" @click="submitTest" :disabled="testForm.processing || !props.template" class="px-3 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50">Pošlji test</button>
|
||||
</div>
|
||||
<!-- Result details removed in favor of flash messages after redirect -->
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,25 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Head, useForm, Link, router } from "@inertiajs/vue3";
|
||||
import { computed, ref } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPen, faTrash, faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PlusIcon, PencilIcon, Trash2Icon, SendIcon, MessageSquareIcon, MoreVerticalIcon } from 'lucide-vue-next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Label } from '@/Components/ui/label';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
import { Switch } from '@/Components/ui/switch';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/Components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/Components/ui/dropdown-menu';
|
||||
|
||||
const props = defineProps({
|
||||
initialTemplates: { type: Array, default: () => [] },
|
||||
|
|
@ -159,174 +174,173 @@ function currentSendersForTest() {
|
|||
<template>
|
||||
<AdminLayout title="SMS predloge">
|
||||
<Head title="SMS predloge" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">SMS predloge</h1>
|
||||
<Link
|
||||
:href="route('admin.sms-templates.create')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nova predloga
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Ime</th>
|
||||
<th class="px-3 py-2 text-left">Slug</th>
|
||||
<th class="px-3 py-2 text-left">Privzet profil/sender</th>
|
||||
<th class="px-3 py-2">Aktivno</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="t in templates"
|
||||
:key="t.id"
|
||||
class="border-t last:border-b hover:bg-gray-50"
|
||||
>
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ t.name }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ t.slug }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">
|
||||
<span>{{ profilesById[t.default_profile_id]?.name || "—" }}</span>
|
||||
<span v-if="t.default_sender_id">
|
||||
/ {{ sendersById[t.default_sender_id]?.sname }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="t.is_active ? 'text-emerald-600' : 'text-rose-600'">{{
|
||||
t.is_active ? "Da" : "Ne"
|
||||
}}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<Link
|
||||
:href="route('admin.sms-templates.edit', t.id)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-700 border-indigo-300 bg-indigo-50 hover:bg-indigo-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPen" class="w-3.5 h-3.5" /> Uredi
|
||||
</Link>
|
||||
<button
|
||||
@click="openTest(t)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-emerald-700 border-emerald-300 bg-emerald-50 hover:bg-emerald-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Test
|
||||
</button>
|
||||
<button
|
||||
@click="destroyTemplate(t)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-rose-700 border-rose-300 bg-rose-50 hover:bg-rose-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTrash" class="w-3.5 h-3.5" /> Izbriši
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||
<MessageSquareIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>SMS predloge</CardTitle>
|
||||
<CardDescription>Upravljajte predloge za SMS sporočila</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button as-child>
|
||||
<Link :href="route('admin.sms-templates.create')">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nova predloga
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
<!-- Edit/Create now handled on dedicated page -->
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ime</TableHead>
|
||||
<TableHead>Slug</TableHead>
|
||||
<TableHead>Privzet profil/sender</TableHead>
|
||||
<TableHead class="text-center">Aktivno</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="t in templates" :key="t.id">
|
||||
<TableCell class="font-medium">{{ t.name }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{{ t.slug }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="text-sm">
|
||||
<span>{{ profilesById[t.default_profile_id]?.name || "—" }}</span>
|
||||
<span v-if="t.default_sender_id" class="text-muted-foreground">
|
||||
/ {{ sendersById[t.default_sender_id]?.sname }}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge v-if="t.is_active" variant="default" class="bg-green-100 text-green-800 hover:bg-green-100">Da</Badge>
|
||||
<Badge v-else variant="secondary">Ne</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVerticalIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Akcije</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem as-child>
|
||||
<Link :href="route('admin.sms-templates.edit', t.id)">
|
||||
<PencilIcon class="h-4 w-4 mr-2" />
|
||||
Uredi
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="openTest(t)">
|
||||
<SendIcon class="h-4 w-4 mr-2" />
|
||||
Testno pošiljanje
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="destroyTemplate(t)" class="text-destructive">
|
||||
<Trash2Icon class="h-4 w-4 mr-2" />
|
||||
Izbriši
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Test Send Modal -->
|
||||
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
|
||||
<template #title> Testno pošiljanje </template>
|
||||
<template #content>
|
||||
<Dialog :open="testOpen" @update:open="(val) => testOpen = val">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Testno pošiljanje</DialogTitle>
|
||||
<DialogDescription>Pošljite testno SMS sporočilo</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="test-to">Prejemnik (E.164)</Label>
|
||||
<Input
|
||||
id="test-to"
|
||||
v-model="testForm.to"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="+386..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="test-country">Državna koda</Label>
|
||||
<Input
|
||||
id="test-country"
|
||||
v-model="testForm.country_code"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="SI"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="testForm.profile_id" class="input">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-profile">Profil</Label>
|
||||
<Select v-model="testForm.profile_id">
|
||||
<SelectTrigger id="test-profile">
|
||||
<SelectValue placeholder="Privzeti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Privzeti</SelectItem>
|
||||
<SelectItem v-for="p in props.profiles" :key="p.id" :value="p.id">
|
||||
{{ p.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Sender</label>
|
||||
<select
|
||||
v-model="testForm.sender_id"
|
||||
class="input"
|
||||
:disabled="!testForm.profile_id"
|
||||
>
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="s in currentSendersForTest()" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-sender">Sender</Label>
|
||||
<Select v-model="testForm.sender_id" :disabled="!testForm.profile_id">
|
||||
<SelectTrigger id="test-sender">
|
||||
<SelectValue placeholder="Privzeti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Privzeti</SelectItem>
|
||||
<SelectItem v-for="s in currentSendersForTest()" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label for="test-delivery">Dostavna poročila</Label>
|
||||
<Select v-model="testForm.delivery_report">
|
||||
<SelectTrigger id="test-delivery">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result details removed in favor of flash messages after redirect -->
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
@click="() => (testOpen = false)"
|
||||
class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50"
|
||||
>
|
||||
Zapri
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !testTarget"
|
||||
class="px-4 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5 mr-1" /> Pošlji test
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="() => testOpen = false">Zapri</Button>
|
||||
<Button
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !testTarget"
|
||||
>
|
||||
<SendIcon class="h-4 w-4 mr-2" />
|
||||
Pošlji test
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--tw-color-gray-300, #d1d5db);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,35 @@
|
|||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link, router } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMagnifyingGlass, faFloppyDisk, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { SearchIcon, SaveIcon, UserPlusIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
|
|
@ -29,6 +55,7 @@ function toggle(userId, roleId) {
|
|||
? form.roles.filter((id) => id !== roleId)
|
||||
: [...form.roles, roleId];
|
||||
form.dirty = true;
|
||||
console.log("Toggle checkbox");
|
||||
}
|
||||
|
||||
function submit(userId) {
|
||||
|
|
@ -105,336 +132,313 @@ function toggleCreateRole(roleId) {
|
|||
}
|
||||
|
||||
function toggleUserActive(userId) {
|
||||
router.patch(route("admin.users.toggle-active", { user: userId }), {}, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
router.patch(
|
||||
route("admin.users.toggle-active", { user: userId }),
|
||||
{},
|
||||
{
|
||||
preserveScroll: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Upravljanje vlog uporabnikov">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6 space-y-7">
|
||||
<header class="space-y-1">
|
||||
<h1 class="text-xl font-semibold leading-tight tracking-tight">
|
||||
Uporabniki & Vloge
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Uporabniki & Vloge</CardTitle>
|
||||
<CardDescription>
|
||||
Dodeli ali odstrani vloge. Uporabi iskanje ali filter po vlogah za hitrejše
|
||||
upravljanje.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"
|
||||
>
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<div class="relative">
|
||||
<span class="absolute left-2 top-1.5 text-gray-400">
|
||||
<FontAwesomeIcon :icon="faMagnifyingGlass" class="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Išči uporabnika..."
|
||||
class="pl-8 pr-3 py-1.5 text-sm rounded-md border border-gray-300 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="roleFilter = null"
|
||||
:class="[
|
||||
'px-2.5 py-1 rounded-full text-xs border transition',
|
||||
roleFilter === null
|
||||
? 'bg-indigo-600 text-white border-indigo-600'
|
||||
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50',
|
||||
]"
|
||||
>
|
||||
Vse
|
||||
</button>
|
||||
<button
|
||||
v-for="r in props.roles"
|
||||
:key="'rf-' + r.id"
|
||||
type="button"
|
||||
@click="roleFilter = r.id"
|
||||
:class="[
|
||||
'px-2.5 py-1 rounded-full text-xs border transition',
|
||||
roleFilter === r.id
|
||||
? 'bg-indigo-600 text-white border-indigo-600'
|
||||
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50',
|
||||
]"
|
||||
>
|
||||
{{ r.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateModal"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" />
|
||||
Ustvari uporabnika
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitAll"
|
||||
:disabled="!anyDirty"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:class="
|
||||
anyDirty
|
||||
? 'bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500'
|
||||
: 'bg-white border-gray-300 text-gray-400'
|
||||
"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faFloppyDisk" class="w-4 h-4" />
|
||||
Shrani vse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-slate-200">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
||||
Uporabnik
|
||||
</th>
|
||||
<th class="p-2 text-center font-medium text-[11px] uppercase tracking-wide">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="p-2 font-medium text-[11px] uppercase tracking-wide text-center"
|
||||
>
|
||||
{{ role.name }}
|
||||
</th>
|
||||
<th
|
||||
class="p-2 font-medium text-[11px] uppercase tracking-wide text-center"
|
||||
>
|
||||
Akcije
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(user, idx) in filteredUsers"
|
||||
:key="user.id"
|
||||
:class="[
|
||||
'border-t border-slate-100',
|
||||
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
||||
!user.active && 'opacity-60',
|
||||
]"
|
||||
>
|
||||
<td class="p-2 whitespace-nowrap align-top">
|
||||
<div class="font-medium text-sm flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-7 w-7 rounded-full bg-indigo-50 text-indigo-600 text-xs font-semibold"
|
||||
>{{ user.name.substring(0, 2).toUpperCase() }}</span
|
||||
>
|
||||
<span>{{ user.name }}</span>
|
||||
<span
|
||||
v-if="forms[user.id].dirty"
|
||||
class="ml-1 inline-block px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 text-[10px] font-medium"
|
||||
>Spremembe</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 mt-0.5 font-mono">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2 text-center align-top">
|
||||
<button
|
||||
@click="toggleUserActive(user.id)"
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium transition"
|
||||
:class="
|
||||
user.active
|
||||
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
"
|
||||
>
|
||||
{{ user.active ? 'Aktiven' : 'Neaktiven' }}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="p-2 text-center align-top"
|
||||
>
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded-md border-2 border-slate-400 bg-white text-indigo-600 accent-indigo-600 hover:border-slate-500 focus:ring-indigo-500 focus:ring-offset-0 focus:outline-none transition"
|
||||
:checked="forms[user.id].roles.includes(role.id)"
|
||||
@change="toggle(user.id, role.id)"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td class="p-2 text-center align-top">
|
||||
<button
|
||||
@click="submit(user.id)"
|
||||
:disabled="forms[user.id].processing || !forms[user.id].dirty"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:class="
|
||||
forms[user.id].dirty
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-500'
|
||||
: 'bg-gray-100 text-gray-400'
|
||||
"
|
||||
>
|
||||
<span v-if="forms[user.id].processing">...</span>
|
||||
<span v-else>Shrani</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!filteredUsers.length">
|
||||
<td
|
||||
:colspan="props.roles.length + 3"
|
||||
class="p-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
Ni rezultatov
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2
|
||||
class="text-[11px] font-semibold tracking-wide uppercase text-slate-500 mb-3"
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"
|
||||
>
|
||||
Referenca vlog in dovoljenj
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<div class="relative">
|
||||
<SearchIcon
|
||||
class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Išči uporabnika..."
|
||||
class="pl-9 w-64"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
@click="roleFilter = null"
|
||||
:variant="roleFilter === null ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="rounded-full"
|
||||
>
|
||||
Vse
|
||||
</Button>
|
||||
<Button
|
||||
v-for="r in props.roles"
|
||||
:key="'rf-' + r.id"
|
||||
type="button"
|
||||
@click="roleFilter = r.id"
|
||||
:variant="roleFilter === r.id ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="rounded-full"
|
||||
>
|
||||
{{ r.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button type="button" @click="openCreateModal" size="sm">
|
||||
<UserPlusIcon class="h-4 w-4 mr-2" />
|
||||
Ustvari uporabnika
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@click="submitAll"
|
||||
:disabled="!anyDirty"
|
||||
size="sm"
|
||||
:variant="anyDirty ? 'default' : 'outline'"
|
||||
>
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
Shrani vse
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="text-left">Uporabnik</TableHead>
|
||||
<TableHead class="text-center">Status</TableHead>
|
||||
<TableHead
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="text-center"
|
||||
>
|
||||
{{ role.name }}
|
||||
</TableHead>
|
||||
<TableHead class="text-center">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="user in filteredUsers"
|
||||
:key="user.id"
|
||||
:class="!user.active && 'opacity-60'"
|
||||
>
|
||||
<TableCell class="whitespace-nowrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-primary/10 text-primary text-xs font-semibold"
|
||||
>
|
||||
{{ user.name.substring(0, 2).toUpperCase() }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-sm flex items-center gap-2">
|
||||
{{ user.name }}
|
||||
<Badge
|
||||
v-if="forms[user.id].dirty"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
Spremembe
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge
|
||||
@click="toggleUserActive(user.id)"
|
||||
:variant="user.active ? 'default' : 'secondary'"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{{ user.active ? "Aktiven" : "Neaktiven" }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="text-center"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<Checkbox
|
||||
:default-value="forms[user.id].roles.includes(role.id)"
|
||||
@update:model-value="toggle(user.id, role.id)"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Button
|
||||
@click="submit(user.id)"
|
||||
:disabled="forms[user.id].processing || !forms[user.id].dirty"
|
||||
size="sm"
|
||||
:variant="forms[user.id].dirty ? 'default' : 'ghost'"
|
||||
>
|
||||
<span v-if="forms[user.id].processing">...</span>
|
||||
<span v-else>Shrani</span>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!filteredUsers.length">
|
||||
<TableCell
|
||||
:colspan="props.roles.length + 3"
|
||||
class="text-center text-sm text-muted-foreground py-8"
|
||||
>
|
||||
Ni rezultatov
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Referenca vlog in dovoljenj</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div
|
||||
<Card
|
||||
v-for="role in props.roles"
|
||||
:key="'ref-' + role.id"
|
||||
class="px-3 py-2 rounded-lg border border-slate-200 bg-white shadow-sm"
|
||||
class="border-muted"
|
||||
>
|
||||
<div class="font-medium text-sm flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-6 w-6 rounded-md bg-indigo-50 text-indigo-600 text-[11px] font-semibold"
|
||||
>{{ role.name.substring(0, 1).toUpperCase() }}</span
|
||||
>
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
<span
|
||||
v-for="perm in role.permissions"
|
||||
:key="perm.id"
|
||||
class="text-[10px] uppercase tracking-wide bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded"
|
||||
>{{ perm.slug }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<div
|
||||
class="inline-flex items-center justify-center h-6 w-6 rounded-md bg-primary/10 text-primary text-xs font-semibold"
|
||||
>
|
||||
{{ role.name.substring(0, 1).toUpperCase() }}
|
||||
</div>
|
||||
{{ role.name }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<Badge
|
||||
v-for="perm in role.permissions"
|
||||
:key="perm.id"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ perm.slug }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
<DialogModal :show="showCreateModal" @close="closeCreateModal" max-width="2xl">
|
||||
<template #title>Ustvari novega uporabnika</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ime</label>
|
||||
<input
|
||||
<Dialog :open="showCreateModal" @update:open="(val) => (showCreateModal = val)">
|
||||
<DialogContent class="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ustvari novega uporabnika</DialogTitle>
|
||||
<DialogDescription>
|
||||
Dodaj novega uporabnika s privzetimi vlogami
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Ime</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="createForm.name"
|
||||
type="text"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="Ime uporabnika"
|
||||
/>
|
||||
<div v-if="createForm.errors.name" class="text-red-600 text-xs mt-1">
|
||||
<p v-if="createForm.errors.name" class="text-sm text-destructive">
|
||||
{{ createForm.errors.name }}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">E-pošta</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="email">E-pošta</Label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="createForm.email"
|
||||
type="email"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="uporabnik@example.com"
|
||||
/>
|
||||
<div v-if="createForm.errors.email" class="text-red-600 text-xs mt-1">
|
||||
<p v-if="createForm.errors.email" class="text-sm text-destructive">
|
||||
{{ createForm.errors.email }}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Geslo</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="password">Geslo</Label>
|
||||
<Input
|
||||
id="password"
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
<div v-if="createForm.errors.password" class="text-red-600 text-xs mt-1">
|
||||
<p v-if="createForm.errors.password" class="text-sm text-destructive">
|
||||
{{ createForm.errors.password }}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Potrdi geslo</label>
|
||||
<input
|
||||
<div class="space-y-2">
|
||||
<Label for="password_confirmation">Potrdi geslo</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
v-model="createForm.password_confirmation"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Vloge</label>
|
||||
<div class="space-y-2">
|
||||
<Label>Vloge</Label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="role in props.roles"
|
||||
:key="'create-role-' + role.id"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition hover:bg-accent"
|
||||
:class="
|
||||
createForm.roles.includes(role.id)
|
||||
? 'bg-indigo-50 border-indigo-600 text-indigo-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
createForm.roles.includes(role.id) && 'bg-primary/10 border-primary'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
:checked="createForm.roles.includes(role.id)"
|
||||
@change="toggleCreateRole(role.id)"
|
||||
<Checkbox
|
||||
:default-value="createForm.roles.includes(role.id)"
|
||||
@update:model-value="toggleCreateRole(role.id)"
|
||||
/>
|
||||
<span class="text-sm font-medium">{{ role.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="createForm.errors.roles" class="text-red-600 text-xs mt-1">
|
||||
<p v-if="createForm.errors.roles" class="text-sm text-destructive">
|
||||
{{ createForm.errors.roles }}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeCreateModal"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitCreateUser"
|
||||
:disabled="createForm.processing"
|
||||
class="ml-3 px-4 py-2 rounded-md text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="createForm.processing">Ustvarjanje...</span>
|
||||
<span v-else>Ustvari</span>
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="closeCreateModal">
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@click="submitCreateUser"
|
||||
:disabled="createForm.processing"
|
||||
>
|
||||
<span v-if="createForm.processing">Ustvarjanje...</span>
|
||||
<span v-else>Ustvari</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -751,14 +751,16 @@ async function fetchColumns() {
|
|||
|
||||
async function applyTemplateToImport() {
|
||||
if (!importId.value || !form.value.import_template_id) return;
|
||||
|
||||
|
||||
// Find the selected template to get its UUID
|
||||
const template = (props.templates || []).find((t) => t.id === form.value.import_template_id);
|
||||
const template = (props.templates || []).find(
|
||||
(t) => t.id === form.value.import_template_id
|
||||
);
|
||||
if (!template?.uuid) {
|
||||
console.error('Template UUID not found');
|
||||
console.error("Template UUID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (templateApplied.value) {
|
||||
const ok = window.confirm(
|
||||
|
|
@ -1137,12 +1139,12 @@ async function fetchSimulation() {
|
|||
// V2 format
|
||||
paymentSimRows.value = Array.isArray(data?.rows) ? data.rows : [];
|
||||
paymentSimSummary.value = data?.summaries || null;
|
||||
|
||||
|
||||
// Extract unique entity types from rows for SimulationModal
|
||||
const entitySet = new Set();
|
||||
for (const row of data?.rows || []) {
|
||||
if (row.entities && typeof row.entities === 'object') {
|
||||
Object.keys(row.entities).forEach(key => entitySet.add(key));
|
||||
if (row.entities && typeof row.entities === "object") {
|
||||
Object.keys(row.entities).forEach((key) => entitySet.add(key));
|
||||
}
|
||||
}
|
||||
paymentSimEntities.value = Array.from(entitySet);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||
|
|
@ -30,7 +36,7 @@ const emit = defineEmits(["save"]);
|
|||
<Label for="name">Ime predloge</Label>
|
||||
<Input id="name" v-model="form.name" type="text" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="source_type">Vir</Label>
|
||||
<Select v-model="form.source_type">
|
||||
|
|
@ -82,7 +88,6 @@ const emit = defineEmits(["save"]);
|
|||
<SelectValue placeholder="(Auto-detect)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
<SelectItem value=",">Vejica ,</SelectItem>
|
||||
<SelectItem value=";">Podpičje ;</SelectItem>
|
||||
<SelectItem value="\t">Tab \t</SelectItem>
|
||||
|
|
@ -146,19 +151,11 @@ const emit = defineEmits(["save"]);
|
|||
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
:checked="form.is_active"
|
||||
@update:checked="form.is_active = $event"
|
||||
/>
|
||||
<Checkbox id="is_active" v-model="form.is_active" />
|
||||
<Label for="is_active" class="cursor-pointer">Aktivna</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="reactivate"
|
||||
:checked="form.reactivate"
|
||||
@update:checked="form.reactivate = $event"
|
||||
/>
|
||||
<Checkbox id="reactivate" v-model="form.reactivate" />
|
||||
<Label for="reactivate" class="cursor-pointer">Reaktivacija</Label>
|
||||
</div>
|
||||
<Button @click="emit('save')" class="ml-auto">Shrani</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user