Changes to post|put|patch|delete

This commit is contained in:
Simon Pocrnjič 2025-11-02 21:46:02 +01:00
parent 63e0958b66
commit fd9f26d82a
21 changed files with 786 additions and 465 deletions

View File

@ -121,7 +121,7 @@ public function store(Request $request)
});
}
return to_route('client.show', $client);
return back()->with('success', 'Client created.')->with('flash_method', 'POST');
}
public function storeContract(ClientCase $clientCase, StoreContractRequest $request)
@ -156,7 +156,7 @@ public function storeContract(ClientCase $clientCase, StoreContractRequest $requ
// Preserve segment filter if present
$segment = request('segment');
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
return back()->with('success', 'Contract created.')->with('flash_method', 'POST');
}
public function updateContract(ClientCase $clientCase, string $uuid, UpdateContractRequest $request)
@ -222,7 +222,7 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
// Preserve segment filter if present
$segment = request('segment');
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
return back()->with('success', 'Contract updated.')->with('flash_method', 'PUT');
}
/**
@ -322,7 +322,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
// Stay on the current page (desktop or phone) instead of forcing a redirect to the desktop route.
// Use 303 to align with Inertia's recommended POST/Redirect/GET behavior.
return back(303)->with('success', 'Successful created!');
return back(303)->with('success', 'Successful created!')->with('flash_method', 'POST');
} catch (QueryException $e) {
logger()->error('Database error occurred:', ['error' => $e->getMessage()]);
@ -357,7 +357,7 @@ public function deleteContract(ClientCase $clientCase, string $uuid, Request $re
// Preserve segment filter if present
$segment = request('segment');
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment])->with('flash_method', 'DELETE');
}
public function updateContractSegment(ClientCase $clientCase, string $uuid, Request $request)
@ -395,7 +395,7 @@ public function updateContractSegment(ClientCase $clientCase, string $uuid, Requ
}
});
return back()->with('success', 'Contract segment updated.');
return back()->with('success', 'Contract segment updated.')->with('flash_method', 'PATCH');
}
public function attachSegment(ClientCase $clientCase, Request $request)
@ -446,7 +446,7 @@ public function attachSegment(ClientCase $clientCase, Request $request)
}
});
return back()->with('success', 'Segment attached to case.');
return back()->with('success', 'Segment attached to case.')->with('flash_method', 'PATCH');
}
public function storeDocument(ClientCase $clientCase, Request $request)
@ -500,7 +500,7 @@ public function storeDocument(ClientCase $clientCase, Request $request)
\App\Jobs\GenerateDocumentPreview::dispatch($doc->id);
}
return back()->with('success', 'Document uploaded.');
return back()->with('success', 'Document uploaded.')->with('flash_method', 'POST');
}
public function updateDocument(ClientCase $clientCase, Document $document, Request $request)
@ -583,7 +583,7 @@ public function updateDocument(ClientCase $clientCase, Document $document, Reque
$document->save();
// Refresh documents list on page
return back()->with('success', __('Document updated.'));
return back()->with('success', 'Document updated.')->with('flash_method', 'PUT');
}
public function viewDocument(ClientCase $clientCase, Document $document, Request $request)
@ -960,9 +960,7 @@ public function deleteDocument(ClientCase $clientCase, Document $document, Reque
$document->delete(); // soft delete
return $request->wantsJson()
? response()->json(['status' => 'ok'])
: back()->with('success', 'Document deleted.');
return back()->with('success', 'Document deleted.')->with('flash_method', 'DELETE');
}
/**
@ -979,9 +977,7 @@ public function deleteContractDocument(Contract $contract, Document $document, R
$document->delete();
return $request->wantsJson()
? response()->json(['status' => 'ok'])
: back()->with('success', 'Document deleted.');
return back()->with('success', 'Document deleted.')->with('flash_method', 'DELETE');
}
/**
@ -1162,7 +1158,7 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
$message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived');
return back()->with('success', $message);
return back()->with('success', $message)->with('flash_method', 'PATCH');
}
/**

View File

@ -197,14 +197,14 @@ public function store(Request $request)
// \App\Models\Person\PersonAddress::create($address);
return to_route('client');
return back()->with('success', 'Client created')->with('flash_method', 'POST');
}
public function update(Client $client, Request $request)
{
return to_route('client.show', $client);
return back()->with('success', 'Client updated')->with('flash_method', 'PUT');
}
/**

View File

@ -47,7 +47,7 @@ public function store(Request $request)
});
}
return to_route('clientCase.show', $clientCase);
return back()->with('success', 'Contract created')->with('flash_method', 'POST');
}
public function update(Contract $contract, Request $request){
@ -56,6 +56,6 @@ public function update(Contract $contract, Request $request){
'type_id' => $request->input('type_id')
]);
return back()->with('success', 'Contract updated')->with('flash_method', 'PUT');
}
}

View File

@ -26,18 +26,10 @@ public function update(Person $person, Request $request)
$person->update($attributes);
if ($request->header('X-Inertia')) {
return back()->with('success', 'Person updated');
}
return back()->with('success', 'Person updated')->with('flash_method', 'PUT');
return response()->json([
'person' => [
'full_name' => $person->full_name,
'tax_number' => $person->tax_number,
'social_security_number' => $person->social_security_number,
'description' => $person->description,
],
]);
}
public function createAddress(Person $person, Request $request)
@ -60,13 +52,8 @@ public function createAddress(Person $person, Request $request)
], $attributes);
// Support Inertia form submissions (redirect back) and JSON (for API/axios)
if ($request->header('X-Inertia')) {
return back()->with('success', 'Address created');
}
return back()->with('success', 'Address created')->with('flash_method', 'POST');
return response()->json([
'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address->id),
]);
}
public function updateAddress(Person $person, int $address_id, Request $request)
@ -84,13 +71,8 @@ public function updateAddress(Person $person, int $address_id, Request $request)
$address->update($attributes);
if ($request->header('X-Inertia')) {
return back()->with('success', 'Address updated');
}
return back()->with('success', 'Address updated')->with('flash_method', 'PUT');
return response()->json([
'address' => $address,
]);
}
public function deleteAddress(Person $person, int $address_id, Request $request)
@ -98,11 +80,8 @@ public function deleteAddress(Person $person, int $address_id, Request $request)
$address = $person->addresses()->findOrFail($address_id);
$address->delete(); // soft delete
if ($request->header('X-Inertia')) {
return back()->with('success', 'Address deleted');
}
return response()->json(['status' => 'ok']);
return back()->with('success', 'Address deleted')->with('flash_method', 'DELETE');
}
public function createPhone(Person $person, Request $request)
@ -122,7 +101,7 @@ public function createPhone(Person $person, Request $request)
'country_code' => $attributes['country_code'] ?? null,
], $attributes);
return back()->with('success', 'Phone added successfully');
return back()->with('success', 'Phone added successfully')->with('flash_method', 'POST');
}
public function updatePhone(Person $person, int $phone_id, Request $request)
@ -140,7 +119,7 @@ public function updatePhone(Person $person, int $phone_id, Request $request)
$phone->update($attributes);
return back()->with('success', 'Phone updated successfully');
return back()->with('success', 'Phone updated successfully')->with('flash_method', 'PUT');
}
public function deletePhone(Person $person, int $phone_id, Request $request)
@ -148,7 +127,7 @@ public function deletePhone(Person $person, int $phone_id, Request $request)
$phone = $person->phones()->findOrFail($phone_id);
$phone->delete(); // soft delete
return back()->with('success', 'Phone deleted');
return back()->with('success', 'Phone deleted')->with('flash_method', 'DELETE');
}
public function createEmail(Person $person, Request $request)
@ -170,7 +149,7 @@ public function createEmail(Person $person, Request $request)
'value' => $attributes['value'],
], $attributes);
return back()->with('success', 'Email added successfully');
return back()->with('success', 'Email added successfully')->with('flash_method', 'POST');
}
public function updateEmail(Person $person, int $email_id, Request $request)
@ -191,7 +170,7 @@ public function updateEmail(Person $person, int $email_id, Request $request)
$email->update($attributes);
return back()->with('success', 'Email updated successfully');
return back()->with('success', 'Email updated successfully')->with('flash_method', 'PUT');
}
public function deleteEmail(Person $person, int $email_id, Request $request)
@ -203,7 +182,7 @@ public function deleteEmail(Person $person, int $email_id, Request $request)
return back()->with('success', 'Email deleted');
}
return response()->json(['status' => 'ok']);
return back()->with('success', 'Email deleted')->with('flash_method', 'DELETE');
}
// TRR (bank account) CRUD
@ -225,13 +204,10 @@ public function createTrr(Person $person, Request $request)
// Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided
$trr = $person->bankAccounts()->create($attributes);
if ($request->header('X-Inertia')) {
return back()->with('success', 'TRR added successfully');
}
return response()->json([
'trr' => BankAccount::findOrFail($trr->id),
]);
return back()->with('success', 'TRR added successfully')->with('flash_method', 'POST');
}
public function updateTrr(Person $person, int $trr_id, Request $request)
@ -253,13 +229,8 @@ public function updateTrr(Person $person, int $trr_id, Request $request)
$trr = $person->bankAccounts()->findOrFail($trr_id);
$trr->update($attributes);
if ($request->header('X-Inertia')) {
return back()->with('success', 'TRR updated successfully');
}
return back()->with('success', 'TRR updated successfully')->with('flash_method', 'PUT');
return response()->json([
'trr' => $trr,
]);
}
public function deleteTrr(Person $person, int $trr_id, Request $request)
@ -267,10 +238,8 @@ public function deleteTrr(Person $person, int $trr_id, Request $request)
$trr = $person->bankAccounts()->findOrFail($trr_id);
$trr->delete();
if ($request->header('X-Inertia')) {
return back()->with('success', 'TRR deleted');
}
return response()->json(['status' => 'ok']);
return back()->with('success', 'TRR deleted')->with('flash_method', 'DELETE');
}
}

View File

@ -57,6 +57,7 @@ public function share(Request $request): array
'error' => fn () => $request->session()->get('error'),
'warning' => fn () => $request->session()->get('warning'),
'info' => fn () => $request->session()->get('info'),
'method' => fn () => $request->session()->get('flash_method'), // HTTP method for toast styling
],
'notifications' => function () use ($request) {
try {

View File

@ -1,97 +0,0 @@
<script setup>
import UpdateDialog from '@/Components/Dialogs/UpdateDialog.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import { useForm } from '@inertiajs/vue3'
import { computed, watch } from 'vue'
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 form = useForm({
name: '',
description: '',
is_public: false,
contract_uuid: null,
})
watch(
() => props.document,
(d) => {
if (!d) return
form.name = d.name || d.original_name || ''
form.description = d.description || ''
form.is_public = !!d.is_public
// Pre-fill contract selection if this doc belongs to a contract
const isContract = (d?.documentable_type || '').toLowerCase().includes('contract')
form.contract_uuid = isContract ? d.contract_uuid || null : null
},
{ immediate: true }
)
const submit = () => {
if (!props.document) return
form.patch(
route('clientCase.document.update', {
client_case: props.client_case_uuid,
document: props.document.uuid,
}),
{
preserveScroll: true,
onSuccess: () => {
emit('saved')
emit('close')
},
}
)
}
const onConfirm = () => {
submit()
}
const contractOptions = computed(() => {
return props.contracts || []
})
</script>
<template>
<UpdateDialog
:show="show"
title="Uredi dokument"
:processing="form.processing"
@close="$emit('close')"
@confirm="onConfirm"
>
<div class="space-y-4">
<div>
<InputLabel for="docName" value="Ime" />
<TextInput id="docName" v-model="form.name" class="mt-1 block w-full" />
<div v-if="form.errors.name" class="text-sm text-red-600 mt-1">{{ form.errors.name }}</div>
</div>
<div>
<InputLabel for="docDesc" value="Opis" />
<TextInput id="docDesc" v-model="form.description" class="mt-1 block w-full" />
<div v-if="form.errors.description" class="text-sm text-red-600 mt-1">{{ form.errors.description }}</div>
</div>
<div class="flex items-center gap-2">
<input id="docPublic" type="checkbox" v-model="form.is_public" />
<InputLabel for="docPublic" value="Javno" />
</div>
<div>
<InputLabel for="docContract" value="Pogodba" />
<select id="docContract" v-model="form.contract_uuid" class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option :value="null"> Brez (dok. pri primeru)</option>
<option v-for="c in contractOptions" :key="c.uuid || c.id" :value="c.uuid">{{ c.reference || c.uuid }}</option>
</select>
<div v-if="form.errors.contract_uuid" class="text-sm text-red-600 mt-1">{{ form.errors.contract_uuid }}</div>
</div>
</div>
</UpdateDialog>
</template>

View File

@ -1,130 +0,0 @@
<script setup>
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
import ActionMessage from '@/Components/ActionMessage.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import { useForm } from '@inertiajs/vue3'
import { ref, watch } from 'vue'
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 form = useForm({
name: '',
description: '',
file: null,
is_public: true,
contract_uuid: null,
})
const localError = ref('')
watch(() => props.show, (v) => {
if (!v) return
localError.value = ''
})
const onFileChange = (e) => {
localError.value = ''
const f = e.target.files?.[0]
if (!f) { form.file = null; return }
const ext = (f.name.split('.').pop() || '').toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) {
localError.value = 'Unsupported file type. Allowed: ' + ALLOWED_EXTS.join(', ')
e.target.value = ''
form.file = null
return
}
if (f.size > MAX_SIZE) {
localError.value = 'File is too large. Maximum size is 25MB.'
e.target.value = ''
form.file = null
return
}
form.file = f
if (!form.name) {
form.name = f.name.replace(/\.[^.]+$/, '')
}
}
const submit = () => {
localError.value = ''
if (!form.file) {
localError.value = 'Please choose a file.'
return
}
const ext = (form.file.name.split('.').pop() || '').toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) {
localError.value = 'Unsupported file type. Allowed: ' + ALLOWED_EXTS.join(', ')
return
}
if (form.file.size > MAX_SIZE) {
localError.value = 'File is too large. Maximum size is 25MB.'
return
}
form.post(props.postUrl, {
forceFormData: true,
onSuccess: () => {
emit('uploaded')
close()
form.reset()
},
})
}
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.processing"
:disabled="!form.file"
@close="close"
@confirm="onConfirm"
>
<div class="space-y-4">
<div v-if="props.contracts && props.contracts.length" class="grid grid-cols-1 gap-2">
<InputLabel for="doc_attach" value="Pripiši k" />
<select id="doc_attach" v-model="form.contract_uuid" class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option :value="null">Primer</option>
<option v-for="c in props.contracts" :key="c.uuid" :value="c.uuid">Pogodba: {{ c.reference }}</option>
</select>
</div>
<div>
<InputLabel for="doc_name" value="Name" />
<TextInput id="doc_name" class="mt-1 block w-full" v-model="form.name" />
</div>
<div>
<InputLabel for="doc_desc" value="Description" />
<textarea id="doc_desc" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" rows="3" v-model="form.description"></textarea>
</div>
<div>
<InputLabel for="doc_file" value="File (max 25MB)" />
<input id="doc_file" type="file" class="mt-1 block w-full" @change="onFileChange" accept=".doc,.docx,.pdf,.txt,.csv,.xls,.xlsx,.jpeg,.jpg,.png" />
<div v-if="localError" class="text-sm text-red-600 mt-1">{{ localError }}</div>
</div>
<label class="inline-flex items-center gap-2 text-sm">
<input type="checkbox" v-model="form.is_public" class="rounded" />
Public
</label>
</div>
</CreateDialog>
</template>

View File

@ -1,26 +0,0 @@
<script setup>
import DialogModal from '@/Components/DialogModal.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
const props = defineProps({
show: { type: Boolean, default: false },
src: { type: String, default: '' },
title: { type: String, default: 'Document' }
})
const emit = defineEmits(['close'])
</script>
<template>
<DialogModal :show="props.show" @close="$emit('close')" maxWidth="4xl">
<template #title>{{ props.title }}</template>
<template #content>
<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">No document to display.</div>
</div>
</template>
<template #footer>
<SecondaryButton type="button" @click="$emit('close')">Close</SecondaryButton>
</template>
</DialogModal>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -13,11 +13,11 @@ import {
faTrash,
faFileAlt,
} from "@fortawesome/free-solid-svg-icons";
import { ref, computed } from "vue";
import { ref } from "vue";
import { router } from "@inertiajs/vue3";
import DataTable from "./DataTable/DataTable.vue";
import DataTable from "../DataTable/DataTable.vue";
import Dropdown from "@/Components/Dropdown.vue";
import DeleteDialog from "./Dialogs/DeleteDialog.vue";
import DeleteDialog from "../Dialogs/DeleteDialog.vue";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
@ -253,7 +253,7 @@ function closeActions() {
:hoverable="true"
:show-actions="true"
row-key="uuid"
empty-text="No documents."
empty-text="Ni dokumentov."
empty-icon="faFileAlt"
>
<!-- Name column -->

View File

@ -1,6 +1,5 @@
<script setup>
import { ref, computed } from "vue";
import axios from "axios";
import { router } from "@inertiajs/vue3";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import PersonUpdateForm from "./PersonUpdateForm.vue";
@ -151,36 +150,49 @@ const closeConfirm = () => {
const onConfirmDelete = async () => {
const { type, id } = confirm.value;
try {
if (type === "email") {
await axios.delete(
route("person.email.delete", { person: props.person, email_id: id })
);
const list = props.person.emails || [];
const idx = list.findIndex((e) => e.id === id);
if (idx !== -1) list.splice(idx, 1);
router.delete(
route("person.email.delete", { person: props.person, email_id: id }),
{
preserveScroll: true,
onSuccess: () => {
closeConfirm();
},
onError: (errors) => {
console.error("Delete failed", errors);
closeConfirm();
},
}
);
} else if (type === "trr") {
await axios.delete(
route("person.trr.delete", { person: props.person, trr_id: id })
);
let list =
props.person.trrs ||
props.person.bank_accounts ||
props.person.accounts ||
props.person.bankAccounts ||
[];
const idx = list.findIndex((a) => a.id === id);
if (idx !== -1) list.splice(idx, 1);
router.delete(
route("person.trr.delete", { person: props.person, trr_id: id }),
{
preserveScroll: true,
onSuccess: () => {
closeConfirm();
},
onError: (errors) => {
console.error("Delete failed", errors);
closeConfirm();
},
}
);
} else if (type === "address") {
await axios.delete(
route("person.address.delete", { person: props.person, address_id: id })
);
const list = props.person.addresses || [];
const idx = list.findIndex((a) => a.id === id);
if (idx !== -1) list.splice(idx, 1);
router.delete(
route("person.address.delete", { person: props.person, address_id: id }),
{
preserveScroll: true,
onSuccess: () => {
closeConfirm();
},
onError: (errors) => {
console.error("Delete failed", errors);
closeConfirm();
},
}
);
} else if (type === "phone") {
router.delete(
route("person.phone.delete", { person: props.person, phone_id: id }),
@ -196,10 +208,6 @@ const onConfirmDelete = async () => {
}
);
}
} catch (e) {
console.error("Delete failed", e?.response || e);
closeConfirm();
}
};
// SMS handlers

View File

@ -4,7 +4,7 @@ import SectionTitle from '@/Components/SectionTitle.vue';
import { useForm, Field as FormField } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import axios from 'axios';
import { router } from '@inertiajs/vue3';
import { ref } from 'vue';
import {
FormControl,
@ -64,23 +64,17 @@ const updatePerson = async () => {
processingUpdate.value = true;
const { values } = form;
try {
const response = await axios({
method: 'put',
url: route('person.update', props.person),
data: values
});
props.person.full_name = response.data.person.full_name;
props.person.tax_number = response.data.person.tax_number;
props.person.social_security_number = response.data.person.social_security_number;
props.person.description = response.data.person.description;
router.put(
route('person.update', props.person),
values,
{
preserveScroll: true,
onSuccess: () => {
processingUpdate.value = false;
close();
} catch (reason) {
const errors = reason.response?.data?.errors || {};
// Map axios errors to VeeValidate field errors
},
onError: (errors) => {
// Map Inertia errors to VeeValidate field errors
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
@ -88,7 +82,12 @@ const updatePerson = async () => {
form.setFieldError(field, errorMessages[0]);
});
processingUpdate.value = false;
},
onFinish: () => {
processingUpdate.value = false;
},
}
);
}
const onSubmit = form.handleSubmit(() => {

View File

@ -3,7 +3,7 @@ import { ref, watch } from 'vue';
import { useForm, Field as FormField } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import axios from 'axios';
import { router } from '@inertiajs/vue3';
import CreateDialog from '../Dialogs/CreateDialog.vue';
import UpdateDialog from '../Dialogs/UpdateDialog.vue';
import SectionTitle from '../SectionTitle.vue';
@ -95,26 +95,32 @@ const create = async () => {
errors.value = {};
const { values } = form;
try {
const { data } = await axios.post(route('person.trr.create', props.person), values);
if (!Array.isArray(props.person.trrs)) props.person.trrs = (props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || []);
(props.person.trrs).push(data.trr);
router.post(
route('person.trr.create', props.person),
values,
{
preserveScroll: true,
onSuccess: () => {
processing.value = false;
close();
resetForm();
} catch (e) {
errors.value = e?.response?.data?.errors || {};
// Map axios errors to VeeValidate field errors
if (errors.value) {
},
onError: (inertiaErrors) => {
errors.value = inertiaErrors || {};
// Map Inertia errors to VeeValidate field errors
Object.keys(errors.value).forEach((field) => {
const errorMessages = Array.isArray(errors.value[field])
? errors.value[field]
: [errors.value[field]];
form.setFieldError(field, errorMessages[0]);
});
}
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
};
const update = async () => {
@ -122,27 +128,32 @@ const update = async () => {
errors.value = {};
const { values } = form;
try {
const { data } = await axios.put(route('person.trr.update', { person: props.person, trr_id: props.id }), values);
let list = props.person.trrs || props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || [];
const idx = list.findIndex(a => a.id === data.trr.id);
if (idx !== -1) list[idx] = data.trr;
router.put(
route('person.trr.update', { person: props.person, trr_id: props.id }),
values,
{
preserveScroll: true,
onSuccess: () => {
processing.value = false;
close();
resetForm();
} catch (e) {
errors.value = e?.response?.data?.errors || {};
// Map axios errors to VeeValidate field errors
if (errors.value) {
},
onError: (inertiaErrors) => {
errors.value = inertiaErrors || {};
// Map Inertia errors to VeeValidate field errors
Object.keys(errors.value).forEach((field) => {
const errorMessages = Array.isArray(errors.value[field])
? errors.value[field]
: [errors.value[field]];
form.setFieldError(field, errorMessages[0]);
});
}
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
};
watch(

View File

@ -1,11 +1,63 @@
<script setup>
import { watch, onMounted, onUnmounted } from 'vue';
import { watch, onMounted, onUnmounted, h } from 'vue';
import { usePage } from '@inertiajs/vue3';
import { Toaster } from '@/Components/ui/sonner';
import { toast } from 'vue-sonner';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faCircleCheck,
faPen,
faPencil,
faTrash
} from '@fortawesome/free-solid-svg-icons';
library.add(faCircleCheck, faPen, faPencil, faTrash);
const page = usePage();
// Helper function to get toast config based on HTTP method
const getToastConfig = (method, message) => {
const methodUpper = method?.toUpperCase() || '';
switch (methodUpper) {
case 'POST':
return {
type: 'success',
message: message,
icon: () => h(FontAwesomeIcon, { icon: faCircleCheck, class: 'text-green-600' }),
description: 'Ustvarjeno',
className: 'border-green-500 bg-green-50',
};
case 'PUT':
return {
type: 'info',
message: message,
icon: () => h(FontAwesomeIcon, { icon: faPen, class: 'text-blue-600' }),
description: 'Posodobljeno',
className: 'border-blue-500 bg-blue-50',
};
case 'PATCH':
return {
type: 'info',
message: message,
icon: () => h(FontAwesomeIcon, { icon: faPencil, class: 'text-indigo-600' }),
description: 'Spremenjeno',
className: 'border-indigo-500 bg-indigo-50',
};
case 'DELETE':
return {
type: 'error',
message: message,
icon: () => h(FontAwesomeIcon, { icon: faTrash, class: 'text-red-600' }),
description: 'Izbrisano',
className: 'border-red-500 bg-red-50',
};
default:
return null;
}
};
// Watch for flash messages from Inertia
watch(
() => page.props.flash,
@ -13,21 +65,80 @@ watch(
if (!flash) return;
const flashTypes = ['success', 'error', 'warning', 'info'];
const method = flash.method?.toUpperCase();
for (const type of flashTypes) {
if (flash[type]) {
switch (type) {
const message = flash[type];
// If method is provided, use custom styling
if (method) {
const config = getToastConfig(method, message);
if (config) {
// Use toast.custom or the appropriate type with options
switch (config.type) {
case 'success':
toast.success(flash[type]);
toast.success(message, {
description: config.description,
icon: config.icon,
class: config.className,
});
break;
case 'error':
toast.error(flash[type]);
break;
case 'warning':
toast.warning(flash[type]);
toast.error(message, {
description: config.description,
icon: config.icon,
class: config.className,
});
break;
case 'info':
toast.info(flash[type]);
toast.info(message, {
description: config.description,
icon: config.icon,
class: config.className,
});
break;
case 'warning':
toast.warning(message, {
description: config.description,
icon: config.icon,
class: config.className,
});
break;
}
} else {
// Fallback to default behavior
switch (type) {
case 'success':
toast.success(message);
break;
case 'error':
toast.error(message);
break;
case 'warning':
toast.warning(message);
break;
case 'info':
toast.info(message);
break;
}
}
} else {
// Default behavior when no method is specified
switch (type) {
case 'success':
toast.success(message);
break;
case 'error':
toast.error(message);
break;
case 'warning':
toast.warning(message);
break;
case 'info':
toast.info(message);
break;
}
}
}
}

View File

@ -0,0 +1,46 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { SwitchRoot, SwitchThumb, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
defaultValue: { type: Boolean, required: false },
modelValue: { type: [Boolean, null], required: false },
disabled: { type: Boolean, required: false },
id: { type: String, required: false },
value: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
name: { type: String, required: false },
required: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<SwitchRoot
v-bind="forwarded"
:class="
cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
props.class,
)
"
>
<SwitchThumb
:class="
cn(
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
)
"
>
<slot name="thumb" />
</SwitchThumb>
</SwitchRoot>
</template>

View File

@ -0,0 +1 @@
export { default as Switch } from "./Switch.vue";

View File

@ -16,14 +16,14 @@ const props = defineProps({
edit: Boolean,
});
const columns = computed(() => [
{ key: "decision_dot", label: "", class: "w-[6%]" },
const columns = [
{ key: "decision_dot", label: " ", class: "w-[6%]" },
{ key: "contract", label: "Pogodba", class: "w-[14%]" },
{ key: "decision", label: "Odločitev", class: "w-[26%]" },
{ key: "note", label: "Opomba", class: "w-[14%]" },
{ key: "promise", label: "Obljuba", class: "w-[20%]" },
{ key: "user", label: "Dodal", class: "w-[10%]" },
]);
];
const rows = computed(() => props.activities?.data || []);

View File

@ -8,10 +8,10 @@ import ContractDrawer from "./Partials/ContractDrawer.vue";
import ContractTable from "./Partials/ContractTable.vue";
import ActivityDrawer from "./Partials/ActivityDrawer.vue";
import ActivityTable from "./Partials/ActivityTable.vue";
import DocumentsTable from "@/Components/DocumentsTable.vue";
import DocumentEditDialog from "@/Components/DocumentEditDialog.vue";
import DocumentUploadDialog from "@/Components/DocumentUploadDialog.vue";
import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue";
import DocumentsTable from "@/Components/DocumentsTable/DocumentsTable.vue";
import DocumentEditDialog from "@/Components/DocumentsTable/DocumentEditDialog.vue";
import DocumentUploadDialog from "@/Components/DocumentsTable/DocumentUploadDialog.vue";
import DocumentViewerDialog from "@/Components/DocumentsTable/DocumentViewerDialog.vue";
import { classifyDocument } from "@/Services/documents";
import { router, useForm, usePage } from "@inertiajs/vue3";
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";

View File

@ -4,7 +4,7 @@ import SectionTitle from "@/Components/SectionTitle.vue";
import PersonDetailPhone from "@/Components/PersonDetailPhone.vue";
// Removed table-based component for phone; render a list instead
// import DocumentsTable from '@/Components/DocumentsTable.vue';
import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue";
import DocumentViewerDialog from "@/Components/DocumentsTable/DocumentViewerDialog.vue";
import { classifyDocument } from "@/Services/documents";
import { reactive, ref, computed, watch, onMounted } from "vue";
import DialogModal from "@/Components/DialogModal.vue";