Files
app-v2/src/components/settings/CategoryCreateUpdate.vue
2025-10-31 15:22:44 +03:00

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>