production #1
|
|
@ -21,14 +21,35 @@ class ImportController extends Controller
|
||||||
// List imports (paginated)
|
// List imports (paginated)
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$paginator = Import::query()
|
$query = Import::query()
|
||||||
->with([
|
->with([
|
||||||
'client:id,uuid,person_id',
|
'client:id,uuid,person_id',
|
||||||
'client.person:id,uuid,full_name',
|
'client.person:id,uuid,full_name',
|
||||||
'template:id,name',
|
'template:id,name',
|
||||||
])
|
])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at');
|
||||||
->paginate(15);
|
|
||||||
|
// Apply search filter
|
||||||
|
if ($search = $request->input('search')) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('original_name', 'LIKE', "%{$search}%")
|
||||||
|
->orWhere('status', 'LIKE', "%{$search}%")
|
||||||
|
->orWhereHas('client.person', function ($q) use ($search) {
|
||||||
|
$q->where('full_name', 'LIKE', "%{$search}%");
|
||||||
|
})
|
||||||
|
->orWhereHas('template', function ($q) use ($search) {
|
||||||
|
$q->where('name', 'LIKE', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get per_page from request, default to 25
|
||||||
|
$perPage = (int) $request->input('per_page', 25);
|
||||||
|
if ($perPage < 1 || $perPage > 100) {
|
||||||
|
$perPage = 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
$imports = [
|
$imports = [
|
||||||
'data' => $paginator->items(),
|
'data' => $paginator->items(),
|
||||||
|
|
|
||||||
|
|
@ -547,6 +547,7 @@ public function updateMapping(Request $request, ImportTemplate $template, Import
|
||||||
'options' => 'nullable|array',
|
'options' => 'nullable|array',
|
||||||
'position' => 'nullable|integer',
|
'position' => 'nullable|integer',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
$mapping->update([
|
$mapping->update([
|
||||||
'source_column' => $data['source_column'],
|
'source_column' => $data['source_column'],
|
||||||
'entity' => $data['entity'] ?? null,
|
'entity' => $data['entity'] ?? null,
|
||||||
|
|
@ -557,8 +558,7 @@ public function updateMapping(Request $request, ImportTemplate $template, Import
|
||||||
'position' => $data['position'] ?? $mapping->position,
|
'position' => $data['position'] ?? $mapping->position,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
|
return back()->with('success', 'Mapping updated');
|
||||||
->with('success', 'Mapping updated');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a mapping
|
// Delete a mapping
|
||||||
|
|
|
||||||
713
package-lock.json
generated
713
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -41,6 +41,7 @@
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"apexcharts": "^4.7.0",
|
"apexcharts": "^4.7.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clean": "^4.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
||||||
25
resources/js/Components/ui/accordion/Accordion.vue
Normal file
25
resources/js/Components/ui/accordion/Accordion.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup>
|
||||||
|
import { AccordionRoot, useForwardPropsEmits } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
collapsible: { type: Boolean, required: false },
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
dir: { type: String, required: false },
|
||||||
|
orientation: { type: String, required: false },
|
||||||
|
unmountOnHide: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
type: { type: String, required: false },
|
||||||
|
modelValue: { type: null, required: false },
|
||||||
|
defaultValue: { type: null, required: false },
|
||||||
|
});
|
||||||
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</AccordionRoot>
|
||||||
|
</template>
|
||||||
25
resources/js/Components/ui/accordion/AccordionContent.vue
Normal file
25
resources/js/Components/ui/accordion/AccordionContent.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AccordionContent } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
forceMount: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionContent
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
>
|
||||||
|
<div :class="cn('pb-4 pt-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</template>
|
||||||
24
resources/js/Components/ui/accordion/AccordionItem.vue
Normal file
24
resources/js/Components/ui/accordion/AccordionItem.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AccordionItem, useForwardProps } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
value: { type: String, required: true },
|
||||||
|
unmountOnHide: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
|
||||||
|
<slot />
|
||||||
|
</AccordionItem>
|
||||||
|
</template>
|
||||||
35
resources/js/Components/ui/accordion/AccordionTrigger.vue
Normal file
35
resources/js/Components/ui/accordion/AccordionTrigger.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { ChevronDown } from "lucide-vue-next";
|
||||||
|
import { AccordionHeader, AccordionTrigger } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionHeader class="flex">
|
||||||
|
<AccordionTrigger
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<slot name="icon">
|
||||||
|
<ChevronDown
|
||||||
|
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</AccordionTrigger>
|
||||||
|
</AccordionHeader>
|
||||||
|
</template>
|
||||||
4
resources/js/Components/ui/accordion/index.js
Normal file
4
resources/js/Components/ui/accordion/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { default as Accordion } from "./Accordion.vue";
|
||||||
|
export { default as AccordionContent } from "./AccordionContent.vue";
|
||||||
|
export { default as AccordionItem } from "./AccordionItem.vue";
|
||||||
|
export { default as AccordionTrigger } from "./AccordionTrigger.vue";
|
||||||
17
resources/js/Components/ui/alert-dialog/AlertDialog.vue
Normal file
17
resources/js/Components/ui/alert-dialog/AlertDialog.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup>
|
||||||
|
import { AlertDialogRoot, useForwardPropsEmits } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: { type: Boolean, required: false },
|
||||||
|
defaultOpen: { type: Boolean, required: false },
|
||||||
|
});
|
||||||
|
const emits = defineEmits(["update:open"]);
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AlertDialogAction } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from '@/Components/ui/button';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogAction
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn(buttonVariants(), props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogAction>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AlertDialogCancel } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from '@/Components/ui/button';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogCancel
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogCancel>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import {
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
forceMount: { type: Boolean, required: false },
|
||||||
|
disableOutsidePointerEvents: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
const emits = defineEmits([
|
||||||
|
"escapeKeyDown",
|
||||||
|
"pointerDownOutside",
|
||||||
|
"focusOutside",
|
||||||
|
"interactOutside",
|
||||||
|
"openAutoFocus",
|
||||||
|
"closeAutoFocus",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay
|
||||||
|
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
|
/>
|
||||||
|
<AlertDialogContent
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AlertDialogDescription } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogDescription
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script setup>
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup>
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
22
resources/js/Components/ui/alert-dialog/AlertDialogTitle.vue
Normal file
22
resources/js/Components/ui/alert-dialog/AlertDialogTitle.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { AlertDialogTitle } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogTitle
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('text-lg font-semibold', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogTitle>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script setup>
|
||||||
|
import { AlertDialogTrigger } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialogTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
</template>
|
||||||
9
resources/js/Components/ui/alert-dialog/index.js
Normal file
9
resources/js/Components/ui/alert-dialog/index.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as AlertDialog } from "./AlertDialog.vue";
|
||||||
|
export { default as AlertDialogAction } from "./AlertDialogAction.vue";
|
||||||
|
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue";
|
||||||
|
export { default as AlertDialogContent } from "./AlertDialogContent.vue";
|
||||||
|
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue";
|
||||||
|
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue";
|
||||||
|
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue";
|
||||||
|
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue";
|
||||||
|
export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue";
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import type { PrimitiveProps } from "reka-ui"
|
import { Primitive } from "reka-ui";
|
||||||
import type { HTMLAttributes } from "vue"
|
import { cn } from "@/lib/utils";
|
||||||
import type { ButtonVariants } from "."
|
import { buttonVariants } from ".";
|
||||||
import { Primitive } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from "."
|
|
||||||
|
|
||||||
interface Props extends PrimitiveProps {
|
const props = defineProps({
|
||||||
variant?: ButtonVariants["variant"]
|
variant: { type: null, required: false },
|
||||||
size?: ButtonVariants["size"]
|
size: { type: null, required: false },
|
||||||
class?: HTMLAttributes["class"]
|
class: { type: null, required: false },
|
||||||
}
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false, default: "button" },
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
});
|
||||||
as: "button",
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ function formatDate(value) {
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<Card class="p-0">
|
<Card class="p-0!">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<PersonInfoGrid
|
<PersonInfoGrid
|
||||||
:types="types"
|
:types="types"
|
||||||
|
|
@ -163,7 +163,7 @@ function formatDate(value) {
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||||
<div class="mx-auto max-w-4x1">
|
<div class="mx-auto max-w-4x1">
|
||||||
<div class="px-3 py-4 flex flex-row items-center gap-3">
|
<div class="p-2 flex flex-row items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
import { Link, router } from "@inertiajs/vue3";
|
||||||
import { ref } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
|
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
|
||||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import {
|
import {
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
|
@ -13,6 +12,13 @@ import {
|
||||||
faTrash,
|
faTrash,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import TableActions from "@/Components/DataTable/TableActions.vue";
|
||||||
|
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
import { ImportIcon } from "lucide-vue-next";
|
||||||
|
import { CardTitle } from "@/Components/ui/card";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
imports: Object,
|
imports: Object,
|
||||||
|
|
@ -23,6 +29,8 @@ const confirming = ref(false);
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
const search = ref(new URLSearchParams(window.location.search).get("search") || "");
|
const search = ref(new URLSearchParams(window.location.search).get("search") || "");
|
||||||
|
|
||||||
|
const rows = computed(() => props.imports?.data || []);
|
||||||
|
|
||||||
function canDelete(status) {
|
function canDelete(status) {
|
||||||
return !["completed", "processing"].includes(status);
|
return !["completed", "processing"].includes(status);
|
||||||
}
|
}
|
||||||
|
|
@ -59,13 +67,33 @@ function statusBadge(status) {
|
||||||
return map[status] || "bg-gray-100 text-gray-800";
|
return map[status] || "bg-gray-100 text-gray-800";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySearch() {
|
||||||
|
const params = {};
|
||||||
|
const currentParams = new URLSearchParams(window.location.search);
|
||||||
|
for (const [key, value] of currentParams.entries()) {
|
||||||
|
if (key !== "search" && key !== "page") {
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const term = (search.value || "").trim();
|
||||||
|
if (term) {
|
||||||
|
params.search = term;
|
||||||
|
}
|
||||||
|
router.get(route("imports.index"), params, {
|
||||||
|
preserveState: true,
|
||||||
|
replace: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
only: ["imports"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "created_at", label: "Datum" },
|
{ key: "created_at", label: "Datum", sortable: false },
|
||||||
{ key: "original_name", label: "Datoteka" },
|
{ key: "original_name", label: "Datoteka", sortable: false },
|
||||||
{ key: "status", label: "Status" },
|
{ key: "status", label: "Status", sortable: false },
|
||||||
{ key: "client", label: "Naročnik" },
|
{ key: "client", label: "Naročnik", sortable: false },
|
||||||
{ key: "template", label: "Predloga" },
|
{ key: "template", label: "Predloga", sortable: false },
|
||||||
{ key: "actions", label: "Akcije", class: "w-px" },
|
{ key: "actions", label: "", sortable: false, hideable: false, align: "center" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function formatDateTimeNoSeconds(value) {
|
function formatDateTimeNoSeconds(value) {
|
||||||
|
|
@ -84,41 +112,61 @@ function formatDateTimeNoSeconds(value) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Uvozi">
|
<AppLayout title="Uvozi">
|
||||||
<template #header>
|
<template #header> </template>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Uvozi</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="py-6">
|
<div class="py-6">
|
||||||
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="flex flex-col gap-3 bg-white shadow sm:rounded-lg p-4">
|
<AppCard
|
||||||
<div class="flex justify-end">
|
title=""
|
||||||
<Link
|
padding="none"
|
||||||
:href="route('imports.create')"
|
class="p-0! gap-0"
|
||||||
class="px-3 py-2 rounded bg-blue-600 text-white text-sm"
|
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||||
>Novi uvoz</Link
|
body-class=""
|
||||||
>
|
>
|
||||||
</div>
|
<template #header>
|
||||||
<DataTableServer
|
<div class="flex items-center gap-2">
|
||||||
|
<ImportIcon size="18" />
|
||||||
|
<CardTitle class="uppercase">Uvozi</CardTitle>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<DataTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="imports?.data || []"
|
:data="rows"
|
||||||
:meta="
|
:meta="{
|
||||||
imports?.meta
|
current_page: imports?.meta?.current_page,
|
||||||
? {
|
per_page: imports?.meta?.per_page,
|
||||||
current_page: imports.meta.current_page,
|
total: imports?.meta?.total,
|
||||||
per_page: imports.meta.per_page,
|
last_page: imports?.meta?.last_page,
|
||||||
total: imports.meta.total,
|
from: imports?.meta?.from,
|
||||||
last_page: imports.meta.last_page,
|
to: imports?.meta?.to,
|
||||||
}
|
links: imports?.links,
|
||||||
: {}
|
}"
|
||||||
"
|
|
||||||
v-model:search="search"
|
|
||||||
route-name="imports.index"
|
route-name="imports.index"
|
||||||
:only-props="['imports']"
|
:only-props="['imports']"
|
||||||
|
:page-size="25"
|
||||||
|
:page-size-options="[10, 15, 25, 50, 100]"
|
||||||
|
:show-pagination="true"
|
||||||
|
:show-toolbar="true"
|
||||||
|
:hoverable="true"
|
||||||
row-key="uuid"
|
row-key="uuid"
|
||||||
empty-text="Ni uvozov."
|
empty-text="Ni uvozov."
|
||||||
>
|
>
|
||||||
|
<template #toolbar-actions>
|
||||||
|
<Button size="sm" variant="default" as-child>
|
||||||
|
<Link :href="route('imports.create')">Novi uvoz</Link>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #toolbar-filters>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
v-model="search"
|
||||||
|
placeholder="Išči uvoz..."
|
||||||
|
class="w-65"
|
||||||
|
@keydown.enter="applySearch"
|
||||||
|
/>
|
||||||
|
<Button size="sm" variant="outline" @click="applySearch">Išči</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<!-- Datum column formatted -->
|
<!-- Datum column formatted -->
|
||||||
<template #cell-created_at="{ row }">
|
<template #cell-created_at="{ row }">
|
||||||
{{ formatDateTimeNoSeconds(row.created_at) }}
|
{{ formatDateTimeNoSeconds(row.created_at) }}
|
||||||
|
|
@ -143,60 +191,40 @@ function formatDateTimeNoSeconds(value) {
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<Dropdown width="48" :close-on-content-click="true">
|
<TableActions align="right">
|
||||||
<template #trigger>
|
<template #default>
|
||||||
<button
|
<ActionMenuItem
|
||||||
type="button"
|
:icon="faEye"
|
||||||
class="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-gray-100"
|
label="Poglej"
|
||||||
aria-label="Akcije"
|
@click="
|
||||||
>
|
$inertia.visit(route('imports.continue', { import: row.uuid }))
|
||||||
<FontAwesomeIcon
|
"
|
||||||
:icon="faEllipsisVertical"
|
/>
|
||||||
class="w-4 h-4 text-gray-600"
|
<ActionMenuItem
|
||||||
/>
|
v-if="row.status !== 'completed'"
|
||||||
</button>
|
:icon="faPlay"
|
||||||
|
label="Nadaljuj"
|
||||||
|
@click="
|
||||||
|
$inertia.visit(route('imports.continue', { import: row.uuid }))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<ActionMenuItem
|
||||||
|
v-if="canDelete(row.status)"
|
||||||
|
:icon="faTrash"
|
||||||
|
label="Izbriši"
|
||||||
|
danger
|
||||||
|
@click="confirmDelete(row)"
|
||||||
|
/>
|
||||||
|
<ActionMenuItem
|
||||||
|
v-if="!canDelete(row.status)"
|
||||||
|
:icon="faCircleCheck"
|
||||||
|
label="Zaključen"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
</TableActions>
|
||||||
<div class="py-1">
|
|
||||||
<Link
|
|
||||||
:href="route('imports.continue', { import: row.uuid })"
|
|
||||||
class="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faEye" class="w-4 h-4 me-2 text-gray-500" />
|
|
||||||
<span>Poglej</span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
v-if="row.status !== 'completed'"
|
|
||||||
:href="route('imports.continue', { import: row.uuid })"
|
|
||||||
class="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
:icon="faPlay"
|
|
||||||
class="w-4 h-4 me-2 text-gray-500"
|
|
||||||
/>
|
|
||||||
<span>Nadaljuj</span>
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
v-if="canDelete(row.status)"
|
|
||||||
type="button"
|
|
||||||
class="flex items-center w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
||||||
@click="confirmDelete(row)"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faTrash" class="w-4 h-4 me-2" />
|
|
||||||
<span>Izbriši</span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex items-center px-4 py-2 text-sm text-gray-400 cursor-default"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faCircleCheck" class="w-4 h-4 me-2" />
|
|
||||||
<span>Zaključen</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</template>
|
</template>
|
||||||
</DataTableServer>
|
</DataTable>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
:show="confirming"
|
:show="confirming"
|
||||||
@close="
|
@close="
|
||||||
|
|
@ -230,7 +258,7 @@ function formatDateTimeNoSeconds(value) {
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
</div>
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,29 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { ref } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import { useForm } from "@inertiajs/vue3";
|
import { useForm } from "@inertiajs/vue3";
|
||||||
import Multiselect from "vue-multiselect";
|
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||||
import { computed, watch } from "vue";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/Components/ui/card";
|
||||||
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import { Textarea } from "@/Components/ui/textarea";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/Components/ui/select";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
import { Separator } from "@/Components/ui/separator";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
clients: Array,
|
clients: Array,
|
||||||
|
|
@ -28,8 +48,8 @@ const form = useForm({
|
||||||
delimiter: "",
|
delimiter: "",
|
||||||
// Payments import mode
|
// Payments import mode
|
||||||
payments_import: false,
|
payments_import: false,
|
||||||
// History import mode
|
// History import mode
|
||||||
history_import: false,
|
history_import: false,
|
||||||
// For payments mode: how to locate Contract - use single key 'reference'
|
// For payments mode: how to locate Contract - use single key 'reference'
|
||||||
contract_key_mode: null,
|
contract_key_mode: null,
|
||||||
},
|
},
|
||||||
|
|
@ -88,7 +108,14 @@ watch(
|
||||||
form.meta.payments_import = false;
|
form.meta.payments_import = false;
|
||||||
form.meta.contract_key_mode = null;
|
form.meta.contract_key_mode = null;
|
||||||
}
|
}
|
||||||
const allowed = ["person", "person_addresses", "person_phones", "contracts", "activities", "client_cases"];
|
const allowed = [
|
||||||
|
"person",
|
||||||
|
"person_addresses",
|
||||||
|
"person_phones",
|
||||||
|
"contracts",
|
||||||
|
"activities",
|
||||||
|
"client_cases",
|
||||||
|
];
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const current = Array.isArray(form.entities) ? [...form.entities] : [];
|
const current = Array.isArray(form.entities) ? [...form.entities] : [];
|
||||||
let filtered = current.filter((e) => allowed.includes(e));
|
let filtered = current.filter((e) => allowed.includes(e));
|
||||||
|
|
@ -103,7 +130,12 @@ watch(
|
||||||
watch(
|
watch(
|
||||||
() => form.entities,
|
() => form.entities,
|
||||||
(vals) => {
|
(vals) => {
|
||||||
if (form.meta.history_import && Array.isArray(vals) && vals.includes("contracts") && ! vals.includes("accounts")) {
|
if (
|
||||||
|
form.meta.history_import &&
|
||||||
|
Array.isArray(vals) &&
|
||||||
|
vals.includes("contracts") &&
|
||||||
|
!vals.includes("accounts")
|
||||||
|
) {
|
||||||
form.entities = [...vals, "accounts"];
|
form.entities = [...vals, "accounts"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,254 +143,278 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Create Import Template">
|
<AppLayout title="Ustvari predlogo uvoza">
|
||||||
<template #header>
|
<template #header> </template>
|
||||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
||||||
Create Import Template
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="py-6">
|
<div class="py-6">
|
||||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white shadow sm:rounded-lg p-6 space-y-6">
|
<Card>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<CardHeader>
|
||||||
<div>
|
<CardTitle>Konfiguracija predloge</CardTitle>
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<CardDescription
|
||||||
>Client (optional)</label
|
>Določite nastavitve predloge uvoza in ciljne entitete</CardDescription
|
||||||
>
|
>
|
||||||
<Multiselect
|
</CardHeader>
|
||||||
v-model="form.client_uuid"
|
<CardContent class="space-y-6">
|
||||||
:options="props.clients || []"
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
:reduce="(c) => c.uuid"
|
<div class="space-y-2">
|
||||||
track-by="uuid"
|
<Label for="client">Stranka (izbirno)</Label>
|
||||||
label="name"
|
<Select v-model="form.client_uuid">
|
||||||
placeholder="Global (no client)"
|
<SelectTrigger id="client">
|
||||||
:searchable="true"
|
<SelectValue placeholder="Globalno (brez stranke)" />
|
||||||
:allow-empty="true"
|
</SelectTrigger>
|
||||||
class="mt-1"
|
<SelectContent>
|
||||||
/>
|
<SelectItem :value="null">Globalno (brez stranke)</SelectItem>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<SelectItem
|
||||||
Leave empty to make this template global (visible to all clients).
|
v-for="c in props.clients || []"
|
||||||
</p>
|
:key="c.uuid"
|
||||||
</div>
|
:value="c.uuid"
|
||||||
<div>
|
>
|
||||||
<div class="flex items-center justify-between">
|
{{ c.name }}
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
</SelectItem>
|
||||||
>Entities (tables)</label
|
</SelectContent>
|
||||||
>
|
</Select>
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<p class="text-xs text-muted-foreground">
|
||||||
<label class="inline-flex items-center gap-2">
|
Pustite prazno za globalno predlogo (vidno vsem strankam).
|
||||||
<input
|
</p>
|
||||||
type="checkbox"
|
|
||||||
v-model="form.meta.history_import"
|
|
||||||
class="rounded"
|
|
||||||
/>
|
|
||||||
<span>History import</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="form.meta.payments_import"
|
|
||||||
class="rounded"
|
|
||||||
/>
|
|
||||||
<span>Payments import</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!form.meta.payments_import">
|
<div class="space-y-2">
|
||||||
<Multiselect
|
<Label>Entitete (tabele)</Label>
|
||||||
v-model="form.entities"
|
<template v-if="!form.meta.payments_import">
|
||||||
:options="[
|
<AppMultiSelect
|
||||||
{ value: 'person', label: 'Person' },
|
v-model="form.entities"
|
||||||
{ value: 'person_addresses', label: 'Person Addresses' },
|
:items="[
|
||||||
{ value: 'person_phones', label: 'Person Phones' },
|
{ value: 'person', label: 'Oseba' },
|
||||||
{ value: 'client_cases', label: 'Client Cases' },
|
{ value: 'person_addresses', label: 'Naslov' },
|
||||||
{ value: 'emails', label: 'Emails' },
|
{ value: 'person_phones', label: 'Telefon' },
|
||||||
{ value: 'accounts', label: 'Accounts' },
|
{ value: 'client_cases', label: 'Primer' },
|
||||||
{ value: 'contracts', label: 'Contracts' },
|
{ value: 'emails', label: 'E-pošta' },
|
||||||
{ value: 'case_objects', label: 'Case Objects' },
|
{ value: 'accounts', label: 'Računi' },
|
||||||
{ value: 'payments', label: 'Payments' },
|
{ value: 'contracts', label: 'Pogodbe' },
|
||||||
{ value: 'activities', label: 'Activities' },
|
{ value: 'case_objects', label: 'Predmet' },
|
||||||
]"
|
{ value: 'payments', label: 'Plačilo' },
|
||||||
:multiple="true"
|
{ value: 'activities', label: 'Aktivnost' },
|
||||||
track-by="value"
|
]"
|
||||||
label="label"
|
placeholder="Izberite eno ali več entitet"
|
||||||
:reduce="(o) => o.value"
|
search-placeholder="Iskanje entitet..."
|
||||||
placeholder="Select one or more entities"
|
content-class="p-0 w-full"
|
||||||
:searchable="false"
|
/>
|
||||||
class="mt-1"
|
</template>
|
||||||
/>
|
<template v-else>
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="mt-1">
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span class="inline-flex items-center rounded-full bg-emerald-100 text-emerald-800 px-3 py-1 text-xs font-medium">Contracts</span>
|
<Badge variant="secondary">Pogodbe</Badge>
|
||||||
<span class="inline-flex items-center rounded-full bg-emerald-100 text-emerald-800 px-3 py-1 text-xs font-medium">Accounts</span>
|
<Badge variant="secondary">Računi</Badge>
|
||||||
<span class="inline-flex items-center rounded-full bg-emerald-100 text-emerald-800 px-3 py-1 text-xs font-medium">Payments</span>
|
<Badge variant="secondary">Plačila</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
Izberite katere tabele ta predloga cilja. Preslikave stolpcev lahko
|
||||||
|
dodate kasneje.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Import Mode Toggles -->
|
||||||
|
<div class="flex items-center gap-6 pt-2">
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
:checked="form.meta.history_import"
|
||||||
|
@update:checked="form.meta.history_import = $event"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Uvoz zgodovine</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
:checked="form.meta.payments_import"
|
||||||
|
@update:checked="form.meta.payments_import = $event"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Uvoz plačil</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<div
|
||||||
Choose which tables this template targets. You can still define per-column
|
v-if="form.meta.history_import"
|
||||||
mappings later.
|
class="mt-2 text-xs text-muted-foreground"
|
||||||
</p>
|
|
||||||
<div v-if="form.meta.history_import" class="mt-2 text-xs text-gray-600">
|
|
||||||
History mode allows only person/address/phone/contracts/activities/client cases. Accounts are auto-added when contracts are present and balances stay unchanged.
|
|
||||||
</div>
|
|
||||||
<div v-if="form.meta.payments_import" class="mt-2 text-xs text-gray-600">
|
|
||||||
Payments mode locks entities to:
|
|
||||||
<span class="font-medium">Contracts → Accounts → Payments</span> and
|
|
||||||
optimizes matching for payments import.
|
|
||||||
</div>
|
|
||||||
<div v-if="form.meta.payments_import" class="mt-3">
|
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
|
||||||
>Contract match key</label
|
|
||||||
>
|
>
|
||||||
<select
|
Način zgodovine dovoljuje samo
|
||||||
v-model="form.meta.contract_key_mode"
|
oseba/naslovi/telefoni/pogodbe/aktivnosti/primeri strank. Računi so
|
||||||
class="mt-1 block w-full border rounded p-2"
|
samodejno dodani, ko so prisotne pogodbe in stanja ostanejo
|
||||||
|
nespremenjena.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="form.meta.payments_import"
|
||||||
|
class="mt-2 text-xs text-muted-foreground"
|
||||||
>
|
>
|
||||||
<option value="reference">
|
Način plačil zaklene entitete na:
|
||||||
Reference (use only contract.reference to locate records)
|
<span class="font-medium">Pogodbe → Računi → Plačila</span> in
|
||||||
</option>
|
optimizira ujemanje za uvoz plačil.
|
||||||
</select>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<div v-if="form.meta.payments_import" class="mt-3 space-y-2">
|
||||||
When importing payments, Contract records are located using the selected
|
<Label>Ključ ujemanja pogodb</Label>
|
||||||
key. Use your CSV mapping to map the appropriate column to the contract
|
<Select v-model="form.meta.contract_key_mode">
|
||||||
reference.
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Izberite ključ pogodbe" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="reference">
|
||||||
|
Referenca (uporabi samo contract.reference za iskanje zapisov)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Pri uvozu plačil se zapisi pogodb najdejo z uporabo izbranega ključa.
|
||||||
|
Uporabite CSV preslikavo za povezavo ustreznega stolpca z referenco
|
||||||
|
pogodbe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Defaults: Segment / Decision / Action -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Privzeti segment</Label>
|
||||||
|
<Select v-model="form.meta.segment_id">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="(brez)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem :value="null">(brez)</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="s in props.segments || []"
|
||||||
|
:key="s.id"
|
||||||
|
:value="s.id"
|
||||||
|
>
|
||||||
|
{{ s.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Privzeto dejanje (za aktivnost)</Label>
|
||||||
|
<Select v-model="form.meta.action_id">
|
||||||
|
<SelectTrigger>
|
||||||
|
<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>Privzeta odločitev</Label>
|
||||||
|
<Select v-model="form.meta.decision_id" :disabled="!form.meta.action_id">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="(brez)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem :value="null">(brez)</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="d in decisionsForSelectedAction"
|
||||||
|
:key="d.id"
|
||||||
|
:value="d.id"
|
||||||
|
>
|
||||||
|
{{ d.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p v-if="!form.meta.action_id" class="text-xs text-muted-foreground">
|
||||||
|
Izberite dejanje za ogled njegovih odločitev.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Defaults: Segment / Decision / Action -->
|
<Separator />
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="space-y-2">
|
||||||
<div>
|
<Label for="name">Ime</Label>
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<Input
|
||||||
>Default Segment</label
|
id="name"
|
||||||
>
|
v-model="form.name"
|
||||||
<select
|
|
||||||
v-model="form.meta.segment_id"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
>
|
|
||||||
<option :value="null">(none)</option>
|
|
||||||
<option v-for="s in props.segments || []" :key="s.id" :value="s.id">
|
|
||||||
{{ s.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
|
||||||
>Default Action (for Activity)</label
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-model="form.meta.action_id"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
>
|
|
||||||
<option :value="null">(none)</option>
|
|
||||||
<option v-for="a in props.actions || []" :key="a.id" :value="a.id">
|
|
||||||
{{ a.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
|
||||||
>Default Decision</label
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-model="form.meta.decision_id"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
:disabled="!form.meta.action_id"
|
|
||||||
>
|
|
||||||
<option :value="null">(none)</option>
|
|
||||||
<option v-for="d in decisionsForSelectedAction" :key="d.id" :value="d.id">
|
|
||||||
{{ d.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<p v-if="!form.meta.action_id" class="text-xs text-gray-500 mt-1">
|
|
||||||
Select an Action to see its Decisions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
|
||||||
<input
|
|
||||||
v-model="form.name"
|
|
||||||
type="text"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Description</label>
|
|
||||||
<textarea
|
|
||||||
v-model="form.description"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Source Type</label>
|
|
||||||
<select
|
|
||||||
v-model="form.source_type"
|
|
||||||
class="mt-1 block w-full border rounded p-2"
|
|
||||||
>
|
|
||||||
<option value="csv">CSV</option>
|
|
||||||
<option value="xml">XML</option>
|
|
||||||
<option value="xls">XLS</option>
|
|
||||||
<option value="xlsx">XLSX</option>
|
|
||||||
<option value="json">JSON</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
|
||||||
>Default Record Type (optional)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="form.default_record_type"
|
|
||||||
type="text"
|
type="text"
|
||||||
class="mt-1 block w-full border rounded p-2"
|
placeholder="Vnesite ime predloge"
|
||||||
placeholder="e.g., account, person"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="space-y-2">
|
||||||
<input
|
<Label for="description">Opis</Label>
|
||||||
id="is_active"
|
<Textarea
|
||||||
v-model="form.is_active"
|
id="description"
|
||||||
type="checkbox"
|
v-model="form.description"
|
||||||
class="rounded"
|
placeholder="Vnesite opis predloge"
|
||||||
/>
|
rows="3"
|
||||||
<label for="is_active" class="text-sm font-medium text-gray-700"
|
/>
|
||||||
>Active</label
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2 ml-6">
|
|
||||||
<input id="reactivate" v-model="form.reactivate" type="checkbox" class="rounded" />
|
|
||||||
<label for="reactivate" class="text-sm font-medium text-gray-700">Reactivation import</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<button
|
<div class="space-y-2">
|
||||||
@click.prevent="submit"
|
<Label for="source-type">Tip vira</Label>
|
||||||
class="px-4 py-2 bg-emerald-600 text-white rounded"
|
<Select v-model="form.source_type">
|
||||||
:disabled="form.processing"
|
<SelectTrigger id="source-type">
|
||||||
>
|
<SelectValue />
|
||||||
{{ form.processing ? "Saving…" : "Create Template" }}
|
</SelectTrigger>
|
||||||
</button>
|
<SelectContent>
|
||||||
</div>
|
<SelectItem value="csv">CSV</SelectItem>
|
||||||
|
<SelectItem value="xml">XML</SelectItem>
|
||||||
|
<SelectItem value="xls">XLS</SelectItem>
|
||||||
|
<SelectItem value="xlsx">XLSX</SelectItem>
|
||||||
|
<SelectItem value="json">JSON</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="record-type">Privzeti tip zapisa (izbirno)</Label>
|
||||||
|
<Input
|
||||||
|
id="record-type"
|
||||||
|
v-model="form.default_record_type"
|
||||||
|
type="text"
|
||||||
|
placeholder="npr. račun, oseba"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex items-center gap-6">
|
||||||
v-if="form.errors && Object.keys(form.errors).length"
|
<div class="flex items-center gap-2">
|
||||||
class="text-sm text-red-600"
|
<Checkbox
|
||||||
>
|
id="is_active"
|
||||||
<div v-for="(msg, key) in form.errors" :key="key">{{ msg }}</div>
|
:checked="form.is_active"
|
||||||
</div>
|
@update:checked="form.is_active = $event"
|
||||||
</div>
|
/>
|
||||||
|
<Label for="is_active" class="cursor-pointer">Aktivno</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="reactivate"
|
||||||
|
:checked="form.reactivate"
|
||||||
|
@update:checked="form.reactivate = $event"
|
||||||
|
/>
|
||||||
|
<Label for="reactivate" class="cursor-pointer">Uvoz reaktivacije</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2">
|
||||||
|
<div
|
||||||
|
v-if="form.errors && Object.keys(form.errors).length"
|
||||||
|
class="text-sm text-destructive"
|
||||||
|
>
|
||||||
|
<div v-for="(msg, key) in form.errors" :key="key">{{ msg }}</div>
|
||||||
|
</div>
|
||||||
|
<Button @click.prevent="submit" :disabled="form.processing" class="ml-auto">
|
||||||
|
{{ form.processing ? "Shranjevanje…" : "Ustvari predlogo" }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,32 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
import { Link, useForm } from '@inertiajs/vue3';
|
import { Link, useForm } from "@inertiajs/vue3";
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/Components/ui/card";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/Components/ui/alert-dialog";
|
||||||
|
import { Separator } from "@/Components/ui/separator";
|
||||||
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
import { ListIndentIncreaseIcon } from "lucide-vue-next";
|
||||||
|
import TableActions from "@/Components/DataTable/TableActions.vue";
|
||||||
|
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
|
||||||
|
import { faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
// Non-blocking confirm modal state
|
// Non-blocking confirm modal state
|
||||||
const confirmOpen = ref(false);
|
const confirmOpen = ref(false);
|
||||||
|
|
@ -15,7 +40,7 @@ function requestDelete(uuid) {
|
||||||
|
|
||||||
function performDelete() {
|
function performDelete() {
|
||||||
if (!confirmUuid.value) return;
|
if (!confirmUuid.value) return;
|
||||||
deleteForm.delete(route('importTemplates.destroy', { template: confirmUuid.value }), {
|
deleteForm.delete(route("importTemplates.destroy", { template: confirmUuid.value }), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
confirmOpen.value = false;
|
confirmOpen.value = false;
|
||||||
|
|
@ -42,47 +67,124 @@ const props = defineProps({
|
||||||
|
|
||||||
<div class="py-6">
|
<div class="py-6">
|
||||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white shadow sm:rounded-lg p-6">
|
<AppCard
|
||||||
<div class="flex items-center justify-between mb-4">
|
title=""
|
||||||
<div class="text-sm text-gray-600">Skupaj: {{ props.templates?.length || 0 }}</div>
|
padding="none"
|
||||||
<Link :href="route('importTemplates.create')" class="px-3 py-2 bg-emerald-600 text-white rounded">Nova predloga</Link>
|
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">
|
||||||
|
<ListIndentIncreaseIcon size="18" />
|
||||||
|
<CardTitle class="uppercase">Predloge uvoza</CardTitle>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between border-t border-b py-2 px-4">
|
||||||
|
<div>
|
||||||
|
<CardDescription>
|
||||||
|
Skupaj {{ props.templates?.length || 0 }} predlog{{
|
||||||
|
props.templates?.length === 1 ? "a" : ""
|
||||||
|
}}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button as-child>
|
||||||
|
<Link :href="route('importTemplates.create')"> Nova predloga </Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divide-y">
|
<div
|
||||||
<div v-for="t in props.templates" :key="t.uuid" class="py-3 flex items-center justify-between">
|
v-if="!props.templates || props.templates.length === 0"
|
||||||
<div>
|
class="p-8 text-center text-muted-foreground"
|
||||||
<div class="font-medium">{{ t.name }}</div>
|
>
|
||||||
<div class="text-xs text-gray-500">{{ t.description }}</div>
|
Ni predlog uvoza.
|
||||||
<div class="text-xs text-gray-400 mt-1">{{ t.client?.name || 'Global' }} • {{ t.source_type.toUpperCase() }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span :class="['text-xs px-2 py-0.5 rounded', t.is_active ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-500']">{{ t.is_active ? 'Active' : 'Inactive' }}</span>
|
|
||||||
<Link :href="route('importTemplates.edit', { template: t.uuid })" class="px-3 py-1.5 border rounded text-sm">Uredi</Link>
|
|
||||||
<button
|
|
||||||
class="px-3 py-1.5 border rounded text-sm text-red-700 border-red-300 hover:bg-red-50"
|
|
||||||
@click.prevent="requestDelete(t.uuid)"
|
|
||||||
>Izbriši</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="grid gap-4 p-4">
|
||||||
|
<Card
|
||||||
|
v-for="t in props.templates"
|
||||||
|
:key="t.uuid"
|
||||||
|
class="hover:shadow-md transition-shadow px-0! p-4"
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<CardTitle class="text-base">{{ t.name }}</CardTitle>
|
||||||
|
<CardDescription class="mt-1">
|
||||||
|
{{ t.description }}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<TableActions align="right">
|
||||||
|
<template #default>
|
||||||
|
<ActionMenuItem
|
||||||
|
:icon="faPencil"
|
||||||
|
label="Uredi"
|
||||||
|
@click="
|
||||||
|
$inertia.visit(
|
||||||
|
route('importTemplates.edit', { template: t.uuid })
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<ActionMenuItem
|
||||||
|
:icon="faTrash"
|
||||||
|
label="Izbriši"
|
||||||
|
danger
|
||||||
|
@click="requestDelete(t.uuid)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</TableActions>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="pt-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" class="text-xs">
|
||||||
|
{{ t.client?.name || "Globalno" }}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary" class="text-xs">
|
||||||
|
{{ t.source_type.toUpperCase() }}
|
||||||
|
</Badge>
|
||||||
|
<Badge :variant="t.is_active ? 'default' : 'secondary'" class="text-xs">
|
||||||
|
{{ t.is_active ? "Aktivno" : "Neaktivno" }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Delete Modal -->
|
<!-- Confirm Delete Dialog -->
|
||||||
<div v-if="confirmOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
<AlertDialog
|
||||||
<div class="absolute inset-0 bg-black/30" @click="cancelDelete"></div>
|
:open="confirmOpen"
|
||||||
<div class="relative bg-white rounded shadow-lg w-96 max-w-[90%] p-5">
|
@update:open="
|
||||||
<div class="text-lg font-semibold mb-2">Izbrišem predlogo?</div>
|
(val) => {
|
||||||
<p class="text-sm text-gray-600 mb-4">Tega dejanja ni mogoče razveljaviti. Vse preslikave te predloge bodo izbrisane.</p>
|
if (!val) cancelDelete();
|
||||||
<div class="flex items-center justify-end gap-2">
|
}
|
||||||
<button class="px-3 py-1.5 border rounded" @click.prevent="cancelDelete" :disabled="deleteForm.processing">Prekliči</button>
|
"
|
||||||
<button class="px-3 py-1.5 rounded text-white bg-red-600 disabled:opacity-60" @click.prevent="performDelete" :disabled="deleteForm.processing">
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Izbrišem predlogo?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Tega dejanja ni mogoče razveljaviti. Vse preslikave te predloge bodo
|
||||||
|
izbrisane.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel @click="cancelDelete" :disabled="deleteForm.processing">
|
||||||
|
Prekliči
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
@click="performDelete"
|
||||||
|
:disabled="deleteForm.processing"
|
||||||
|
class="bg-destructive hover:bg-destructive/90"
|
||||||
|
>
|
||||||
<span v-if="deleteForm.processing">Brisanje…</span>
|
<span v-if="deleteForm.processing">Brisanje…</span>
|
||||||
<span v-else>Izbriši</span>
|
<span v-else>Izbriši</span>
|
||||||
</button>
|
</AlertDialogAction>
|
||||||
</div>
|
</AlertDialogFooter>
|
||||||
</div>
|
</AlertDialogContent>
|
||||||
</div>
|
</AlertDialog>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
<script setup>
|
||||||
|
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 { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
form: { type: Object, required: true },
|
||||||
|
clients: { type: Array, default: () => [] },
|
||||||
|
segments: { type: Array, default: () => [] },
|
||||||
|
actions: { type: Array, default: () => [] },
|
||||||
|
decisions: { type: Array, default: () => [] },
|
||||||
|
canChangeClient: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["save"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Osnovne informacije</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-6">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<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">
|
||||||
|
<SelectTrigger id="source_type">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="csv">CSV</SelectItem>
|
||||||
|
<SelectItem value="xml">XML</SelectItem>
|
||||||
|
<SelectItem value="xls">XLS</SelectItem>
|
||||||
|
<SelectItem value="xlsx">XLSX</SelectItem>
|
||||||
|
<SelectItem value="json">JSON</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="default_record_type">Privzeti tip zapisa</Label>
|
||||||
|
<Input
|
||||||
|
id="default_record_type"
|
||||||
|
v-model="form.default_record_type"
|
||||||
|
type="text"
|
||||||
|
placeholder="npr.: account, person"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="client">Naročnik (izbirno)</Label>
|
||||||
|
<Select v-model="form.client_uuid" :disabled="!canChangeClient">
|
||||||
|
<SelectTrigger id="client">
|
||||||
|
<SelectValue placeholder="Globalno (brez naročnika)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem :value="null">Globalno (brez naročnika)</SelectItem>
|
||||||
|
<SelectItem v-for="c in clients || []" :key="c.uuid" :value="c.uuid">
|
||||||
|
{{ c.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p v-if="!canChangeClient" class="text-xs text-amber-600">
|
||||||
|
Ni mogoče spremeniti naročnika, ker ta predloga že vsebuje preslikave.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="delimiter">Privzeti ločilni znak (CSV)</Label>
|
||||||
|
<Select v-model="form.meta.delimiter">
|
||||||
|
<SelectTrigger id="delimiter">
|
||||||
|
<SelectValue placeholder="(Auto-detect)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
|
||||||
|
<SelectItem value=",">Vejica ,</SelectItem>
|
||||||
|
<SelectItem value=";">Podpičje ;</SelectItem>
|
||||||
|
<SelectItem value="\t">Tab \t</SelectItem>
|
||||||
|
<SelectItem value="|">Pipe |</SelectItem>
|
||||||
|
<SelectItem value=" ">Presledek ␠</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Pusti prazno za samodejno zaznavo. Uporabi, ko zaznavanje ne deluje pravilno.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="segment">Privzeti segment</Label>
|
||||||
|
<Select v-model="form.meta.segment_id">
|
||||||
|
<SelectTrigger id="segment">
|
||||||
|
<SelectValue placeholder="(brez)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem :value="null">(brez)</SelectItem>
|
||||||
|
<SelectItem v-for="s in segments || []" :key="s.id" :value="s.id">
|
||||||
|
{{ s.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="action">Privzeto dejanje (post-contract activity)</Label>
|
||||||
|
<Select v-model="form.meta.action_id">
|
||||||
|
<SelectTrigger id="action">
|
||||||
|
<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">Privzeta odločitev (post-contract)</Label>
|
||||||
|
<Select v-model="form.meta.decision_id" :disabled="!form.meta.action_id">
|
||||||
|
<SelectTrigger id="decision">
|
||||||
|
<SelectValue placeholder="(brez)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem :value="null">(brez)</SelectItem>
|
||||||
|
<SelectItem v-for="d in decisions || []" :key="d.id" :value="d.id">
|
||||||
|
{{ d.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p v-if="!form.meta.action_id" class="text-xs text-muted-foreground">
|
||||||
|
Najprej izberi dejanje, nato odločitev.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<Label for="reactivate" class="cursor-pointer">Reaktivacija</Label>
|
||||||
|
</div>
|
||||||
|
<Button @click="emit('save')" class="ml-auto">Shrani</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
301
resources/js/Pages/Imports/Templates/Partials/EntityMappings.vue
Normal file
301
resources/js/Pages/Imports/Templates/Partials/EntityMappings.vue
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { router } from "@inertiajs/vue3";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/Components/ui/accordion";
|
||||||
|
import { Card, CardContent } from "@/Components/ui/card";
|
||||||
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import { Textarea } from "@/Components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/Components/ui/select";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
import { ArrowUp, ArrowDown } from "lucide-vue-next";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
entities: { type: Array, default: () => [] },
|
||||||
|
entityOptions: { type: Array, default: () => [] },
|
||||||
|
fieldOptions: { type: Object, default: () => ({}) },
|
||||||
|
mappings: { type: Array, default: () => [] },
|
||||||
|
templateUuid: { type: String, required: true },
|
||||||
|
allSourceColumns: { type: Array, default: () => [] },
|
||||||
|
entityAliases: { type: Object, default: () => ({}) },
|
||||||
|
actions: { type: Array, default: () => [] },
|
||||||
|
decisions: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["refresh"]);
|
||||||
|
|
||||||
|
const newRows = ref({});
|
||||||
|
const bulkRows = ref({});
|
||||||
|
|
||||||
|
function addRow(entity) {
|
||||||
|
const row = newRows.value[entity];
|
||||||
|
if (!row || !row.source || !row.field) return;
|
||||||
|
const target_field = `${entity}.${row.field}`;
|
||||||
|
const opts = {};
|
||||||
|
if (row.group) opts.group = row.group;
|
||||||
|
if (row.field === "meta" && row.metaKey) {
|
||||||
|
opts.key = String(row.metaKey).trim();
|
||||||
|
if (row.metaType) opts.type = String(row.metaType).trim();
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
source_column: row.source,
|
||||||
|
target_field,
|
||||||
|
transform: row.transform || "",
|
||||||
|
apply_mode: row.apply_mode || "both",
|
||||||
|
options: Object.keys(opts).length ? opts : null,
|
||||||
|
position: (props.mappings?.length || 0) + 1,
|
||||||
|
};
|
||||||
|
router.post(
|
||||||
|
route("importTemplates.mappings.add", { template: props.templateUuid }),
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
newRows.value[entity] = {};
|
||||||
|
emit("refresh");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMapping(m) {
|
||||||
|
const payload = {
|
||||||
|
source_column: m.source_column,
|
||||||
|
target_field: m.target_field,
|
||||||
|
transform: m.transform || "",
|
||||||
|
apply_mode: m.apply_mode || "both",
|
||||||
|
options: m.options || null,
|
||||||
|
position: m.position,
|
||||||
|
};
|
||||||
|
router.put(
|
||||||
|
route("importTemplates.mappings.update", {
|
||||||
|
template: props.templateUuid,
|
||||||
|
mapping: m.id,
|
||||||
|
}),
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => emit("refresh"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMapping(m) {
|
||||||
|
router.delete(
|
||||||
|
route("importTemplates.mappings.delete", {
|
||||||
|
template: props.templateUuid,
|
||||||
|
mapping: m.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => emit("refresh"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorder(entity, direction, m) {
|
||||||
|
const all = [...props.mappings];
|
||||||
|
const aliases = (props.entityAliases[entity] || [entity]).map((a) => a + ".");
|
||||||
|
const entityMaps = all.filter((x) => {
|
||||||
|
const tf = x.target_field || "";
|
||||||
|
return aliases.some((prefix) => tf.startsWith(prefix));
|
||||||
|
});
|
||||||
|
const idx = entityMaps.findIndex((x) => x.id === m.id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
const swapIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||||
|
if (swapIdx < 0 || swapIdx >= entityMaps.length) return;
|
||||||
|
const a = entityMaps[idx];
|
||||||
|
const b = entityMaps[swapIdx];
|
||||||
|
const ordered = all.map((x) => x.id);
|
||||||
|
const ai = ordered.indexOf(a.id);
|
||||||
|
const bi = ordered.indexOf(b.id);
|
||||||
|
if (ai < 0 || bi < 0) return;
|
||||||
|
[ordered[ai], ordered[bi]] = [ordered[bi], ordered[ai]];
|
||||||
|
router.post(
|
||||||
|
route("importTemplates.mappings.reorder", { template: props.templateUuid }),
|
||||||
|
{ order: ordered },
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => emit("refresh"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntityMappings(entity) {
|
||||||
|
const aliases = (props.entityAliases[entity] || [entity]).map((a) => a + ".");
|
||||||
|
return (props.mappings || []).filter((m) => {
|
||||||
|
const tf = m.target_field || "";
|
||||||
|
return aliases.some((prefix) => tf.startsWith(prefix));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardContent class="p-0">
|
||||||
|
<Accordion type="multiple" collapsible class="w-full">
|
||||||
|
<AccordionItem v-for="entity in entities" :key="entity" :value="entity">
|
||||||
|
<AccordionTrigger class="px-4 hover:no-underline">
|
||||||
|
<span class="font-medium">
|
||||||
|
{{ entityOptions.find((e) => e.key === entity)?.label || entity }}
|
||||||
|
</span>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent class="px-4 pb-4 space-y-4">
|
||||||
|
<!-- Existing mappings -->
|
||||||
|
<div v-if="getEntityMappings(entity).length > 0" class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="m in getEntityMappings(entity)"
|
||||||
|
:key="m.id"
|
||||||
|
class="p-3 border rounded-lg bg-muted/30"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-5 gap-2 items-center">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-xs">Izvor</Label>
|
||||||
|
<Input v-model="m.source_column" class="text-sm" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-xs">Cilj</Label>
|
||||||
|
<Input v-model="m.target_field" class="text-sm" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-xs">Transform</Label>
|
||||||
|
<Select v-model="m.transform">
|
||||||
|
<SelectTrigger class="text-sm">
|
||||||
|
<SelectValue placeholder="Brez" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="trim">trim</SelectItem>
|
||||||
|
<SelectItem value="upper">upper</SelectItem>
|
||||||
|
<SelectItem value="lower">lower</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-xs">Način</Label>
|
||||||
|
<Select v-model="m.apply_mode">
|
||||||
|
<SelectTrigger class="text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="both">both</SelectItem>
|
||||||
|
<SelectItem value="insert">insert</SelectItem>
|
||||||
|
<SelectItem value="update">update</SelectItem>
|
||||||
|
<SelectItem value="keyref">keyref</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end gap-2">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
class="h-6 w-6"
|
||||||
|
@click="reorder(entity, 'up', m)"
|
||||||
|
>
|
||||||
|
<ArrowUp class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
class="h-6 w-6"
|
||||||
|
@click="reorder(entity, 'down', m)"
|
||||||
|
>
|
||||||
|
<ArrowDown class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button size="sm" @click="updateMapping(m)">Shrani</Button>
|
||||||
|
<Button size="sm" variant="destructive" @click="deleteMapping(m)">
|
||||||
|
Izbriši
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground py-4 text-center">
|
||||||
|
Ni definiranih preslikav za to entiteto.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add new mapping -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="text-sm font-medium">Dodaj novo preslikavo</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-4 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-xs">Izvorno polje</Label>
|
||||||
|
<Input
|
||||||
|
v-model="(newRows[entity] ||= {}).source"
|
||||||
|
placeholder="npr.: reference"
|
||||||
|
list="`src-opts-${entity}`"
|
||||||
|
/>
|
||||||
|
<datalist :id="`src-opts-${entity}`">
|
||||||
|
<option v-for="s in allSourceColumns" :key="s" :value="s" />
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-xs">Polje</Label>
|
||||||
|
<Select v-model="(newRows[entity] ||= {}).field">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="f in fieldOptions[entity] || []"
|
||||||
|
:key="f"
|
||||||
|
:value="f"
|
||||||
|
>
|
||||||
|
{{ f }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-xs">Transform</Label>
|
||||||
|
<Select v-model="(newRows[entity] ||= {}).transform">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Brez" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="trim">trim</SelectItem>
|
||||||
|
<SelectItem value="upper">upper</SelectItem>
|
||||||
|
<SelectItem value="lower">lower</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-xs">Način</Label>
|
||||||
|
<Select v-model="(newRows[entity] ||= {}).apply_mode">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="both" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="both">both</SelectItem>
|
||||||
|
<SelectItem value="insert">insert</SelectItem>
|
||||||
|
<SelectItem value="update">update</SelectItem>
|
||||||
|
<SelectItem value="keyref">keyref</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button @click="addRow(entity)" size="sm">Dodaj preslikavo</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup>
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/Components/ui/card";
|
||||||
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
form: { type: Object, required: true },
|
||||||
|
entities: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Nastavitve načina uvoza</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Konfiguriraj način uvoza za zgodovino ali plačila
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
:checked="form.meta.history_import"
|
||||||
|
@update:checked="form.meta.history_import = $event"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium">Uvoz zgodovine</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
:checked="form.meta.payments_import"
|
||||||
|
@update:checked="form.meta.payments_import = $event"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium">Uvoz plačil</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Zgodovina dovoljuje oseba/naslov/telefon/pogodbe/aktivnosti/primeri strank; računi so
|
||||||
|
samodejno dodani s pogodbami. Plačila zaklene entitete na Pogodbe → Računi → Plačila.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="form.meta.payments_import" class="space-y-2 pt-2">
|
||||||
|
<Label for="contract_key">Ključ ujemanja pogodb</Label>
|
||||||
|
<Select v-model="form.meta.contract_key_mode">
|
||||||
|
<SelectTrigger id="contract_key">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="reference">
|
||||||
|
Referenca (uporabi samo contract.reference za iskanje zapisov)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Preslika stolpec CSV na contract.reference za reševanje pogodb za tega naročnika.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entities locked info for payments mode -->
|
||||||
|
<div v-if="form.meta.payments_import" class="p-3 bg-emerald-50 rounded-lg border border-emerald-200">
|
||||||
|
<div class="text-sm text-emerald-900 mb-2 font-medium">Entitete so zaklenjene:</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Pogodbe</Badge>
|
||||||
|
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Računi</Badge>
|
||||||
|
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Plačila</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useForm } from "@inertiajs/vue3";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} 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 { Button } from "@/Components/ui/button";
|
||||||
|
import { Badge } from "@/Components/ui/badge";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
unassigned: { type: Array, default: () => [] },
|
||||||
|
templateUuid: { type: String, required: true },
|
||||||
|
entityOptions: { type: Array, default: () => [] },
|
||||||
|
fieldOptions: { type: Object, default: () => ({}) },
|
||||||
|
suggestions: { type: Object, default: () => ({}) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["refresh"]);
|
||||||
|
|
||||||
|
const unassignedState = ref({});
|
||||||
|
|
||||||
|
function saveUnassigned(m) {
|
||||||
|
const st = unassignedState.value[m.id] || {};
|
||||||
|
if (st.entity && st.field) {
|
||||||
|
m.target_field = `${st.entity}.${st.field}`;
|
||||||
|
} else {
|
||||||
|
m.target_field = null;
|
||||||
|
}
|
||||||
|
if (st.group) {
|
||||||
|
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||||
|
m.options.group = st.group;
|
||||||
|
}
|
||||||
|
if (st.field === "meta") {
|
||||||
|
if (st.metaKey && String(st.metaKey).trim() !== "") {
|
||||||
|
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||||
|
m.options.key = String(st.metaKey).trim();
|
||||||
|
}
|
||||||
|
if (st.metaType && String(st.metaType).trim() !== "") {
|
||||||
|
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||||
|
m.options.type = String(st.metaType).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
source_column: m.source_column,
|
||||||
|
target_field: m.target_field,
|
||||||
|
transform: m.transform,
|
||||||
|
apply_mode: m.apply_mode,
|
||||||
|
options: m.options || null,
|
||||||
|
position: m.position,
|
||||||
|
};
|
||||||
|
useForm(payload).put(
|
||||||
|
route("importTemplates.mappings.update", {
|
||||||
|
template: props.templateUuid,
|
||||||
|
mapping: m.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => emit("refresh"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMapping(m) {
|
||||||
|
useForm({}).delete(
|
||||||
|
route("importTemplates.mappings.delete", {
|
||||||
|
template: props.templateUuid,
|
||||||
|
mapping: m.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => emit("refresh"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySuggestion(m, suggestion) {
|
||||||
|
if (!suggestion || !suggestion.entity || !suggestion.field) return;
|
||||||
|
const state = unassignedState.value[m.id] || {};
|
||||||
|
state.entity = suggestion.entity;
|
||||||
|
state.field = suggestion.field;
|
||||||
|
unassignedState.value[m.id] = state;
|
||||||
|
saveUnassigned(m);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card v-if="unassigned && unassigned.length > 0" class="border-amber-200">
|
||||||
|
<CardHeader class="bg-amber-50">
|
||||||
|
<CardTitle class="text-amber-900">
|
||||||
|
Nedodeljene preslikave ({{ unassigned.length }})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="text-amber-700">
|
||||||
|
Te preslikave nimajo dodeljene ciljne entitete in polja
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-4 space-y-3">
|
||||||
|
<div v-for="m in unassigned" :key="m.id" class="p-3 bg-white border rounded-lg">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- Source column with suggestion -->
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<Label class="text-xs text-muted-foreground">Izvorno polje</Label>
|
||||||
|
<div class="font-medium">{{ m.source_column }}</div>
|
||||||
|
<div v-if="suggestions && suggestions[m.source_column]" class="mt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
class="h-auto p-0 text-xs text-indigo-700"
|
||||||
|
@click="applySuggestion(m, suggestions[m.source_column])"
|
||||||
|
>
|
||||||
|
Predlog: {{ suggestions[m.source_column].entity }}.{{
|
||||||
|
suggestions[m.source_column].field
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entity and Field selection -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="entity">Entiteta</Label>
|
||||||
|
<Select v-model="(unassignedState[m.id] ||= {}).entity">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="(izberi)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="opt in entityOptions"
|
||||||
|
:key="opt.key"
|
||||||
|
:value="opt.key"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="field">Polje</Label>
|
||||||
|
<Select
|
||||||
|
v-model="(unassignedState[m.id] ||= {}).field"
|
||||||
|
:disabled="!(unassignedState[m.id] || {}).entity"
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="(izberi)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="f in fieldOptions[(unassignedState[m.id] || {}).entity] || []"
|
||||||
|
:key="f"
|
||||||
|
:value="f"
|
||||||
|
>
|
||||||
|
{{ f }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transform, Apply Mode, Group -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Transform</Label>
|
||||||
|
<Select v-model="m.transform">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Brez" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="trim">trim</SelectItem>
|
||||||
|
<SelectItem value="upper">upper</SelectItem>
|
||||||
|
<SelectItem value="lower">lower</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Način</Label>
|
||||||
|
<Select v-model="m.apply_mode">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="both">both</SelectItem>
|
||||||
|
<SelectItem value="insert">insert</SelectItem>
|
||||||
|
<SelectItem value="update">update</SelectItem>
|
||||||
|
<SelectItem value="keyref">keyref</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Skupina</Label>
|
||||||
|
<Input
|
||||||
|
v-model="(unassignedState[m.id] ||= {}).group"
|
||||||
|
placeholder="1, 2, home, work"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta fields if applicable -->
|
||||||
|
<div
|
||||||
|
v-if="(unassignedState[m.id] || {}).field === 'meta'"
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 gap-3"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Meta ključ</Label>
|
||||||
|
<Input
|
||||||
|
v-model="(unassignedState[m.id] ||= {}).metaKey"
|
||||||
|
placeholder="npr.: note, category"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>Meta tip</Label>
|
||||||
|
<Select v-model="(unassignedState[m.id] ||= {}).metaType">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="(auto/string)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="string">string</SelectItem>
|
||||||
|
<SelectItem value="number">number</SelectItem>
|
||||||
|
<SelectItem value="date">date</SelectItem>
|
||||||
|
<SelectItem value="boolean">boolean</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center gap-2 pt-2">
|
||||||
|
<Button size="sm" @click="saveUnassigned(m)">Shrani</Button>
|
||||||
|
<Button size="sm" variant="destructive" @click="deleteMapping(m)"
|
||||||
|
>Izbriši</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
// this import. This is nice for IDE syntax and refactoring.
|
// this import. This is nice for IDE syntax and refactoring.
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\ClientCase;
|
use App\Models\ClientCase;
|
||||||
|
use App\Models\ImportTemplate;
|
||||||
use App\Models\Segment;
|
use App\Models\Segment;
|
||||||
use Diglactic\Breadcrumbs\Breadcrumbs;
|
use Diglactic\Breadcrumbs\Breadcrumbs;
|
||||||
// This import is also not required, and you could replace `BreadcrumbTrail $trail`
|
// This import is also not required, and you could replace `BreadcrumbTrail $trail`
|
||||||
|
|
@ -107,3 +108,33 @@
|
||||||
$trail->parent('dashboard');
|
$trail->parent('dashboard');
|
||||||
$trail->push('Terenska dela', route('fieldjobs.index'));
|
$trail->push('Terenska dela', route('fieldjobs.index'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dashboard > Imports
|
||||||
|
|
||||||
|
Breadcrumbs::for('imports.index', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('dashboard');
|
||||||
|
$trail->push('Uvozi', route('imports.index'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dashboard > Imports > Templates
|
||||||
|
|
||||||
|
Breadcrumbs::for('importTemplates.index', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('imports.index');
|
||||||
|
$trail->push('Uvozne predloge', route('importTemplates.index'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Dashboard > Imports > Templates > Create
|
||||||
|
|
||||||
|
Breadcrumbs::for('importTemplates.create', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('importTemplates.index');
|
||||||
|
$trail->push('Ustvari', route('importTemplates.create'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Dashboard > Imports > Templates > Edit > [Template]
|
||||||
|
|
||||||
|
Breadcrumbs::for('importTemplates.edit', function (BreadcrumbTrail $trail, ImportTemplate $template) {
|
||||||
|
$trail->parent('importTemplates.index');
|
||||||
|
$trail->push($template->name, route('importTemplates.edit', $template));
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user