feat: bind crud
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s

This commit is contained in:
Thanaphon Frappet 2025-03-10 17:19:45 +07:00
parent 39c7754d5f
commit bfa0571e0b
4 changed files with 400 additions and 17 deletions

View file

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { onMounted, reactive, ref } from 'vue';
import { QSelect, QTableProps } from 'quasar';
@ -9,9 +10,13 @@ import StatCardComponent from 'src/components/StatCardComponent.vue';
import NoData from 'src/components/NoData.vue';
import { watch } from 'vue';
import KebabAction from 'src/components/shared/KebabAction.vue';
import { PaginationComponent } from 'src/components';
import { FloatingActionButton, PaginationComponent } from 'src/components';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import PropertyDialog from './PropertyDialog.vue';
import { Property } from 'src/stores/property/types';
import { dialog } from 'src/stores/utils';
const { t } = useI18n();
const $q = useQuasar();
const navigatorStore = useNavigator();
const propertyStore = useProperty();
@ -22,6 +27,12 @@ const {
pageMax: propertyPageMax,
} = storeToRefs(propertyStore);
const currPropertyData = ref<Property>();
const formProperty = ref<Property>({
name: '',
nameEN: '',
type: {},
});
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refFilter = ref<InstanceType<typeof QSelect>>();
const fieldSelected = ref<('name' | 'type')[]>(['name', 'type']);
@ -92,6 +103,135 @@ async function fetchPropertyList(mobileFetch?: boolean) {
}
}
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
pageState.addModal = true;
pageState.isDrawerEdit = true;
}
if (type === 'view') {
pageState.viewDrawer = true;
pageState.isDrawerEdit = false;
}
if (type === 'edit') {
pageState.viewDrawer = true;
pageState.isDrawerEdit = true;
}
}
function assignFormData(propertyData: Property) {
currPropertyData.value = JSON.parse(JSON.stringify(propertyData));
formProperty.value = JSON.parse(
JSON.stringify({
name: propertyData.name,
nameEN: propertyData.nameEN,
type: propertyData.type,
status: propertyData.status,
}),
);
}
function resetForm() {
currPropertyData.value = undefined;
pageState.isDrawerEdit = true;
pageState.addModal = false;
pageState.viewDrawer = false;
formProperty.value = {
name: '',
nameEN: '',
type: {},
};
}
async function deleteProperty(id?: string) {
const targetId = id || currPropertyData.value?.id;
if (targetId === undefined) return;
dialog({
color: 'negative',
icon: 'mdi-trash-can-outline',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
await propertyStore.deleteProperty(targetId);
await fetchPropertyList($q.screen.xs);
resetForm();
},
cancel: () => {},
});
}
async function changeStatus(id?: string) {
const targetId = id || currPropertyData.value?.id;
if (targetId === undefined) return;
formProperty.value.status =
formProperty.value.status !== 'INACTIVE' ? 'INACTIVE' : 'ACTIVE';
const res = await propertyStore.editProperty({
id: targetId,
...formProperty.value,
});
if (res) {
formProperty.value.status = res.data.status;
if (currPropertyData.value) {
currPropertyData.value.status = res.data.status;
}
await fetchPropertyList();
}
}
async function triggerChangeStatus(data?: Property) {
const targetId = (data && data.id) || currPropertyData.value?.id;
const targetStatus = (data && data.status) || currPropertyData.value?.status;
if (data) assignFormData(data);
if (targetId === undefined || targetStatus === undefined) return;
return await new Promise((resolve, reject) => {
dialog({
color: targetStatus !== 'INACTIVE' ? 'warning' : 'info',
icon:
targetStatus !== 'INACTIVE'
? 'mdi-alert'
: 'mdi-message-processing-outline',
title: t('dialog.title.confirmChangeStatus'),
actionText:
targetStatus !== 'INACTIVE' ? t('general.close') : t('general.open'),
message:
targetStatus !== 'INACTIVE'
? t('dialog.message.confirmChangeStatusOff')
: t('dialog.message.confirmChangeStatusOn'),
action: async () => {
await changeStatus(targetId).then(resolve).catch(reject);
},
cancel: () => {},
});
});
}
function undo() {
if (!currPropertyData.value) return;
assignFormData(currPropertyData.value);
pageState.isDrawerEdit = false;
}
async function submit() {
if (currPropertyData.value?.id !== undefined) {
await propertyStore.editProperty({
id: currPropertyData.value.id,
...formProperty.value,
});
} else {
await propertyStore.creatProperty({
...formProperty.value,
});
}
await fetchPropertyList($q.screen.xs);
resetForm();
}
onMounted(async () => {
navigatorStore.current.title = 'property.title';
navigatorStore.current.path = [{ text: 'property.caption', i18n: true }];
@ -113,6 +253,12 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
});
</script>
<template>
<FloatingActionButton
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
></FloatingActionButton>
<div class="column full-height no-wrap">
<!-- SEC: stat -->
<section class="text-body-2 q-mb-xs flex items-center">
@ -367,7 +513,7 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
<section class="row items-center no-wrap">
{{
$i18n.locale === 'eng'
? props.row.nameEn
? props.row.nameEN
: props.row.name
}}
</section>
@ -392,10 +538,28 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
<KebabAction
:id-name="props.row.name"
:status="props.row.status"
@view="() => {}"
@edit="() => {}"
@delete="() => {}"
@change-status="() => {}"
@view="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
@edit="
() => {
assignFormData(props.row);
triggerDialog('edit');
}
"
@delete="
() => {
deleteProperty(props.row.id);
}
"
@change-status="
() => {
triggerChangeStatus(props.row);
}
"
/>
</q-td>
</q-tr>
@ -427,10 +591,17 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
'app-text-muted': props.row.status === 'INACTIVE',
}"
>
{{ props.row.name }}
{{
$i18n.locale === 'eng'
? props.row.nameEN
: props.row.name
}}
<q-tooltip>
{{ props.row.name }}
{{
$i18n.locale === 'eng'
? props.row.nameEN
: props.row.name
}}
</q-tooltip>
</div>
<div
@ -458,10 +629,28 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
<KebabAction
:id-name="props.row.id"
:status="props.row.status"
@view="() => {}"
@edit="() => {}"
@delete="() => {}"
@change-status="() => {}"
@view="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
@edit="
() => {
assignFormData(props.row);
triggerDialog('edit');
}
"
@delete="
() => {
deleteProperty(props.row.id);
}
"
@change-status="
() => {
triggerChangeStatus(props.row);
}
"
/>
</nav>
</div>
@ -514,12 +703,26 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
<PaginationComponent
v-model:current-page="propertyPage"
v-model:max-page="propertyPageMax"
:fetch-data="() => {}"
:fetch-data="() => fetchPropertyList()"
/>
</nav>
</footer>
</div>
</section>
</div>
<PropertyDialog
@change-status="() => triggerChangeStatus()"
@drawer-delete="() => deleteProperty()"
@drawer-edit="pageState.isDrawerEdit = true"
@drawer-undo="() => undo()"
@close="() => resetForm()"
@submit="() => submit()"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"
v-model:property-data="formProperty"
v-model:drawer-model="pageState.viewDrawer"
/>
</template>
<style scoped></style>

