changes UI

This commit is contained in:
Simon Pocrnjič
2025-11-04 18:53:23 +01:00
parent fd9f26d82a
commit b7fa2d261b
15 changed files with 911 additions and 730 deletions
@@ -14,7 +14,7 @@ import {
FormMessage,
} from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import { Checkbox } from "@/Components/ui/checkbox";
import { Switch } from "@/Components/ui/switch";
const props = defineProps({
show: { type: Boolean, default: false },
@@ -216,9 +216,9 @@ const onConfirm = () => {
>
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:checked="value"
@update:checked="handleChange"
<Switch
:model-value="value"
@update:model-value="handleChange"
/>
</FormControl>
<div class="space-y-1 leading-none">
@@ -1,5 +1,5 @@
<script setup>
import { PlusIcon, DottedMenu, EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import { DottedMenu, EditIcon, TrashBinIcon, PlusIcon } from "@/Utilities/Icons";
import Dropdown from "../Dropdown.vue";
const props = defineProps({
@@ -15,17 +15,7 @@ const handleDelete = (id, label) => emit('delete', id, label);
</script>
<template>
<div class="flex justify-end mb-3" v-if="edit">
<button
@click="handleAdd"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all"
title="Dodaj naslov"
>
<PlusIcon size="sm" />
<span>Dodaj naslov</span>
</button>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div
class="rounded-lg p-4 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
v-for="address in person.addresses"
@@ -80,6 +70,14 @@ const handleDelete = (id, label) => emit('delete', id, label);
}}
</p>
</div>
<button
v-if="edit"
@click="handleAdd"
class="rounded-lg p-4 bg-white border-2 border-dashed border-gray-300 hover:border-gray-400 hover:bg-gray-50 transition-all flex items-center justify-center min-h-[120px]"
title="Dodaj naslov"
>
<PlusIcon class="h-8 w-8 text-gray-400" />
</button>
</div>
</template>
@@ -1,5 +1,5 @@
<script setup>
import { PlusIcon, DottedMenu, EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import { DottedMenu, EditIcon, TrashBinIcon, PlusIcon } from "@/Utilities/Icons";
import Dropdown from "../Dropdown.vue";
const props = defineProps({
@@ -17,17 +17,7 @@ const handleDelete = (id, label) => emit('delete', id, label);
</script>
<template>
<div class="flex justify-end mb-3" v-if="edit">
<button
@click="handleAdd"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all"
title="Dodaj email"
>
<PlusIcon size="sm" />
<span>Dodaj email</span>
</button>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getEmails(person).length">
<div
class="rounded-lg p-4 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
@@ -89,7 +79,15 @@ const handleDelete = (id, label) => emit('delete', id, label);
</p>
</div>
</template>
<p v-else class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
<button
v-if="edit"
@click="handleAdd"
class="rounded-lg p-4 bg-white border-2 border-dashed border-gray-300 hover:border-gray-400 hover:bg-gray-50 transition-all flex items-center justify-center min-h-[120px]"
title="Dodaj email"
>
<PlusIcon class="h-8 w-8 text-gray-400" />
</button>
<p v-else-if="!edit && !getEmails(person).length" class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
Ni e-poštnih naslovov.
</p>
</div>
@@ -2,6 +2,8 @@
import { ref, computed } from "vue";
import { router } from "@inertiajs/vue3";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import { Button } from "@/Components/ui/button";
import { PlusIcon } from "@/Utilities/Icons";
import PersonUpdateForm from "./PersonUpdateForm.vue";
import AddressCreateForm from "./AddressCreateForm.vue";
import AddressUpdateForm from "./AddressUpdateForm.vue";
@@ -20,7 +22,6 @@ import PersonInfoPhonesTab from "./PersonInfoPhonesTab.vue";
import PersonInfoEmailsTab from "./PersonInfoEmailsTab.vue";
import PersonInfoTrrTab from "./PersonInfoTrrTab.vue";
import PersonInfoSmsDialog from "./PersonInfoSmsDialog.vue";
import Separator from "../ui/separator/Separator.vue";
const props = defineProps({
person: Object,
@@ -95,8 +96,12 @@ const openDrawerAddAddress = (edit = false, id = 0) => {
const closeDrawerAddAddress = () => {
drawerAddAddress.value = false;
const wasEdit = editAddress.value;
editAddress.value = false;
editAddressId.value = 0;
if (!wasEdit) {
switchToTab('addresses');
}
};
// Phone handlers
@@ -111,8 +116,12 @@ const operDrawerAddPhone = openDrawerAddPhone;
const closeDrawerAddPhone = () => {
drawerAddPhone.value = false;
const wasEdit = editPhone.value;
editPhone.value = false;
editPhoneId.value = 0;
if (!wasEdit) {
switchToTab('phones');
}
};
// Email handlers
@@ -122,6 +131,16 @@ const openDrawerAddEmail = (edit = false, id = 0) => {
editEmailId.value = id;
};
const closeDrawerAddEmail = () => {
drawerAddEmail.value = false;
const wasEdit = editEmail.value;
editEmail.value = false;
editEmailId.value = 0;
if (!wasEdit) {
switchToTab('emails');
}
};
// TRR handlers
const openDrawerAddTrr = (edit = false, id = 0) => {
drawerAddTrr.value = true;
@@ -129,6 +148,16 @@ const openDrawerAddTrr = (edit = false, id = 0) => {
editTrrId.value = id;
};
const closeDrawerAddTrr = () => {
drawerAddTrr.value = false;
const wasEdit = editTrr.value;
editTrr.value = false;
editTrrId.value = 0;
if (!wasEdit) {
switchToTab('trr');
}
};
// Confirm dialog handlers
const openConfirm = (type, id, label = "") => {
confirm.value = {
@@ -258,10 +287,16 @@ const trrsCount = computed(() => {
const formatBadgeCount = (count) => {
return count >= 999 ? '999+' : String(count);
};
// Tab switching
const activeTab = ref('person');
const switchToTab = (tab) => {
activeTab.value = tab;
};
</script>
<template>
<Tabs default-value="person" class="mt-2">
<Tabs v-model="activeTab" class="mt-2">
<TabsList class="flex w-full bg-white gap-2 p-1">
<TabsTrigger value="person" class="border border-gray-200 data-[state=active]:bg-primary-50 data-[state=active]:text-primary-700 flex-1 py-2">Oseba</TabsTrigger>
<TabsTrigger value="addresses" class="border border-gray-200 data-[state=active]:bg-primary-50 data-[state=active]:text-primary-700 flex-1 py-2 px-3">
@@ -403,14 +438,14 @@ const formatBadgeCount = (count) => {
<!-- Email Dialogs -->
<EmailCreateForm
:show="drawerAddEmail && !editEmail"
@close="drawerAddEmail = false"
@close="closeDrawerAddEmail"
:person="person"
:types="types.email_types ?? []"
:is-client-context="!!person?.client"
/>
<EmailUpdateForm
:show="drawerAddEmail && editEmail"
@close="drawerAddEmail = false"
@close="closeDrawerAddEmail"
:person="person"
:types="types.email_types ?? []"
:id="editEmailId"
@@ -420,7 +455,7 @@ const formatBadgeCount = (count) => {
<!-- TRR Dialogs -->
<TrrCreateForm
:show="drawerAddTrr && !editTrr"
@close="drawerAddTrr = false"
@close="closeDrawerAddTrr"
:person="person"
:types="types.trr_types ?? []"
:banks="types.banks ?? []"
@@ -428,7 +463,7 @@ const formatBadgeCount = (count) => {
/>
<TrrUpdateForm
:show="drawerAddTrr && editTrr"
@close="drawerAddTrr = false"
@close="closeDrawerAddTrr"
:person="person"
:types="types.trr_types ?? []"
:banks="types.banks ?? []"
@@ -1,5 +1,5 @@
<script setup>
import { PlusIcon, DottedMenu, EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import { DottedMenu, EditIcon, TrashBinIcon, PlusIcon } from "@/Utilities/Icons";
import Dropdown from "../Dropdown.vue";
const props = defineProps({
@@ -19,18 +19,7 @@ const handleSms = (phone) => emit('sms', phone);
</script>
<template>
<div class="flex justify-end mb-3" v-if="edit">
<button
type="button"
@click="handleAdd"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all"
title="Dodaj telefon"
>
<PlusIcon size="sm" />
<span>Dodaj telefon</span>
</button>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getPhones(person).length">
<div
class="rounded-lg p-4 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
@@ -90,7 +79,15 @@ const handleSms = (phone) => emit('sms', phone);
<p class="text-sm font-medium text-gray-900 leading-relaxed">{{ phone.nu }}</p>
</div>
</template>
<p v-else class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
<button
v-if="edit"
@click="handleAdd"
class="rounded-lg p-4 bg-white border-2 border-dashed border-gray-300 hover:border-gray-400 hover:bg-gray-50 transition-all flex items-center justify-center min-h-[120px]"
title="Dodaj telefon"
>
<PlusIcon class="h-8 w-8 text-gray-400" />
</button>
<p v-else-if="!edit && !getPhones(person).length" class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
Ni telefonov.
</p>
</div>
@@ -1,7 +1,34 @@
<script setup>
import { ref, watch, computed } from "vue";
import DialogModal from "@/Components/DialogModal.vue";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import { router, usePage } from "@inertiajs/vue3";
import { useForm, Field as FormField } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import {
FormControl,
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 { Switch } from "@/Components/ui/switch";
import { Button } from "@/Components/ui/button";
const props = defineProps({
show: { type: Boolean, default: false },
@@ -12,15 +39,11 @@ const props = defineProps({
smsTemplates: { type: Array, default: () => [] },
});
const emit = defineEmits(['close']);
const emit = defineEmits(["close"]);
// SMS dialog state
const smsMessage = ref("");
const smsSending = ref(false);
// Page-level props fallback for SMS metadata
const page = usePage();
const pageProps = computed(() => page?.props ?? {});
const pageSmsProfiles = computed(() => {
const fromProps =
Array.isArray(props.smsProfiles) && props.smsProfiles.length
@@ -28,11 +51,13 @@ const pageSmsProfiles = computed(() => {
: null;
return fromProps ?? pageProps.value?.sms_profiles ?? [];
});
const pageSmsSenders = computed(() => {
const fromProps =
Array.isArray(props.smsSenders) && props.smsSenders.length ? props.smsSenders : null;
return fromProps ?? pageProps.value?.sms_senders ?? [];
});
const pageSmsTemplates = computed(() => {
const fromProps =
Array.isArray(props.smsTemplates) && props.smsTemplates.length
@@ -41,7 +66,32 @@ const pageSmsTemplates = computed(() => {
return fromProps ?? pageProps.value?.sms_templates ?? [];
});
// Helpers: EU formatter and token renderer
// SMS encoding helpers
const GSM7_EXTENDED = new Set(["^", "{", "}", "\\", "[", "~", "]", "|"]);
const isGsm7 = (text) => {
for (const ch of text || "") {
if (ch === "€") continue;
const code = ch.charCodeAt(0);
if (code >= 0x80) return false;
}
return true;
};
const gsm7Length = (text) => {
let len = 0;
for (const ch of text || "") {
if (ch === "€" || GSM7_EXTENDED.has(ch)) {
len += 2;
} else {
len += 1;
}
}
return len;
};
const ucs2Length = (text) => (text ? text.length : 0);
const formatEu = (value, decimals = 2) => {
if (value === null || value === undefined || value === "") {
return new Intl.NumberFormat("de-DE", {
@@ -81,52 +131,6 @@ const renderTokens = (text, vars) => {
});
};
// SMS length, encoding and credits
const GSM7_EXTENDED = new Set(["^", "{", "}", "\\", "[", "~", "]", "|"]);
const isGsm7 = (text) => {
for (const ch of text || "") {
if (ch === "€") continue;
const code = ch.charCodeAt(0);
if (code >= 0x80) return false;
}
return true;
};
const gsm7Length = (text) => {
let len = 0;
for (const ch of text || "") {
if (ch === "€" || GSM7_EXTENDED.has(ch)) {
len += 2;
} else {
len += 1;
}
}
return len;
};
const ucs2Length = (text) => (text ? text.length : 0);
const smsEncoding = computed(() => (isGsm7(smsMessage.value) ? "GSM-7" : "UCS-2"));
const charCount = computed(() =>
smsEncoding.value === "GSM-7"
? gsm7Length(smsMessage.value)
: ucs2Length(smsMessage.value)
);
const perSegment = computed(() => {
const count = charCount.value;
if (smsEncoding.value === "GSM-7") {
return count <= 160 ? 160 : 153;
}
return count <= 70 ? 70 : 67;
});
const segments = computed(() => {
const count = charCount.value;
const size = perSegment.value || 1;
return count > 0 ? Math.ceil(count / size) : 0;
});
const creditsNeeded = computed(() => segments.value);
const maxAllowed = computed(() => (smsEncoding.value === "GSM-7" ? 640 : 320));
const remaining = computed(() => Math.max(0, maxAllowed.value - charCount.value));
const truncateToLimit = (text, limit, encoding) => {
if (!text) return "";
if (limit <= 0) return "";
@@ -144,42 +148,137 @@ const truncateToLimit = (text, limit, encoding) => {
return out;
};
watch(smsMessage, (val) => {
const limit = maxAllowed.value;
if (charCount.value > limit) {
smsMessage.value = truncateToLimit(val, limit, smsEncoding.value);
}
// Form schema with custom validation
const formSchema = toTypedSchema(
z.object({
message: z
.string()
.min(1, "Vsebina sporočila je obvezna.")
.refine(
(val) => {
const encoding = isGsm7(val) ? "GSM-7" : "UCS-2";
const maxAllowed = encoding === "GSM-7" ? 640 : 320;
const count =
encoding === "GSM-7" ? gsm7Length(val) : ucs2Length(val);
return count <= maxAllowed;
},
{
message: "Sporočilo presega dovoljeno dolžino.",
}
),
template_id: z.number().nullable().optional(),
contract_uuid: z.string().nullable().optional(),
profile_id: z.number().nullable().optional(),
sender_id: z.number().nullable().optional(),
delivery_report: z.boolean().default(false),
})
);
const form = useForm({
validationSchema: formSchema,
initialValues: {
message: "",
template_id: null,
contract_uuid: null,
profile_id: null,
sender_id: null,
delivery_report: false,
},
});
const processing = ref(false);
const contractsForCase = ref([]);
const selectedContractUuid = ref(null);
const selectedProfileId = ref(null);
const selectedSenderId = ref(null);
const deliveryReport = ref(false);
const selectedTemplateId = ref(null);
const sendersForSelectedProfile = computed(() => {
if (!selectedProfileId.value) return pageSmsSenders.value;
if (!form.values.profile_id) return pageSmsSenders.value;
return (pageSmsSenders.value || []).filter(
(s) => s.profile_id === selectedProfileId.value
(s) => s.profile_id === form.values.profile_id
);
});
watch(selectedProfileId, () => {
if (!selectedSenderId.value) return;
const ok = sendersForSelectedProfile.value.some((s) => s.id === selectedSenderId.value);
if (!ok) selectedSenderId.value = null;
const smsEncoding = computed(() =>
isGsm7(form.values.message) ? "GSM-7" : "UCS-2"
);
const charCount = computed(() =>
smsEncoding.value === "GSM-7"
? gsm7Length(form.values.message)
: ucs2Length(form.values.message)
);
const perSegment = computed(() => {
const count = charCount.value;
if (smsEncoding.value === "GSM-7") {
return count <= 160 ? 160 : 153;
}
return count <= 70 ? 70 : 67;
});
watch(sendersForSelectedProfile, (list) => {
if (!Array.isArray(list)) return;
if (!selectedSenderId.value && list.length > 0) {
selectedSenderId.value = list[0].id;
const segments = computed(() => {
const count = charCount.value;
const size = perSegment.value || 1;
return count > 0 ? Math.ceil(count / size) : 0;
});
const creditsNeeded = computed(() => segments.value);
const maxAllowed = computed(() =>
smsEncoding.value === "GSM-7" ? 640 : 320
);
const remaining = computed(() =>
Math.max(0, maxAllowed.value - charCount.value)
);
// Truncate message if exceeds limit
watch(
() => form.values.message,
(val) => {
const limit = maxAllowed.value;
if (charCount.value > limit) {
form.setFieldValue(
"message",
truncateToLimit(val, limit, smsEncoding.value)
);
}
}
);
// Auto-select sender when profile changes
watch(form.values.profile_id, (profileId) => {
if (!profileId) {
form.setFieldValue("sender_id", null);
return;
}
const prof = (pageSmsProfiles.value || []).find((p) => p.id === profileId);
if (prof?.default_sender_id) {
const inList = sendersForSelectedProfile.value.find(
(s) => s.id === prof.default_sender_id
);
if (inList) {
form.setFieldValue("sender_id", prof.default_sender_id);
return;
}
}
// Auto-select first sender if available
if (sendersForSelectedProfile.value.length > 0) {
form.setFieldValue("sender_id", sendersForSelectedProfile.value[0].id);
} else {
form.setFieldValue("sender_id", null);
}
});
// Reset sender if not available for selected profile
watch(sendersForSelectedProfile, (list) => {
if (!form.values.sender_id || !Array.isArray(list)) return;
const ok = list.some((s) => s.id === form.values.sender_id);
if (!ok) form.setFieldValue("sender_id", null);
});
const buildVarsFromSelectedContract = () => {
const uuid = selectedContractUuid.value;
const uuid = form.values.contract_uuid;
if (!uuid) return {};
const c = (contractsForCase.value || []).find((x) => x.uuid === uuid);
if (!c) return {};
@@ -197,10 +296,14 @@ const buildVarsFromSelectedContract = () => {
type: c.account.type,
initial_amount:
c.account.initial_amount ??
(c.account.initial_amount_raw ? formatEu(c.account.initial_amount_raw) : null),
(c.account.initial_amount_raw
? formatEu(c.account.initial_amount_raw)
: null),
balance_amount:
c.account.balance_amount ??
(c.account.balance_amount_raw ? formatEu(c.account.balance_amount_raw) : null),
(c.account.balance_amount_raw
? formatEu(c.account.balance_amount_raw)
: null),
initial_amount_raw: c.account.initial_amount_raw ?? null,
balance_amount_raw: c.account.balance_amount_raw ?? null,
};
@@ -209,9 +312,11 @@ const buildVarsFromSelectedContract = () => {
};
const updateSmsFromSelection = async () => {
if (!selectedTemplateId.value) return;
if (!form.values.template_id) return;
try {
const url = route("clientCase.sms.preview", { client_case: props.clientCaseUuid });
const url = route("clientCase.sms.preview", {
client_case: props.clientCaseUuid,
});
const res = await fetch(url, {
method: "POST",
headers: {
@@ -222,49 +327,56 @@ const updateSmsFromSelection = async () => {
"",
},
body: JSON.stringify({
template_id: selectedTemplateId.value,
contract_uuid: selectedContractUuid.value || null,
template_id: form.values.template_id,
contract_uuid: form.values.contract_uuid || null,
}),
credentials: "same-origin",
});
if (res.ok) {
const data = await res.json();
if (typeof data?.content === "string" && data.content.trim() !== "") {
smsMessage.value = data.content;
form.setFieldValue("message", data.content);
return;
}
}
} catch (e) {
// ignore and fallback
}
// Fallback to client-side template rendering
const tpl = (pageSmsTemplates.value || []).find(
(t) => t.id === selectedTemplateId.value
(t) => t.id === form.values.template_id
);
if (tpl && typeof tpl.content === "string") {
smsMessage.value = renderTokens(tpl.content, buildVarsFromSelectedContract());
form.setFieldValue(
"message",
renderTokens(tpl.content, buildVarsFromSelectedContract())
);
}
};
watch(selectedTemplateId, () => {
if (!selectedTemplateId.value) return;
watch(form.values.template_id, () => {
if (!form.values.template_id) return;
updateSmsFromSelection();
});
watch(selectedContractUuid, () => {
if (!selectedTemplateId.value) return;
watch(form.values.contract_uuid, () => {
if (!form.values.template_id) return;
updateSmsFromSelection();
});
watch(pageSmsTemplates, (list) => {
if (!Array.isArray(list)) return;
if (!selectedTemplateId.value && list.length > 0) {
selectedTemplateId.value = list[0].id;
if (!form.values.template_id && list.length > 0) {
form.setFieldValue("template_id", list[0].id);
}
});
const loadContractsForCase = async () => {
try {
const url = route("clientCase.contracts.list", { client_case: props.clientCaseUuid });
const url = route("clientCase.contracts.list", {
client_case: props.clientCaseUuid,
});
const res = await fetch(url, {
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin",
@@ -280,145 +392,208 @@ watch(
() => props.show,
(newVal) => {
if (newVal) {
smsMessage.value = "";
selectedProfileId.value =
(pageSmsProfiles.value && pageSmsProfiles.value[0]?.id) || null;
if (selectedProfileId.value) {
const prof = (pageSmsProfiles.value || []).find(
(p) => p.id === selectedProfileId.value
);
if (prof && prof.default_sender_id) {
form.resetForm({
values: {
message: "",
template_id: pageSmsTemplates.value?.[0]?.id ?? null,
contract_uuid: null,
profile_id: pageSmsProfiles.value?.[0]?.id ?? null,
sender_id: null,
delivery_report: false,
},
});
// Set default sender after profile is set
const profileId = pageSmsProfiles.value?.[0]?.id;
if (profileId) {
const prof = (pageSmsProfiles.value || []).find((p) => p.id === profileId);
if (prof?.default_sender_id) {
const inList = sendersForSelectedProfile.value.find(
(s) => s.id === prof.default_sender_id
);
selectedSenderId.value = inList ? prof.default_sender_id : null;
} else {
selectedSenderId.value = null;
if (inList) {
form.setFieldValue("sender_id", prof.default_sender_id);
}
}
} else {
selectedSenderId.value = null;
}
deliveryReport.value = false;
selectedTemplateId.value =
(pageSmsTemplates.value && pageSmsTemplates.value[0]?.id) || null;
loadContractsForCase();
}
}
);
const closeSmsDialog = () => {
emit('close');
emit("close");
};
const submitSms = () => {
if (!props.phone || !smsMessage.value || !props.clientCaseUuid) {
return;
}
smsSending.value = true;
const onSubmit = form.handleSubmit((values) => {
if (!props.phone || !props.clientCaseUuid) return;
processing.value = true;
router.post(
route("clientCase.phone.sms", {
client_case: props.clientCaseUuid,
phone_id: props.phone.id,
}),
{
message: smsMessage.value,
template_id: selectedTemplateId.value,
contract_uuid: selectedContractUuid.value,
profile_id: selectedProfileId.value,
sender_id: selectedSenderId.value,
delivery_report: !!deliveryReport.value,
},
values,
{
preserveScroll: true,
onFinish: () => {
smsSending.value = false;
onSuccess: () => {
processing.value = false;
closeSmsDialog();
},
onError: () => {
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
};
});
const open = computed({
get: () => props.show,
set: (value) => {
if (!value) closeSmsDialog();
},
});
</script>
<template>
<DialogModal :show="show" @close="closeSmsDialog">
<template #title>Pošlji SMS</template>
<template #content>
<div class="space-y-2">
<p class="text-sm text-gray-600">
Prejemnik: <span class="font-mono">{{ phone?.nu }}</span>
<span v-if="phone?.country_code" class="ml-2 text-xs text-gray-500"
>CC +{{ phone.country_code }}</span
>
</p>
<!-- Profile & Sender selectors -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div>
<label class="block text-sm font-medium text-gray-700">Profil</label>
<select
v-model="selectedProfileId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null"></option>
<option v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
{{ p.name || "Profil #" + p.id }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Pošiljatelj</label>
<select
v-model="selectedSenderId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null"></option>
<option v-for="s in sendersForSelectedProfile" :key="s.id" :value="s.id">
{{ s.name || s.phone || "Sender #" + s.id }}
</option>
</select>
</div>
</div>
<!-- Contract selector -->
<div>
<label class="block text-sm font-medium text-gray-700">Pogodba</label>
<select
v-model="selectedContractUuid"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null"></option>
<option v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
{{ c.reference || c.uuid }}
</option>
</select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in {account.*}
mest.
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Pošlji SMS</DialogTitle>
<DialogDescription>
<p class="text-sm text-gray-600">
Prejemnik: <span class="font-mono">{{ phone?.nu }}</span>
<span v-if="phone?.country_code" class="ml-2 text-xs text-gray-500">
CC +{{ phone.country_code }}
</span>
</p>
</DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField v-slot="{ value, handleChange }" name="profile_id">
<FormItem>
<FormLabel>Profil</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="p in pageSmsProfiles"
:key="p.id"
:value="p.id"
>
{{ p.name || "Profil #" + p.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="sender_id">
<FormItem>
<FormLabel>Pošiljatelj</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="s in sendersForSelectedProfile"
:key="s.id"
:value="s.id"
>
{{ s.name || s.phone || "Sender #" + s.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</div>
<!-- Template selector -->
<div>
<label class="block text-sm font-medium text-gray-700">Predloga</label>
<select
v-model="selectedTemplateId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null"></option>
<option v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
{{ t.name || "Predloga #" + t.id }}
</option>
</select>
</div>
<FormField v-slot="{ value, handleChange }" name="contract_uuid">
<FormItem>
<FormLabel>Pogodba</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="c in contractsForCase"
:key="c.uuid"
:value="c.uuid"
>
{{ c.reference || c.uuid }}
</SelectItem>
</SelectContent>
</Select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in
{account.*} mest.
</p>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="template_id">
<FormItem>
<FormLabel>Predloga</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="t in pageSmsTemplates"
:key="t.id"
:value="t.id"
>
{{ t.name || "Predloga #" + t.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="message">
<FormItem>
<FormLabel>Vsebina sporočila</FormLabel>
<FormControl>
<Textarea
rows="4"
placeholder="Vpišite SMS vsebino..."
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<label class="block text-sm font-medium text-gray-700">Vsebina sporočila</label>
<textarea
v-model="smsMessage"
rows="4"
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Vpišite SMS vsebino..."
></textarea>
<!-- Live counters -->
<div class="mt-1 text-xs text-gray-600 flex flex-col gap-1">
<div class="text-xs text-gray-600 flex flex-col gap-1">
<div>
<span class="font-medium">Znakov:</span>
<span class="font-mono">{{ charCount }}</span>
@@ -437,44 +612,49 @@ const submitSms = () => {
<span class="font-mono">{{ maxAllowed }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Preostanek:</span>
<span class="font-mono" :class="{ 'text-red-600': remaining === 0 }">{{
remaining
}}</span>
<span class="font-mono" :class="{ 'text-red-600': remaining === 0 }">
{{ remaining }}
</span>
</div>
<p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake, ki
ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
sporočilih 153 znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in )
štejejo dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo
znake, ki ne zahtevajo enkodiranja. Če npr. želite pošiljati
šumnike, ki niso del 7-bitne abecede GSM, morate uporabiti Unicode
enkodiranje (UCS2). V tem primeru je največja dolžina enega SMS
sporočila 70 znakov (pri daljših sporočilih 67 znakov na del),
medtem ko je pri GSM7 160 znakov (pri daljših sporočilih 153
znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in ) štejejo
dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
320 (UCS2) znakov.
</p>
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 mt-1">
<input
type="checkbox"
v-model="deliveryReport"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
Zahtevaj poročilo o dostavi
</label>
</div>
</template>
<template #footer>
<button class="px-3 py-1 rounded border mr-2" @click="closeSmsDialog">
Prekliči
</button>
<button
class="px-3 py-1 rounded bg-indigo-600 text-white disabled:opacity-50"
:disabled="smsSending || !smsMessage"
@click="submitSms"
>
Pošlji
</button>
</template>
</DialogModal>
</template>
<FormField v-slot="{ value, handleChange }" name="delivery_report">
<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>Zahtevaj poročilo o dostavi</FormLabel>
</div>
</FormItem>
</FormField>
</form>
<DialogFooter>
<Button variant="outline" @click="closeSmsDialog" :disabled="processing">
Prekliči
</Button>
<Button
@click="onSubmit"
:disabled="processing || !form.values.message"
>
Pošlji
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -1,5 +1,5 @@
<script setup>
import { PlusIcon, DottedMenu, EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import { DottedMenu, EditIcon, TrashBinIcon, PlusIcon } from "@/Utilities/Icons";
import Dropdown from "../Dropdown.vue";
const props = defineProps({
@@ -23,17 +23,7 @@ const handleDelete = (id, label) => emit('delete', id, label);
</script>
<template>
<div class="flex justify-end mb-3" v-if="edit">
<button
@click="handleAdd"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all"
title="Dodaj TRR"
>
<PlusIcon size="sm" />
<span>Dodaj TRR</span>
</button>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getTRRs(person).length">
<div
class="rounded-lg p-4 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
@@ -108,7 +98,15 @@ const handleDelete = (id, label) => emit('delete', id, label);
</p>
</div>
</template>
<p v-else class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
<button
v-if="edit"
@click="handleAdd"
class="rounded-lg p-4 bg-white border-2 border-dashed border-gray-300 hover:border-gray-400 hover:bg-gray-50 transition-all flex items-center justify-center min-h-[120px]"
title="Dodaj TRR"
>
<PlusIcon class="h-8 w-8 text-gray-400" />
</button>
<p v-else-if="!edit && !getTRRs(person).length" class="col-span-full p-4 text-sm text-gray-500 text-center bg-gray-50 rounded-lg border border-gray-200">
Ni TRR računov.
</p>
</div>
@@ -20,7 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Switch } from "@/Components/ui/switch";
const props = defineProps({
show: {
@@ -216,7 +216,7 @@ const onSubmit = form.handleSubmit(() => {
<FormField v-slot="{ value, handleChange }" name="validated">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox :checked="value" @update:checked="handleChange" />
<Switch :model-value="value" @update:model-value="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel class="cursor-pointer">Potrjeno</FormLabel>
@@ -20,7 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Switch } from "@/Components/ui/switch";
const props = defineProps({
show: {
@@ -241,7 +241,7 @@ const onSubmit = form.handleSubmit(() => {
<FormField v-slot="{ value, handleChange }" name="validated">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox :checked="value" @update:checked="handleChange" />
<Switch :model-value="value" @update:model-value="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel class="cursor-pointer">Potrjeno</FormLabel>