Commit 058f162e authored by Dhevan's avatar Dhevan

nyicil form

parent 7a32c366
......@@ -9,6 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@popperjs/core": "2.11.8",
"@vueform/multiselect": "^2.6.6",
"@vuepic/vue-datepicker": "^8.2.0",
"axios": "^1.6.7",
"bootstrap": "5.3.3",
"chart.js": "4.4.1",
......@@ -17,11 +19,13 @@
"pinia": "^2.1.7",
"quill": "1.3.7",
"sweetalert2": "^11.10.6",
"vee-validate": "^4.12.6",
"vue": "^3.4.15",
"vue-count-to": "1.0.13",
"vue-flatpickr-component": "11.0.4",
"vue-router": "^4.2.5",
"vuex": "^4.1.0"
"vuex": "^4.1.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
......@@ -1046,6 +1050,25 @@
}
}
},
"node_modules/@vueform/multiselect": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@vueform/multiselect/-/multiselect-2.6.6.tgz",
"integrity": "sha512-JDWesVRmyGz9HmHp2Ooy1cb8XgKohiztwMDtjm8c0/Th+7wEZENZuYa0iY5CTvaJNANl3LVqh9BNnCc/YlM/Bg=="
},
"node_modules/@vuepic/vue-datepicker": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-8.2.0.tgz",
"integrity": "sha512-H3X9/XRvF4/ttdgxgNhFCp9m9gt9UVsiuv8FXOfAPpfQvVlmlTHZ3yf3E5oAxzP/INVhBIJCGrkijhCl5+UAPg==",
"dependencies": {
"date-fns": "^3.3.1"
},
"engines": {
"node": ">=18.12.0"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
......@@ -1832,6 +1855,15 @@
"node": ">=18"
}
},
"node_modules/date-fns": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.4.0.tgz",
"integrity": "sha512-Akz4R8J9MXBsOgF1QeWeCsbv6pntT5KCPjU0Q9prBxVmWJYPLhwAIsNg3b0QAdr0ttiozYLD3L/af7Ra0jqYXw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
......@@ -3704,6 +3736,11 @@
"node": ">= 0.6.0"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
......@@ -4390,6 +4427,11 @@
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
},
"node_modules/tinybench": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
......@@ -4435,6 +4477,11 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
},
"node_modules/tough-cookie": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
......@@ -4566,6 +4613,29 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/vee-validate": {
"version": "4.12.6",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.12.6.tgz",
"integrity": "sha512-EKM3YHy8t1miPh30d5X6xOrfG/Ctq0nbN4eMpCK7ezvI6T98/S66vswP+ihL4QqAK/k5KqreWOxof09+JG7N/A==",
"dependencies": {
"@vue/devtools-api": "^6.5.1",
"type-fest": "^4.8.3"
},
"peerDependencies": {
"vue": "^3.3.11"
}
},
"node_modules/vee-validate/node_modules/type-fest": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz",
"integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
......@@ -5197,6 +5267,28 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz",
"integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
......@@ -13,6 +13,8 @@
},
"dependencies": {
"@popperjs/core": "2.11.8",
"@vueform/multiselect": "^2.6.6",
"@vuepic/vue-datepicker": "^8.2.0",
"axios": "^1.6.7",
"bootstrap": "5.3.3",
"chart.js": "4.4.1",
......@@ -21,11 +23,13 @@
"pinia": "^2.1.7",
"quill": "1.3.7",
"sweetalert2": "^11.10.6",
"vee-validate": "^4.12.6",
"vue": "^3.4.15",
"vue-count-to": "1.0.13",
"vue-flatpickr-component": "11.0.4",
"vue-router": "^4.2.5",
"vuex": "^4.1.0"
"vuex": "^4.1.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
......
<template>
<div class="col">
<label v-if="!hideLabel" :for="`${name}-input`" class="form-label">
{{ label }}
</label>
<Multiselect
v-model="value"
:name="name"
:placeholder="label"
:options="fetchOptions"
:filter-results="false"
:min-chars="1"
:resolve-on-load="resolveOnLoad"
:delay="0"
:searchable="true"
:mode="mode != null ? mode : 'single'"
>
</Multiselect>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script>
import { toRef } from "vue";
import { useField } from "vee-validate";
import Multiselect from "@vueform/multiselect";
export default {
props: {
resolveOnLoad: {
default: true,
},
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
optionText: {
type: String,
default: null,
},
optionValue: {
type: String,
default: null,
},
options: {
required: true,
},
loading: {
type: Boolean,
default: false,
},
mode: {
type: String,
default: "single",
},
hideLabel: {
default: false,
},
},
components: { Multiselect },
setup(props, ctx) {
const name = toRef(props, "name");
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
const fetchOptions = async (keyword) => {
console.log("masuk sini");
let data = await props.options(keyword);
data = data.map((item) => ({
value: item[props.optionValue],
label: item[props.optionText],
}));
return data;
};
return {
name,
value,
errorMessage,
handleBlur,
fetchOptions,
};
},
};
</script>
<template>
<div class="col px-1 mt-1">
<label v-if="!hideLabel" :for="`${name}-input`" class="form-label">{{
label
}}</label>
<select
v-model="value"
class="form-control"
:name="name"
:disabled="readonly"
:placeholder="label"
>
<template v-for="option in options">
<option>{{ option }}</option>
</template>
</select>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { toRef } from "vue";
import { useField } from "vee-validate";
import Multiselect from "@vueform/multiselect";
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
optionText: {
type: String,
default: null,
},
optionValue: {
type: String,
default: null,
},
options: {
type: Array,
default: [],
},
loading: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
mode: {
type: String,
required: false,
default: "single",
},
hideLabel: {
default: false,
},
});
const name = toRef(props, "name");
const options = computed(() =>
props.options.map((option) => {
if (typeof option != "object") {
return option;
}
return {
value: option[props.optionValue],
label: option[props.optionText],
};
})
);
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
:id="`${name}-input`"
:name="name"
:label="label"
v-model="value"
@blur="handleBlur"
/>
<label
:for="`${name}-input`"
class="form-check-label"
for="flexCheckDefault"
>
{{ label }}
</label>
</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
});
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<VueDatePicker
v-model="value"
:name="name"
:disabled="readonly"
:placeholder="label"
format="dd MMMM yyyy"
model-type="yyyy-MM-dd"
></VueDatePicker>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { toRef } from "vue";
import { useField } from "vee-validate";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
});
const name = toRef(props, "name");
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<input
:id="`${name}-input`"
:name="name"
:label="label"
v-model="value"
type="number"
step="0.000000000000001"
class="form-control"
:placeholder="label"
@blur="handleBlur"
/>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
});
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
\ No newline at end of file
<template>
<form @submit="onSubmit">
<div class="card-body">
<div class="row g-3">
<template v-for="schema in schemas">
<template v-if="schema.type != 'section'">
<div :class="`col-md-${schema.cols}`">
<slot :name="schema.name" v-bind="values">
<template v-if="schema.type?.includes('-select')">
<component
:is="schemaTypeToComponent[schema.type ?? 'select']"
:name="schema.name"
:label="schema.label"
:mode="schema.mode"
:readonly="schema.readonly"
:keyword="
formValue[schema.name] ? formValue[schema.name] : null
"
class="py-0 mb-1"
/>
</template>
<template v-else>
<component
@change="handleChange"
:is="schemaTypeToComponent[schema.type ?? 'text']"
:name="schema.name"
:label="schema.label"
class="py-0 mb-1"
:options="schema.options"
:option-text="schema.optionText"
:option-value="schema.optionValue"
:rows="schema.rows"
:readonly="schema.readonly"
@removePhoto="handleRemoveFile"
/>
</template>
</slot>
</div>
</template>
<template v-if="schema.type == 'section'">
<h3>{{ schema.title }}</h3>
<hr />
<template v-for="childSchema in schema.children">
<div :class="`col-md-${childSchema.cols}`">
<slot :name="childSchema.name" v-bind="values">
<template v-if="childSchema.type?.includes('-select')">
<component
:is="schemaTypeToComponent[childSchema.type ?? 'select']"
:name="childSchema.name"
:label="childSchema.label"
:mode="childSchema.mode"
:readonly="childSchema.readonly"
:keyword="
formValue[childSchema.name]
? formValue[childSchema.name]
: null
"
class="py-0 mb-1"
/>
</template>
<template v-else>
<component
:is="schemaTypeToComponent[childSchema.type ?? 'text']"
:name="childSchema.name"
:label="childSchema.label"
class="py-0 mb-1"
:options="childSchema.options"
:option-text="childSchema.optionText"
:option-value="childSchema.optionValue"
:rows="childSchema.rows"
:readonly="childSchema.readonly"
@removePhoto="handleRemoveFile"
/>
</template>
</slot>
</div>
</template>
</template>
</template>
</div>
<slot name="bottom-action"></slot>
</div>
<div v-if="!hideAction" class="card-footer toolbar py-2">
<div class="d-flex justify-content-end">
<button class="btn btn-light me-1" @click="$router.go(-1)">
<i class="pli-back fs-5"></i>
<span class="d-none d-lg-inline-block ms-lg-2">Back</span>
</button>
<button type="submit" class="btn btn-primary">
<i class="pli-save fs-5"></i>
<span v-if="saveText" class="d-none d-lg-inline-block ms-lg-2">{{
saveText
}}</span>
<span v-else class="d-none d-lg-inline-block ms-lg-2">{{
isEdit ? "Update" : "Simpan"
}}</span>
</button>
</div>
</div>
</form>
</template>
<script>
import { ref, computed, onMounted, watch } from "vue";
import { useForm } from "vee-validate";
import { object as YupObject } from "yup";
import TextField from "./TextField.vue";
import NumberField from "./NumberField.vue";
import TextArea from "./TextArea.vue";
import DecimalField from "./DecimalField.vue";
import SelectField from "./SelectField.vue";
import Checkbox from "./Checkbox.vue";
import ImageInput from "./ImageInput.vue";
import MultipleImageInput from "./MultipleImageInput.vue";
import DatePicker from "./DatePicker.vue";
import TimePicker from "./TimePicker.vue";
import PasswordField from "./PasswordField.vue";
import FileInput from "./FileInput.vue";
export default {
props: {
schemas: {
default: [],
},
formValue: {
type: Object,
default: {},
},
isEdit: {
type: Boolean,
default: false,
},
hideAction: {
type: Boolean,
default: false,
},
saveText: {
default: null,
},
},
setup(props, ctx) {
const schemaTypeToComponent = {
text: TextField,
password: PasswordField,
select: SelectField,
"image-input": ImageInput,
"multiple-image-input": MultipleImageInput,
date: DatePicker,
time: TimePicker,
textarea: TextArea,
number: NumberField,
decimal: DecimalField,
checkbox: Checkbox,
"file-input": FileInput,
};
const removedFile = ref([]);
const validation_schema = computed(() => {
const validations = {};
props.schemas.forEach((schema) => {
validations[schema.name] = schema.validation;
});
return YupObject(validations);
});
const { handleSubmit, resetForm, values, setFieldError } = useForm({
initialValues: props.formValue,
validationSchema: validation_schema,
});
function onInvalidSubmit({ values, errors, results }) {
console.log(values); // current form values
console.log(errors); // a map of field names and their first error message
console.log(results); // a detailed map of field names and their validation results
}
const onSubmit = handleSubmit((values, { resetForm }) => {
if (values.photo) {
values.photo = values.photo
.filter((photo) => photo.id == null || !photo.id)
.map((photo) => photo.url);
values.remove_photo = removedFile.value.join(",");
}
ctx.emit("formSubmit", values, { resetForm });
}, onInvalidSubmit);
const setError = (errors) => {
Object.keys(errors).forEach((field) => {
setFieldError(field, errors[field].join(", "));
});
};
const handleRemoveFile = (id) => {
removedFile.value.push(id);
};
onMounted(async () => {
resetForm({ values: props.formValue });
});
const handleChange = (_change) => {
// console.log(change.target._modelValue);
};
watch(
() => values,
(newValue, _oldValue) => {
ctx.emit("data-change", values);
},
{ deep: true }
);
return {
handleChange,
values,
onSubmit,
schemaTypeToComponent,
setError,
handleRemoveFile,
};
},
};
</script>
<template>
<div class="row g-3">
<template v-for="schema in schemas">
<div :class="`col-md-${schema.cols}`">
<slot :name="schema.name">
<template v-if="schema.type?.includes('-select')">
<component
:is="schemaTypeToComponent[schema.type ?? 'select']"
:name="schema.name"
:label="schema.label"
:mode="schema.mode"
:readonly="schema.readonly"
:keyword="formValue[schema.name] ? formValue[schema.name] : null"
class="py-0 mb-1"
/>
</template>
<template v-else>
<component
:is="schemaTypeToComponent[schema.type ?? 'text']"
:name="schema.name"
:label="schema.label"
class="py-0 mb-1"
:options="schema.options"
:option-text="schema.optionText"
:option-value="schema.optionValue"
:rows="schema.rows"
:readonly="schema.readonly"
/>
</template>
</slot>
</div>
</template>
</div>
</template>
<script setup>
import TextField from "./TextField.vue";
import NumberField from "./NumberField.vue";
import TextArea from "./TextArea.vue";
import DecimalField from "./DecimalField.vue";
import SelectField from "./SelectField.vue";
import Checkbox from "./Checkbox.vue";
import RepresentativeSelect from "./../representative/RepresentativeSelect.vue";
import RetailChannelSelect from "./../retail_channel/RetailChannelSelect.vue";
import ChannelType from "./../channel_type/ChannelTypeSelect.vue";
import DeviceCategorySelect from "./../device_category/DeviceCategorySelect.vue";
import RegionSelect from "./../region/RegionSelect.vue";
import SalesAreaSelect from "./../sales_area/SalesAreaSelect.vue";
import FuelTankTypeSelect from "./../fuel_tank_type/FuelTankTypeSelect.vue";
import ImageInput from "./ImageInput.vue";
import MultipleImageInput from "./MultipleImageInput.vue";
import ProductSelect from "../product/ProductSelect.vue";
import BrandSelect from "../brand/BrandSelect.vue";
import DatePicker from "./DatePicker.vue";
import TimePicker from "./TimePicker.vue";
import RoleSelect from "./../role/RoleSelect.vue";
import ProvinceSelect from "../province/ProvinceSelect.vue";
import CitySelect from "../city/CitySelect.vue";
import DistrictSelect from "../district/DistrictSelect.vue";
import PasswordField from "./PasswordField.vue";
import UserSelect from "../user/UserSelect.vue";
import SurveyorUserSelect from "../surveyor/SurveyorUserSelect.vue";
import SupervisorUserSelect from "../supervisor/SupervisorUserSelect.vue";
import FileInput from "./FileInput.vue";
const props = defineProps({
schemas: {
default: [],
},
formValue: {
type: Object,
default: {},
},
});
const schemaTypeToComponent = {
text: TextField,
password: PasswordField,
select: SelectField,
"representative-select": RepresentativeSelect,
"brand-select": BrandSelect,
"retail-channel-select": RetailChannelSelect,
"channel-type-select": ChannelType,
"device-category-select": DeviceCategorySelect,
"region-select": RegionSelect,
"sales-area-select": SalesAreaSelect,
"product-select": ProductSelect,
"fuel-tank-type-select": FuelTankTypeSelect,
"image-input": ImageInput,
"multiple-image-input": MultipleImageInput,
date: DatePicker,
time: TimePicker,
"role-select": RoleSelect,
"province-select": ProvinceSelect,
"city-select": CitySelect,
"district-select": DistrictSelect,
textarea: TextArea,
number: NumberField,
decimal: DecimalField,
checkbox: Checkbox,
"user-select": UserSelect,
"surveyor-user-select": SurveyorUserSelect,
"supervisor-user-select": SupervisorUserSelect,
"file-input": FileInput,
};
</script>
<template>
<div class="card">
<div class="row" style="min-height: 400px">
<div class="col-md-8">
<div class="card-header toolbar">
<div class="toolbar-start">
<template v-if="isCanBack == true">
<button class="btn btn-light" @click="$router.go(-1)">
<i class="pli-back fs-5"></i>
<span class="d-none d-lg-inline-block ms-lg-2">Back</span>
</button>
</template>
<slot name="action-e" :item="content"> </slot>
</div>
</div>
<div class="card-body h-100 px-xl-3">
<div class="list-group list-group-borderless gap-0 mb-0">
<template v-for="item in schema">
<slot v-if="item.type != 'map'" :name="item.value" v-bind="content">
<template v-if="item.type != 'section'">
<ListField :label="item.label" :value="valueResolver(content, item.value, '.') ?? '-'"></ListField>
</template>
<template v-if="item.type == 'section'">
<div class="mb-4">
<h3 id="section-title">{{ item.label }}</h3>
<hr />
<template v-for="sectionItem in item.children">
<ListField :label="sectionItem.label" :value="valueResolver(content, sectionItem.value, '.') ?? '-'
"></ListField>
</template>
</div>
</template>
</slot>
</template>
</div>
</div>
</div>
<template v-for="item in schema">
<div v-if="item.type == 'map'" :class="`col-md-${item.cols != null ? item.cols : 4}`">
<div class="card-body bg-light h-100 rounded text-white p-2">
<MapViewer :long="valueResolver(content, item.long, '.') ?? 0"
:lat="valueResolver(content, item.lat, '.') ?? 0" />
</div>
</div>
</template>
<div v-if="isExist == false" :class="`col-md-4 d-none d-lg-inline-block`">
<div
class="card-body bg-gray-600 h-100 rounded text-white text-center d-flex align-items-center justify-content-center">
<i class="pli-letter-open" style="font-size: 200px"></i>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropertySchema } from "@/types/property_schema";
const props = defineProps({
schema: {
type: Array<PropertySchema>,
default: [],
},
content: {
type: Object,
default: {},
},
isCanBack: {
type: Boolean,
default: true,
},
});
let isExist = false;
const exist = () => {
props.schema.forEach((element) => {
if (element.type == "map") {
isExist = true;
}
});
};
const valueResolver = (obj: any, path: any, separator = ".") => {
try {
var properties = path.split(separator);
return properties.reduce((prev: any, curr: any) => prev && prev[curr], obj);
} catch (err) {
return null;
}
};
onBeforeMount(() => {
exist();
});
</script>
<template>
<div class="col border mx-2 rounded px-2 py-2">
<label v-if="label" :for="`${name}-input`" class="form-label">{{
label
}}</label>
<input
ref="fileInput"
:id="`${name}-input`"
:name="name"
:label="label"
@change="handleFileChange"
type="file"
style="display: none"
:placeholder="label"
@blur="handleBlur"
/>
<div class="mt-2">
<template v-if="value">
<a :href="value" target="_blank" class="btn btn-primary btn-block btn-rounded">Lihat File</a>
</template>
<div class="d-flex mt-2">
<button
type="button"
class="btn btn-light btn-sm btn rounded btn border btn-block"
@click="openFilePicker"
>
Select File
</button>
<button
v-if="canRemove"
style="width: 100px"
type="button"
class="btn btn-danger btn-sm btn rounded btn border btn-block ms-2"
@click="handleFileRemove"
>
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<div class="text-danger">{{ errorMessage }}</div>
<slot name="additional-field"> </slot>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { ref } from "vue";
const emit = defineEmits(["update:modelValue", "remove", "change"]);
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: false,
default: null,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: null,
},
canRemove: {
default: false,
type: Boolean,
},
});
const existingPhoto = ref(null);
const { value, errorMessage, handleBlur } = props.name
? useField(() => props.name, props.rules, {
syncVModel: true,
})
: { value: ref(props.modelValue), errorMessage: null, handleBlur: null };
const fileInput = ref(null);
const openFilePicker = () => {
fileInput.value.click();
};
const handleFileChange = (event) => {
value.value = event.target.files[0];
emit("change", value.value);
};
const isBlob = (variable) => {
return variable instanceof Blob;
};
const createObjectURL = (obj) => {
return URL.createObjectURL(obj);
};
const openFile = (file) => {
window.open(file);
};
const handleFileRemove = () => {
emit("remove");
};
</script>
<template>
<div class="col border mx-2 rounded px-2 py-2">
<label v-if="label" :for="`${name}-input`" class="form-label">{{
label
}}</label>
<input
ref="fileInput"
:id="`${name}-input`"
:name="name"
:label="label"
@change="handleFileChange"
type="file"
style="display: none"
:placeholder="label"
@blur="handleBlur"
/>
<div class="mt-2">
<template v-if="value">
<template v-if="isBlob(value)">
<img
style="height: 200px; width: 100%; object-fit: cover"
:src="createObjectURL(value)"
/>
</template>
<template v-else>
<img
style="height: 200px; width: 100%; object-fit: cover"
:src="value"
/>
</template>
</template>
<!-- <div v-if="typeof value != 'string'" class="mb-1">
{{ value ? value.name : "" }}
</div> -->
<div class="d-flex mt-2">
<button
type="button"
class="btn btn-light btn-sm btn rounded btn border btn-block"
@click="openFilePicker"
>
Select File
</button>
<button
v-if="canRemove"
style="width: 100px"
type="button"
class="btn btn-danger btn-sm btn rounded btn border btn-block ms-2"
@click="handleFileRemove"
>
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<div class="text-danger">{{ errorMessage }}</div>
<slot name="additional-field"> </slot>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { ref } from "vue";
const emit = defineEmits(["update:modelValue", "remove", "change"]);
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: false,
default: null,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: null,
},
canRemove: {
default: false,
type: Boolean,
},
});
const existingPhoto = ref(null);
const { value, errorMessage, handleBlur } = props.name
? useField(() => props.name, props.rules, {
syncVModel: true,
})
: { value: ref(props.modelValue), errorMessage: null, handleBlur: null };
const fileInput = ref(null);
const openFilePicker = () => {
fileInput.value.click();
};
const handleFileChange = (event) => {
value.value = event.target.files[0];
emit("change", value.value);
};
const isBlob = (variable) => {
return variable instanceof Blob;
};
const createObjectURL = (obj) => {
return URL.createObjectURL(obj);
};
const openFile = (file) => {
window.open(file);
};
const handleFileRemove = () => {
emit("remove");
};
</script>
<template>
<div class="list-group-item list-group-item-action d-flex align-items-center">
<div class="d-flex flex-column flex-md-row flex-shrink-0 gap-3 align-items-center">
<div class="text-decoration-none text-dark"><i class="pli-information fs-5"></i></div>
</div>
<div class="flex-fill min-w-0 ms-3">
<div class="d-flex flex-wrap flex-xl-nowrap align-items-xl-center">
<div class="w-md-160px flex-shrink-0">
<div class="h6 fw-normal m-0 text-decoration-none">{{ label }}</div>
</div>
<div class="flex-fill w-100 min-w-0 mt-2 mt-xl-0 mx-xl-3 order-xl-2">
<div class="d-block h6 fw-normal m-0 text-decoration-none text-truncate">
{{ value == null || value == "" ? '-' : value }}
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.list-group-item {
border: none;
}
</style>
<script setup lang="ts">
const props = defineProps({
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
});
</script>
<template>
<div class="mb-5">
<div class="col">
<!-- {{ value == }} -->
<label :for="`${name}-input`" class="form-label">{{ label }} <span v-if="errorMessage" class="text-red">({{ errorMessage }}) </span></label>
<div class="row align-items-end">
<div v-for="(image, index) in value" class="col-md-3 col-sm-6 col-12 mb-3" v-if="value">
<FileInput :key="value[index]['url']" :can-remove="value.length > 1 || index != 0"
v-model="value[index]['url']" @remove="handleRemoveFile(index)"
@change="(file) => handleFileChange(index, file)"></FileInput>
</div>
</div>
<div class="d-flex justify-content-end">
<div>
<button @click="handleAddFile" type="button" class="btn btn-primary">
Tambah File
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { ref } from "vue";
const emit = defineEmits(["update:modelValue", "removeDocument"]);
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: null,
},
});
const { value, errorMessage, handleBlur } = useField(
() => `${props.name}`,
props.rules,
{
syncVModel: true,
}
);
const handleFileChange = (index, file) => {
if (!value.value) {
value.value = { id: null, url: file };
return;
}
emit("removePhoto", value.value[index]['id'])
value.value[index]['id'] = null;
value.value[index]['url'] = file;
};
const handleAddFile = () => {
if(!value.value) {
value.value = [{id: null, url: null}]
return
}
value.value.push({ id: null, url: null });
};
const handleRemoveFile = (index) => {
if (!value.value) {
return;
}
if (value.value[index].id) {
emit("removeDocument", value.value[index].id)
}
value.value.splice(index, 1);
};
</script>
\ No newline at end of file
<template>
<div class="mb-5">
<div class="col">
<!-- {{ value == }} -->
<label :for="`${name}-input`" class="form-label">{{ label }} <span v-if="errorMessage" class="text-red">({{ errorMessage }}) </span></label>
<div class="row align-items-end">
<div v-for="(image, index) in value" class="col-md-3 col-sm-6 col-12 mb-3" v-if="value">
<ImageInput :key="value[index]['url']" :can-remove="value.length > 1 || index != 0"
v-model="value[index]['url']" @remove="handleRemoveImage(index)"
@change="(file) => handleFileChange(index, file)"></ImageInput>
</div>
</div>
<div class="d-flex justify-content-end">
<div>
<button @click="handleAddImage" type="button" class="btn btn-primary">
Tambah Gambar
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { ref } from "vue";
const emit = defineEmits(["update:modelValue", "removePhoto"]);
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: null,
},
});
const { value, errorMessage, handleBlur } = useField(
() => `${props.name}`,
props.rules,
{
syncVModel: true,
}
);
const handleFileChange = (index, file) => {
if (!value.value) {
value.value = { id: null, url: file };
return;
}
emit("removePhoto", value.value[index]['id'])
value.value[index]['id'] = null;
value.value[index]['url'] = file;
};
const handleAddImage = () => {
if(!value.value) {
value.value = [{id: null, url: null}]
return
}
value.value.push({ id: null, url: null });
};
const handleRemoveImage = (index) => {
if (!value.value) {
return;
}
if (value.value[index].id) {
emit("removePhoto", value.value[index].id)
}
value.value.splice(index, 1);
};
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<input
:id="`${name}-input`"
:name="name"
:label="label"
v-model="value"
type="number"
class="form-control"
:placeholder="label"
@blur="handleBlur"
/>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
});
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
\ No newline at end of file
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<div class="input-group">
<input
:id="`${name}-input`"
:name="name"
:label="label"
v-model="value"
:type="isPasswordShown ? 'text' : 'password'"
class="form-control"
:placeholder="label"
@blur="handleBlur"
/>
<span class="input-group-text">
<i
@click="isPasswordShown = !isPasswordShown"
:class="`fa ${isPasswordShown ? 'fa-eye' : 'fa-eye-slash'}`"
></i>
</span>
</div>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
});
const isPasswordShown = ref(false);
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<p>{{ label }}</p>
<h5>{{ value == null || value == "" ? '-' : value }}</h5>
</template>
<script setup lang="ts">
const props = defineProps({
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
});
</script>
<template>
<div class="col px-1 mt-1">
<label v-if="!hideLabel" :for="`${name}-input`" class="form-label">{{
label
}}</label>
<Multiselect v-model="value" :name="name" :items="options" :disabled="readonly" :placeholder="label"
:options="options" :loading="loading" :searchable="true" :mode="mode != null ? mode : 'single'">
</Multiselect>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { toRef } from "vue";
import { useField } from "vee-validate";
import Multiselect from "@vueform/multiselect";
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
optionText: {
type: String,
default: null,
},
optionValue: {
type: String,
default: null,
},
options: {
type: Array,
default: [],
},
loading: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
mode: {
type: String,
required: false,
default: "single",
},
hideLabel: {
default: false,
},
rules: {
default: undefined
}
});
const name = toRef(props, "name");
const options = computed(() =>
props.options.map((option) => {
if (typeof option != "object") {
return option;
}
return {
value: option[props.optionValue],
label: option[props.optionText],
};
})
);
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<textarea
:id="`${name}-input`"
:name="name"
:label="label"
v-model="value"
type="text"
:rows="rows"
class="form-control"
:placeholder="label"
@blur="handleBlur"
/>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
rows: {
type: Number,
default:5
}
});
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<input
:id="`${name}-input`"
:name="name"
:label="label"
:readonly="readonly"
v-model="value"
type="text"
class="form-control"
:placeholder="label"
@blur="handleBlur"
/>
<div class="text-danger">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
default: null,
},
type: {
type: String,
default: "text",
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
default: undefined,
},
});
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<div class="col">
<label :for="`${name}-input`" class="form-label">{{ label }}</label>
<VueDatePicker
v-model="value"
:name="name"
:disabled="readonly"
:placeholder="label"
time-picker
format="HH:mm"
model-type="HH:mm"
></VueDatePicker>
<div class="text-red">{{ errorMessage }}</div>
</div>
</template>
<script setup>
import { toRef } from "vue";
import { useField } from "vee-validate";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
const props = defineProps({
modelValue: {
default: null,
},
name: {
type: String,
required: false,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
readonly: {
type: Boolean,
default: false,
},
});
const name = toRef(props, "name");
const { value, errorMessage, handleBlur } = useField(
() => props.name,
props.rules,
{
syncVModel: true,
}
);
</script>
<template>
<BasePage>
<div class="card">
<div class="card-header pb-0">
<h6>{{ title }}</h6>
<hr />
</div>
<DynamicForm
ref="createForm"
:schemas="schemas"
@formSubmit="handleSubmit"
:form-value="formValue"
>
<template v-for="(schema, index) in schemas" #[schema.name]="values">
<slot :name="schema.name" v-bind="values"></slot>
</template>
</DynamicForm>
</div>
</BasePage>
</template>
<script setup>
import { ref, onBeforeMount } from "vue";
import BasePage from "./BasePage.vue";
import DynamicForm from "./../Form/DynamicForm.vue";
import { Swal, showLoading, hideLoading } from "@/utils/alert.js";
const props = defineProps({
schemas: {
type: Array,
default: [],
},
title: {
type: String,
default: "",
},
api: {},
});
const createForm = ref(null);
const handleSubmit = async (payload, { resetForm }) => {
// console.log(payload)
// return
try {
showLoading();
const response = await props.api.store(payload);
hideLoading();
Swal.fire("Sukses", "", "success");
resetForm();
emit("created", response);
} catch (err) {
console.log(err);
if (err.response && err.response.status == 422) {
createForm.value.setError(err.response.data);
hideLoading();
return;
}
hideLoading();
Swal.fire("Error", "Terjadi Error", "error");
}
};
const formValue = ref({});
onBeforeMount(async () => {
const initialValue = {};
props.schemas.forEach((schema) => {
if (schema.type == "checkbox") {
initialValue[schema.name] = false;
}
});
formValue.value = initialValue;
});
</script>
......@@ -43,41 +43,6 @@ onBeforeMount(() => {
</sidenav-parent>
</li>
</template>
<!-- <li class="nav-item">
<sidenav-item
to="/dashboard"
:class="getRoute() === 'dashboard' ? 'active' : ''"
:navText="isRTL ? 'لوحة القيادة' : 'Dashboard'"
>
<template v-slot:icon>
<i class="ni ni-gauge-3 text-primary text-sm opacity-10"></i>
</template>
</sidenav-item>
</li>
<li class="nav-item">
<sidenav-item
to="/tables"
:class="getRoute() === 'tables' ? 'active' : ''"
:navText="isRTL ? 'الجداول' : 'Tables'"
>
<template v-slot:icon>
<i class="ni ni-laptop text-warning text-sm opacity-10"></i>
</template>
</sidenav-item>
</li>
<li class="nav-item">
<sidenav-item
to="/billing"
:class="getRoute() === 'billing' ? 'active' : ''"
:navText="isRTL ? 'الفواتیر' : 'Billing'"
>
<template v-slot:icon>
<i class="ni ni-user text-success text-sm opacity-10"></i>
</template>
</sidenav-item>
</li> -->
</ul>
</div>
</template>
import UserProfile from "./../views/user/UserProfile.vue";
import UserIndex from "./../views/user/UserIndex.vue";
import UserCreate from "@/views/user/UserCreate.vue";
export const userRoutes = [
{
path: "/user",
name: "user-index",
children: [
{
path: "",
name: "index",
component: UserIndex,
},
{
path: "/user/create",
name: "user-create",
component: UserIndex,
name: "create",
component: UserCreate,
},
{
path: "/user/:id/edit",
name: "user-edit",
name: "edit",
component: UserIndex,
},
{
path: "/user/profile",
name: "user-profile",
name: "profile",
component: UserProfile,
},
],
},
];
<template>
<form-page title="Tambah User" :schemas="schemas"></form-page>
</template>
<script setup>
import * as Yup from "yup";
import FormPage from "@/components/Page/FormPage.vue";
const schemas = [
{
label: "Nama User",
name: "name",
validation: Yup.string().required().label("Nama User"),
cols: 6,
},
{
label: "Email",
name: "email",
validation: Yup.string().required().label("Email User"),
cols: 6,
},
{
label: "username",
name: "username",
validation: Yup.string().required().label("Username"),
cols: 6,
},
{
label: "Password User",
name: "password",
validation: Yup.string().required().label("Password User"),
cols: 6,
type: "password"
},
];
</script>
......@@ -104,7 +104,6 @@
</template>
<script setup lang="ts">
import BasePage from "./../../components/Page/BasePage.vue";
import ProjectsTable from "./../components/ProjectsTable.vue";
import { UserApi } from "@/api/user.js";
import ServerDatatable from "@/components/Common/ServerDatatable.vue";
import { ref, onMounted, inject } from "vue";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment