Changes to post|put|patch|delete
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<script setup>
|
||||
import UpdateDialog from '@/Components/Dialogs/UpdateDialog.vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import * as z from 'zod'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { router } from '@inertiajs/vue3'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/Components/ui/form'
|
||||
import { Input } from '@/Components/ui/input'
|
||||
import { Textarea } from '@/Components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select'
|
||||
import { Checkbox } from '@/Components/ui/checkbox'
|
||||
import { Switch } from '@/Components/ui/switch'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
client_case_uuid: { type: String, required: true },
|
||||
document: { type: Object, default: null },
|
||||
contracts: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'saved'])
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
name: z.string().min(1, 'Ime je obvezno'),
|
||||
description: z.string().optional(),
|
||||
is_public: z.boolean().default(false),
|
||||
contract_uuid: z.string().nullable().optional(),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
})
|
||||
|
||||
|
||||
const processing = ref(false)
|
||||
|
||||
const update = async () => {
|
||||
if (!props.document) return
|
||||
|
||||
processing.value = true
|
||||
const { values } = form
|
||||
|
||||
router.patch(
|
||||
route('clientCase.document.update', {
|
||||
client_case: props.client_case_uuid,
|
||||
document: props.document.uuid,
|
||||
}),
|
||||
values,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
emit('saved')
|
||||
close()
|
||||
},
|
||||
onError: (errors) => {
|
||||
// Map Inertia errors to VeeValidate field errors
|
||||
Object.keys(errors).forEach((field) => {
|
||||
const errorMessages = Array.isArray(errors[field])
|
||||
? errors[field]
|
||||
: [errors[field]]
|
||||
form.setFieldError(field, errorMessages[0])
|
||||
})
|
||||
processing.value = false
|
||||
},
|
||||
onFinish: () => {
|
||||
processing.value = false
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emit('close')
|
||||
processing.value = false
|
||||
}
|
||||
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
update()
|
||||
})
|
||||
|
||||
const onConfirm = () => {
|
||||
onSubmit()
|
||||
}
|
||||
|
||||
const contractOptions = computed(() => {
|
||||
return props.contracts || []
|
||||
})
|
||||
|
||||
// Watch for dialog opening and document changes
|
||||
watch(
|
||||
() => [props.show, props.document],
|
||||
() => {
|
||||
if (!props.show) {
|
||||
return
|
||||
}
|
||||
// When dialog opens, reset form with document values
|
||||
if (props.document) {
|
||||
form.resetForm({
|
||||
values: {
|
||||
name: props.document.name || props.document.original_name || '',
|
||||
description: props.document.description || '',
|
||||
is_public: !!props.document.is_public,
|
||||
contract_uuid: (props.document?.documentable_type || '').toLowerCase().includes('contract') ? (props.document.contract_uuid || null) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UpdateDialog
|
||||
:show="show"
|
||||
title="Uredi dokument"
|
||||
:processing="processing"
|
||||
@close="close"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<form @submit.prevent="onSubmit" class="space-y-4">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>Ime</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="docName" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="description">
|
||||
<FormItem>
|
||||
<FormLabel>Opis</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea id="docDesc" v-bind="componentField" rows="3" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="is_public">
|
||||
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
:model-value="value"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
<div class="space-y-1 leading-none">
|
||||
<FormLabel>Javno</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="contract_uuid">
|
||||
<FormItem>
|
||||
<FormLabel>Pogodba</FormLabel>
|
||||
<Select :model-value="value" @update:model-value="handleChange">
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="— Brez — (dok. pri primeru)" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">— Brez — (dok. pri primeru)</SelectItem>
|
||||
<SelectItem
|
||||
v-for="c in contractOptions"
|
||||
:key="c.uuid || c.id"
|
||||
:value="c.uuid"
|
||||
>
|
||||
{{ c.reference || c.uuid }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</form>
|
||||
</UpdateDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import * as z from 'zod'
|
||||
import { ref, watch } from 'vue'
|
||||
import { router } from '@inertiajs/vue3'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/Components/ui/form'
|
||||
import { Input } from '@/Components/ui/input'
|
||||
import { Textarea } from '@/Components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select'
|
||||
import { Checkbox } from '@/Components/ui/checkbox'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
postUrl: { type: String, required: true },
|
||||
// Optional list of contracts to allow attaching the document directly to a contract
|
||||
// Each item should have at least: { uuid, reference }
|
||||
contracts: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['close', 'uploaded'])
|
||||
|
||||
const MAX_SIZE = 25 * 1024 * 1024 // 25MB
|
||||
const ALLOWED_EXTS = ['doc','docx','pdf','txt','csv','xls','xlsx','jpeg','jpg','png']
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
name: z.string().min(1, 'Ime je obvezno'),
|
||||
description: z.string().optional(),
|
||||
file: z.instanceof(File).refine((file) => file.size > 0, 'Izberite datoteko'),
|
||||
is_public: z.boolean().default(true),
|
||||
contract_uuid: z.string().nullable().optional(),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
file: null,
|
||||
is_public: true,
|
||||
contract_uuid: null,
|
||||
},
|
||||
})
|
||||
|
||||
const localError = ref('')
|
||||
|
||||
watch(() => props.show, (v) => {
|
||||
if (!v) return
|
||||
localError.value = ''
|
||||
form.resetForm()
|
||||
})
|
||||
|
||||
const onFileChange = (e) => {
|
||||
localError.value = ''
|
||||
const f = e.target.files?.[0]
|
||||
if (!f) {
|
||||
form.setFieldValue('file', null)
|
||||
return
|
||||
}
|
||||
const ext = (f.name.split('.').pop() || '').toLowerCase()
|
||||
if (!ALLOWED_EXTS.includes(ext)) {
|
||||
localError.value = 'Nepodprta vrsta datoteke. Dovoljeno: ' + ALLOWED_EXTS.join(', ')
|
||||
e.target.value = ''
|
||||
form.setFieldValue('file', null)
|
||||
return
|
||||
}
|
||||
if (f.size > MAX_SIZE) {
|
||||
localError.value = 'Datoteka je prevelika. Največja velikost je 25MB.'
|
||||
e.target.value = ''
|
||||
form.setFieldValue('file', null)
|
||||
return
|
||||
}
|
||||
form.setFieldValue('file', f)
|
||||
if (!form.values.name) {
|
||||
form.setFieldValue('name', f.name.replace(/\.[^.]+$/, ''))
|
||||
}
|
||||
}
|
||||
|
||||
const submit = form.handleSubmit(async (values) => {
|
||||
localError.value = ''
|
||||
if (!values.file) {
|
||||
localError.value = 'Prosimo izberite datoteko.'
|
||||
return
|
||||
}
|
||||
const ext = (values.file.name.split('.').pop() || '').toLowerCase()
|
||||
if (!ALLOWED_EXTS.includes(ext)) {
|
||||
localError.value = 'Nepodprta vrsta datoteke. Dovoljeno: ' + ALLOWED_EXTS.join(', ')
|
||||
return
|
||||
}
|
||||
if (values.file.size > MAX_SIZE) {
|
||||
localError.value = 'Datoteka je prevelika. Največja velikost je 25MB.'
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('name', values.name)
|
||||
formData.append('description', values.description || '')
|
||||
formData.append('file', values.file)
|
||||
formData.append('is_public', values.is_public ? '1' : '0')
|
||||
if (values.contract_uuid) {
|
||||
formData.append('contract_uuid', values.contract_uuid)
|
||||
}
|
||||
|
||||
router.post(props.postUrl, formData, {
|
||||
forceFormData: true,
|
||||
onSuccess: () => {
|
||||
emit('uploaded')
|
||||
emit('close')
|
||||
form.resetForm()
|
||||
},
|
||||
onError: (errors) => {
|
||||
// Set form errors if any
|
||||
if (errors.name) form.setFieldError('name', errors.name)
|
||||
if (errors.description) form.setFieldError('description', errors.description)
|
||||
if (errors.file) form.setFieldError('file', errors.file)
|
||||
if (errors.contract_uuid) form.setFieldError('contract_uuid', errors.contract_uuid)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const close = () => emit('close')
|
||||
|
||||
const onConfirm = () => {
|
||||
submit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CreateDialog
|
||||
:show="props.show"
|
||||
title="Dodaj dokument"
|
||||
max-width="lg"
|
||||
confirm-text="Naloži"
|
||||
:processing="!!form.isSubmitting.value"
|
||||
:disabled="!form.values.file"
|
||||
@close="close"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<FormField v-if="props.contracts && props.contracts.length" v-slot="{ value, handleChange }" name="contract_uuid">
|
||||
<FormItem>
|
||||
<FormLabel>Pripiši k</FormLabel>
|
||||
<Select :model-value="value" @update:model-value="handleChange">
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Primer" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Primer</SelectItem>
|
||||
<SelectItem
|
||||
v-for="c in props.contracts"
|
||||
:key="c.uuid"
|
||||
:value="c.uuid"
|
||||
>
|
||||
Pogodba: {{ c.reference }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>Ime</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="doc_name" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="description">
|
||||
<FormItem>
|
||||
<FormLabel>Opis</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea id="doc_desc" v-bind="componentField" rows="3" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="file">
|
||||
<FormItem>
|
||||
<FormLabel>Datoteka (max 25MB)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="doc_file"
|
||||
type="file"
|
||||
@change="onFileChange"
|
||||
accept=".doc,.docx,.pdf,.txt,.csv,.xls,.xlsx,.jpeg,.jpg,.png"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<div v-if="localError" class="text-sm text-red-600 mt-1">{{ localError }}</div>
|
||||
<div v-if="value" class="text-sm text-gray-600 mt-1">
|
||||
Izbrana datoteka: {{ value.name }} ({{ (value.size / 1024).toFixed(2) }} KB)
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="is_public">
|
||||
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
:checked="value"
|
||||
@update:checked="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
<div class="space-y-1 leading-none">
|
||||
<FormLabel>Javno</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</form>
|
||||
</CreateDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/Components/ui/dialog'
|
||||
import { Button } from '@/Components/ui/button'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
src: { type: String, default: '' },
|
||||
title: { type: String, default: 'Dokument' }
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="show" @update:open="(open) => !open && $emit('close')">
|
||||
<DialogContent class="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ props.title }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="h-[70vh]">
|
||||
<iframe v-if="props.src" :src="props.src" class="w-full h-full rounded border" />
|
||||
<div v-else class="text-sm text-gray-500">Ni dokumenta za prikaz.</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<Button type="button" variant="outline" @click="$emit('close')">Zapri</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faFilePdf,
|
||||
faFileWord,
|
||||
faFileExcel,
|
||||
faFileLines,
|
||||
faFileImage,
|
||||
faFile,
|
||||
faCircleInfo,
|
||||
faEllipsisVertical,
|
||||
faDownload,
|
||||
faTrash,
|
||||
faFileAlt,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ref } from "vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import DataTable from "../DataTable/DataTable.vue";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import DeleteDialog from "../Dialogs/DeleteDialog.vue";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
|
||||
const props = defineProps({
|
||||
documents: { type: Array, default: () => [] },
|
||||
viewUrlBuilder: { type: Function, default: null },
|
||||
// Optional: direct download URL builder; if absent we emit 'download'
|
||||
downloadUrlBuilder: { type: Function, default: null },
|
||||
// Optional: direct delete URL builder; if absent we emit 'delete'
|
||||
deleteUrlBuilder: { type: Function, default: null },
|
||||
edit: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
// Define columns for DataTable
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Naziv' },
|
||||
{ key: 'type', label: 'Vrsta' },
|
||||
{ key: 'size', label: 'Velikost', align: 'right' },
|
||||
{ key: 'created_at', label: 'Dodano' },
|
||||
{ key: 'source', label: 'Vir' },
|
||||
{ key: 'description', label: 'Opis', align: 'center' },
|
||||
];
|
||||
// Derive a human-friendly source for a document: Case or Contract reference
|
||||
const sourceLabel = (doc) => {
|
||||
// Server can include optional documentable meta; fall back to type
|
||||
if (doc.documentable_type?.toLowerCase?.().includes("contract")) {
|
||||
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : "Pogodba";
|
||||
}
|
||||
return "Primer";
|
||||
};
|
||||
|
||||
const emit = defineEmits(["view", "download", "delete", "edit"]);
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes == null) return "-";
|
||||
const thresh = 1024;
|
||||
if (Math.abs(bytes) < thresh) return bytes + " B";
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||
return bytes.toFixed(1) + " " + units[u];
|
||||
};
|
||||
|
||||
const extFrom = (doc) => {
|
||||
let ext = (doc?.extension || "").toLowerCase();
|
||||
if (!ext && doc?.original_name) {
|
||||
const parts = String(doc.original_name).toLowerCase().split(".");
|
||||
if (parts.length > 1) ext = parts.pop();
|
||||
}
|
||||
// derive from mime
|
||||
if (!ext && doc?.mime_type) {
|
||||
const mime = String(doc.mime_type).toLowerCase();
|
||||
if (mime.includes("pdf")) ext = "pdf";
|
||||
else if (mime.includes("word") || mime.includes("msword") || mime.includes("doc"))
|
||||
ext = "docx";
|
||||
else if (mime.includes("excel") || mime.includes("sheet")) ext = "xlsx";
|
||||
else if (mime.includes("csv")) ext = "csv";
|
||||
else if (mime.startsWith("image/")) ext = "img";
|
||||
else if (mime.includes("text")) ext = "txt";
|
||||
}
|
||||
return ext;
|
||||
};
|
||||
|
||||
const fileTypeInfo = (doc) => {
|
||||
const ext = extFrom(doc);
|
||||
const mime = (doc?.mime_type || "").toLowerCase();
|
||||
switch (ext) {
|
||||
case "pdf":
|
||||
return { icon: faFilePdf, color: "text-red-600", label: "PDF" };
|
||||
case "doc":
|
||||
case "docx":
|
||||
return {
|
||||
icon: faFileWord,
|
||||
color: "text-blue-600",
|
||||
label: (ext || "DOCX").toUpperCase(),
|
||||
};
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
return {
|
||||
icon: faFileExcel,
|
||||
color: "text-green-600",
|
||||
label: (ext || "XLSX").toUpperCase(),
|
||||
};
|
||||
case "csv":
|
||||
// treat CSV as spreadsheet-like
|
||||
return { icon: faFileExcel, color: "text-emerald-600", label: "CSV" };
|
||||
case "txt":
|
||||
return { icon: faFileLines, color: "text-slate-600", label: "TXT" };
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "img":
|
||||
return {
|
||||
icon: faFileImage,
|
||||
color: "text-fuchsia-600",
|
||||
label: ext === "img" ? "IMG" : (ext || "IMG").toUpperCase(),
|
||||
};
|
||||
default:
|
||||
if (mime.startsWith("image/"))
|
||||
return { icon: faFileImage, color: "text-fuchsia-600", label: "IMG" };
|
||||
return {
|
||||
icon: faFile,
|
||||
color: "text-gray-600",
|
||||
label: (ext || "FILE").toUpperCase(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const hasDesc = (doc) => {
|
||||
const d = doc?.description;
|
||||
return typeof d === "string" && d.trim().length > 0;
|
||||
};
|
||||
|
||||
const expandedDescKey = ref(null);
|
||||
const rowKey = (doc) => doc?.uuid ?? doc?.id ?? null;
|
||||
const toggleDesc = (doc) => {
|
||||
const key = rowKey(doc);
|
||||
if (!key) return;
|
||||
expandedDescKey.value = expandedDescKey.value === key ? null : key;
|
||||
};
|
||||
|
||||
// Track which documents have expanded descriptions
|
||||
const isExpanded = (doc) => {
|
||||
const key = rowKey(doc);
|
||||
return key ? expandedDescKey.value === key : false;
|
||||
};
|
||||
|
||||
const resolveDownloadUrl = (doc) => {
|
||||
if (typeof props.downloadUrlBuilder === "function")
|
||||
return props.downloadUrlBuilder(doc);
|
||||
// If no builder provided, parent can handle via emitted event
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleDownload = (doc) => {
|
||||
const url = resolveDownloadUrl(doc);
|
||||
if (url) {
|
||||
// Trigger a navigation to a direct-download endpoint; server should set Content-Disposition: attachment
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_self";
|
||||
a.rel = "noopener";
|
||||
// In many browsers, simply setting href is enough
|
||||
a.click();
|
||||
} else {
|
||||
emit("download", doc);
|
||||
}
|
||||
closeActions();
|
||||
};
|
||||
|
||||
// ---------------- Delete logic ----------------
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
const docToDelete = ref(null);
|
||||
|
||||
const resolveDeleteUrl = (doc) => {
|
||||
// 1. Explicit builder via prop takes precedence
|
||||
if (typeof props.deleteUrlBuilder === "function") {
|
||||
return props.deleteUrlBuilder(doc);
|
||||
}
|
||||
// 2. Attempt automatic route resolution (requires Ziggy's global `route` helper)
|
||||
try {
|
||||
const type = (doc?.documentable_type || "").toLowerCase();
|
||||
// Contract document
|
||||
if (type.includes("contract") && doc?.contract_uuid && doc?.uuid) {
|
||||
if (typeof route === "function") {
|
||||
return route("contract.document.delete", {
|
||||
contract: doc.contract_uuid,
|
||||
document: doc.uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Case document
|
||||
if (doc?.client_case_uuid && doc?.uuid) {
|
||||
if (typeof route === "function") {
|
||||
return route("clientCase.document.delete", {
|
||||
client_case: doc.client_case_uuid,
|
||||
document: doc.uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// swallow – fallback to emit path
|
||||
}
|
||||
// 3. Fallback: no URL, caller must handle emitted event
|
||||
return null;
|
||||
};
|
||||
|
||||
const requestDelete = async () => {
|
||||
if (!docToDelete.value) {
|
||||
return;
|
||||
}
|
||||
const url = resolveDeleteUrl(docToDelete.value);
|
||||
deleting.value = true;
|
||||
try {
|
||||
if (url) {
|
||||
await router.delete(url, { preserveScroll: true });
|
||||
} else {
|
||||
emit("delete", docToDelete.value);
|
||||
}
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
confirmDelete.value = false;
|
||||
docToDelete.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const askDelete = (doc) => {
|
||||
docToDelete.value = doc;
|
||||
confirmDelete.value = true;
|
||||
};
|
||||
|
||||
const closeDeleteDialog = () => {
|
||||
confirmDelete.value = false;
|
||||
docToDelete.value = null;
|
||||
};
|
||||
|
||||
function closeActions() {
|
||||
/* noop placeholder for symmetry; Dropdown auto-closes */
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="documents"
|
||||
:show-toolbar="false"
|
||||
:show-pagination="false"
|
||||
:striped="false"
|
||||
:hoverable="true"
|
||||
:show-actions="true"
|
||||
row-key="uuid"
|
||||
empty-text="Ni dokumentov."
|
||||
empty-icon="faFileAlt"
|
||||
>
|
||||
<!-- Name column -->
|
||||
<template #cell-name="{ row }">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-indigo-600 hover:underline"
|
||||
@click.stop="$emit('view', row)"
|
||||
>
|
||||
{{ row.name }}
|
||||
</button>
|
||||
<Badge v-if="row.is_public" variant="secondary" class="bg-green-100 text-green-700 hover:bg-green-200">Public</Badge>
|
||||
</div>
|
||||
<!-- Expanded description -->
|
||||
<div
|
||||
v-if="isExpanded(row)"
|
||||
class="mt-2 bg-gray-50 px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400 rounded"
|
||||
>
|
||||
{{ row.description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Type column -->
|
||||
<template #cell-type="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
:icon="fileTypeInfo(row).icon"
|
||||
:class="['h-5 w-5', fileTypeInfo(row).color]"
|
||||
/>
|
||||
<span class="text-gray-700">{{ fileTypeInfo(row).label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Size column -->
|
||||
<template #cell-size="{ row }">
|
||||
{{ formatSize(row.size) }}
|
||||
</template>
|
||||
|
||||
<!-- Created at column -->
|
||||
<template #cell-created_at="{ row }">
|
||||
{{ new Date(row.created_at).toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<!-- Source column -->
|
||||
<template #cell-source="{ row }">
|
||||
<Badge variant="secondary" class="bg-purple-100 text-purple-700 hover:bg-purple-200">{{ sourceLabel(row) }}</Badge>
|
||||
</template>
|
||||
|
||||
<!-- Description column -->
|
||||
<template #cell-description="{ row }">
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
:disabled="!hasDesc(row)"
|
||||
:title="hasDesc(row) ? 'Pokaži opis' : 'Ni opisa'"
|
||||
type="button"
|
||||
@click.stop="toggleDesc(row)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions column -->
|
||||
<template #actions="{ row }">
|
||||
<div @click.stop>
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none transition-colors"
|
||||
title="Možnosti"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="faEllipsisVertical"
|
||||
class="h-4 w-4 text-gray-700"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="py-1">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 transition-colors"
|
||||
@click="emit('edit', row)"
|
||||
v-if="edit"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
|
||||
<span>Uredi</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 transition-colors"
|
||||
@click="handleDownload(row)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
|
||||
<span>Prenos</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 transition-colors"
|
||||
@click="askDelete(row)"
|
||||
v-if="edit"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
|
||||
<span>Izbriši</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<DeleteDialog
|
||||
:show="confirmDelete"
|
||||
title="Potrditev brisanja"
|
||||
:message="`Ali res želite izbrisati dokument '${docToDelete?.name}'?`"
|
||||
:item-name="docToDelete?.name"
|
||||
confirm-text="Izbriši"
|
||||
:processing="deleting"
|
||||
@close="closeDeleteDialog"
|
||||
@confirm="requestDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user