294 lines
9.5 KiB
Vue
294 lines
9.5 KiB
Vue
<script setup lang="ts">
|
|
import {useRoute, useRouter} from "vue-router";
|
|
import {computed, onMounted, ref} from "vue";
|
|
import {useToolbarStore} from "@/stores/toolbar-store";
|
|
import {useToast} from "primevue/usetoast";
|
|
import {SelectButton} from "primevue";
|
|
import {categoriesService} from "@/services/categories-service";
|
|
import {useSpaceStore} from "@/stores/spaceStore";
|
|
import {CategoryType, CategoryTypeName} from "@/models/enums";
|
|
import emojiRegex from 'emoji-regex'
|
|
import {useCategoriesStore} from "@/stores/categories-store";
|
|
import {CreateCategoryDTO, UpdateCategoryDTO} from "@/models/category";
|
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
|
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const toolbar = useToolbarStore();
|
|
const toast = useToast();
|
|
const spaceStore = useSpaceStore();
|
|
const categoriesStore = useCategoriesStore();
|
|
const tgApp = window.Telegram.WebApp
|
|
|
|
|
|
const categoryId = ref<string | undefined>(route.params.id as string)
|
|
const mode = computed(() => {
|
|
return categoryId.value ? "edit" : "create"
|
|
})
|
|
const categoryType = ref<CategoryType>(CategoryType.EXPENSE)
|
|
const isCategoryTypeError = ref<boolean>(false)
|
|
const categoryName = ref<string>('')
|
|
const isCategoryNameError = ref<boolean>(false)
|
|
const categoryIcon = ref<string>("🛍️")
|
|
const isCategoryIconError = ref<boolean>(false)
|
|
const categoryDescription = ref<string>('')
|
|
const isCategoryDescriptionError = ref<boolean>(false)
|
|
|
|
|
|
// Генерим опции: [{ label: "Расходы", value: "EXPENSE" }, ...]
|
|
const options = Object.values(CategoryType).map(type => ({
|
|
label: CategoryTypeName[type],
|
|
value: type
|
|
}))
|
|
|
|
const fetchCategory = async () => {
|
|
try {
|
|
if (spaceStore.selectedSpaceId && categoryId.value) {
|
|
let category = await categoriesService.fetchCategory(spaceStore.selectedSpaceId, Number(categoryId.value))
|
|
categoryType.value = category.type
|
|
categoryName.value = category.name
|
|
categoryDescription.value = category.description
|
|
categoryIcon.value = category.icon
|
|
}
|
|
} catch (err) {
|
|
console.log(err)
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error while fetching category",
|
|
detail: err.detail.message,
|
|
life: 3000,
|
|
})
|
|
}
|
|
}
|
|
const re = emojiRegex()
|
|
|
|
function toOneEmoji(raw: string): string {
|
|
const matches = raw.match(re) ?? [] // ← вместо [...raw.matchAll(re)]
|
|
return matches.length ? matches[matches.length - 1] : '🛍️'
|
|
}
|
|
|
|
function handleInput(e: Event) {
|
|
const el = e.target as HTMLInputElement
|
|
const next = toOneEmoji(el.value)
|
|
if (el.value !== next) {
|
|
el.value = next // визуально заменить
|
|
}
|
|
categoryIcon.value = next // обновить v-model
|
|
}
|
|
|
|
function handlePaste(e: ClipboardEvent) {
|
|
const text = e.clipboardData?.getData('text') ?? ''
|
|
const next = toOneEmoji(text)
|
|
e.preventDefault()
|
|
const target = e.target as HTMLInputElement
|
|
target.value = next
|
|
categoryIcon.value = next
|
|
}
|
|
|
|
// для мобильных IME: окончание композиции
|
|
function handleCompositionEnd(e: CompositionEvent) {
|
|
handleInput(e as unknown as Event)
|
|
}
|
|
|
|
const validateForm = (): boolean => {
|
|
if (!categoryType.value) {
|
|
isCategoryTypeError.value = true
|
|
return false;
|
|
}
|
|
if (categoryName.value.length == 0) {
|
|
isCategoryNameError.value = true
|
|
return false;
|
|
}
|
|
if (categoryIcon.value.length == 0) {
|
|
isCategoryIconError.value = true
|
|
return false;
|
|
}
|
|
console.log(categoryDescription.value.length)
|
|
if (categoryDescription.value.length == 0) {
|
|
isCategoryDescriptionError.value = true
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const buildUpdate = (): UpdateCategoryDTO => {
|
|
if (validateForm()) {
|
|
return {
|
|
type: categoryType.value,
|
|
name: categoryName.value,
|
|
description: categoryDescription.value,
|
|
icon: categoryIcon.value,
|
|
} as UpdateCategoryDTO
|
|
} else {
|
|
throw Error("Error while validating form")
|
|
}
|
|
}
|
|
|
|
const buildCreate = (): CreateCategoryDTO => {
|
|
if (validateForm()) {
|
|
return {
|
|
type: categoryType.value,
|
|
name: categoryName.value,
|
|
description: categoryDescription.value,
|
|
icon: categoryIcon.value,
|
|
} as CreateCategoryDTO
|
|
} else {
|
|
throw Error("Error while validating form")
|
|
}
|
|
}
|
|
|
|
const moveUser = async () => {
|
|
if (window.history.length > 1) {
|
|
console.log('moved back')
|
|
router.back()
|
|
} else {
|
|
console.log('moved forward')
|
|
await router.push('/categories')
|
|
}
|
|
}
|
|
|
|
const isDeleteAlertVisible = ref(false)
|
|
const deleteAlertMessage = ref<string>('Do you really want to delete the category?')
|
|
const deleteCategory = async () => {
|
|
if (spaceStore.selectedSpaceId) {
|
|
await categoriesService.deleteCategory(spaceStore.selectedSpaceId, Number(categoryId.value))
|
|
await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
|
await moveUser()
|
|
}
|
|
}
|
|
|
|
|
|
onMounted(async () => {
|
|
if (mode.value === "edit") {
|
|
await fetchCategory()
|
|
toolbar.registerHandler('deleteCategory', () => {
|
|
if (tgApp.initData) {
|
|
tgApp.showConfirm(deleteAlertMessage.value, async (confirmed: boolean) => {
|
|
if (confirmed) {
|
|
await deleteCategory()
|
|
}
|
|
})
|
|
} else {
|
|
isDeleteAlertVisible.value = true
|
|
}
|
|
})
|
|
toolbar.registerHandler('updateCategory', async () => {
|
|
if (spaceStore.selectedSpaceId) {
|
|
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
|
try {
|
|
let updateDTO = buildUpdate()
|
|
await categoriesService.updateCategory(spaceStore.selectedSpaceId, Number(categoryId.value), updateDTO)
|
|
console.log(updateDTO)
|
|
await moveUser()
|
|
} catch (e) {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error while updating category",
|
|
detail: e,
|
|
life: 3000,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
toolbar.registerHandler('createCategory', async () => {
|
|
if (spaceStore.selectedSpaceId) {
|
|
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
|
try {
|
|
let createDTO = buildCreate()
|
|
await categoriesService.createCategory(spaceStore.selectedSpaceId, createDTO)
|
|
console.log(createDTO)
|
|
await moveUser()
|
|
} catch (e) {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error while creating category",
|
|
detail: e,
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
|
|
<div class="flex flex-col w-full justify-items-start gap-7">
|
|
<ConfirmDialog
|
|
v-if="isDeleteAlertVisible"
|
|
:message="deleteAlertMessage"
|
|
:callback="(confirmed: boolean) => { if (confirmed) deleteCategory(); isDeleteAlertVisible = false; }"
|
|
/>
|
|
<div class="flex flex-col w-full ">
|
|
<div class="flex flex-col " v-tooltip.focus.bottom="'Only emoji supported'">
|
|
<input
|
|
class="
|
|
block w-full
|
|
text-9xl font-extralight
|
|
text-center placeholder:text-center /* центрируем текст и плейсхолдер */
|
|
p-0 m-0 border-0 bg-transparent
|
|
focus:outline-none appearance-none /* убрать системные артефакты iOS/TG */
|
|
leading-none
|
|
"
|
|
placeholder="Icon"
|
|
v-model="categoryIcon"
|
|
@input="handleInput"
|
|
@paste="handlePaste"
|
|
@compositionend="handleCompositionEnd"
|
|
inputmode="text"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
<label class=" !justify-center !font-extralight text-gray-600 text-center">Category
|
|
icon</label>
|
|
<span v-if="isCategoryIconError"
|
|
class="text-sm !text-red-500 font-extralight">Icon cannot be empty or non-emoji</span>
|
|
</div>
|
|
|
|
<div class="flex w-full !items-center !justify-center">
|
|
<SelectButton
|
|
v-model="categoryType"
|
|
:options="options"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
class="!w-full !items-center !justify-center !border-none "
|
|
/>
|
|
<span v-if="isCategoryTypeError"
|
|
class="text-sm !text-red-500 font-extralight">Category type cannot be empty</span>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="flex flex-col w-full justify-items-start">
|
|
<label class="!font-semibold text-gray-600 pl-2">Category name</label>
|
|
<div class="flex card !rounded-3xl !justify-start !items-start !p-4 !pl-5 ">
|
|
<input class="font-extralight w-full focus:outline-0" placeholder="Name" v-model="categoryName"
|
|
@input="categoryName.length !== 0 ? isCategoryNameError = false : true"/>
|
|
</div>
|
|
<span v-if="isCategoryNameError" class="text-sm !text-red-500 font-extralight">Name cannot be empty</span>
|
|
</div>
|
|
<div class="flex flex-col w-full justify-items-start">
|
|
<label class="!font-semibold text-gray-600 !pl-2">Category description</label>
|
|
<div class="flex card !justify-start !items-start !pl-2">
|
|
<textarea
|
|
class="!font-extralight !text-start !pl-2 w-full focus:outline-0 !focus:border-0 !@focus:shadow-none !bg-white !border-0 min-h-36"
|
|
style="box-shadow: none !important;"
|
|
placeholder="Description" v-model="categoryDescription"
|
|
@input="categoryDescription.length !== 0 ? isCategoryDescriptionError = false : true"/>
|
|
</div>
|
|
<span v-if="isCategoryDescriptionError"
|
|
class="text-sm !text-red-500 font-extralight">Description cannot be empty</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<style scoped>
|
|
.p-togglebutton-label, .p-togglebutton {
|
|
padding: 10rem !important;
|
|
}
|
|
</style> |