View file

@ -1,3 +1,179 @@
<script lang="ts" setup></script>
<template></template>
<script lang="ts" setup>
import DialogForm from 'src/components/DialogForm.vue';
import SideMenu from 'src/components/SideMenu.vue';
import DrawerInfo from 'src/components/DrawerInfo.vue';
import {
UndoButton,
SaveButton,
EditButton,
DeleteButton,
} from 'src/components/button';
import { Property } from 'src/stores/property/types';
const model = defineModel<boolean>({ required: true, default: false });
const drawerModel = defineModel<boolean>('drawerModel', {
required: true,
default: false,
});
const formProperty = defineModel<Property>('propertyData', {
required: true,
default: {
name: '',
nameEN: '',
type: {},
},
});
withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
}>(),
{ readonly: false, isEdit: false },
);
defineEmits<{
(e: 'submit'): void;
(e: 'close'): void;
(e: 'changeStatus'): void;
(e: 'drawerUndo'): void;
(e: 'drawerEdit'): void;
(e: 'drawerDelete'): void;
}>();
</script>
<template>
<DialogForm
:title="$t('flow.title')"
v-model:modal="model"
:submit="() => $emit('submit')"
:close="() => $emit('close')"
hide-footer
>
<div
class="col surface-1 rounded bordered scroll row relative-position"
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<div
class="rounded row"
style="position: absolute; z-index: 999; right: 0; top: 0"
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
>
<div class="surface-1 row rounded">
<SaveButton id="btn-info-basic-save" icon-only type="submit" />
</div>
</div>
<section
class="col-12 col-md-10"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
id="flow-form-dialog"
></section>
</div>
</DialogForm>
<DrawerInfo
bg-on
hide-action
:is-edit="isEdit"
:title="propertyData.name"
v-model:drawerOpen="drawerModel"
:submit="() => $emit('submit')"
:close="() => $emit('close')"
>
<div class="col column full-height">
<div
style="flex: 1; width: 100%; overflow-y: auto"
id="drawer-user-form"
:class="{
'q-px-lg q-py-md': $q.screen.gt.sm,
'q-px-md q-py-sm': !$q.screen.gt.sm,
}"
>
<div
class="col surface-1 full-height rounded bordered scroll row relative-position"
>
<div
class="rounded row"
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div
v-if="propertyData.status !== 'INACTIVE'"
class="surface-1 row rounded"
>
<UndoButton
v-if="isEdit"
id="btn-info-basic-undo"
icon-only
@click="
() => {
$emit('drawerUndo');
}
"
type="button"
/>
<SaveButton
v-if="isEdit"
id="btn-info-basic-save"
icon-only
type="submit"
/>
<EditButton
v-if="!isEdit"
id="btn-info-basic-edit"
icon-only
@click="
() => {
$emit('drawerEdit');
// infoDrawerEdit = true;
// isImageEdit = true;
}
"
type="button"
/>
<DeleteButton
v-if="!isEdit"
id="btn-info-basic-delete"
icon-only
@click="
() => {
$emit('drawerDelete');
// onDelete(currentUser.id);
}
"
type="button"
/>
</div>
</div>
<section
class="col-12 col-md-10"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
id="flow-form-drawer"
>
asdas
</section>
</div>
</div>
</div>
</DrawerInfo>
</template>
<style scoped></style>

View file

@ -41,7 +41,7 @@ export const useProperty = defineStore('property-store', () => {
return res;
}
async function editProperty(data: Property & { id: string }) {
async function editProperty(data: Property) {
const res = await api.put<Property>(`/property/${data.id}`, {
...data,
id: undefined,

View file

@ -1,5 +1,9 @@
import { Status } from '../types';
export type Property = {
id?: string;
name: string;
nameEN: string;
type: Record<string, any>;
status?: Status;
};