687 lines
19 KiB
Vue
687 lines
19 KiB
Vue
<script setup>
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
usePageTitle('Create User');
|
|
|
|
import { ref, computed, onMounted, h } from 'vue'
|
|
import axios from 'axios'
|
|
import { useNavigate } from '../composables/Core/useNavigate'
|
|
import { useModal } from '../composables/Core/useModal'
|
|
import { useUserStore } from '../stores/user'
|
|
|
|
const { navigate } = useNavigate()
|
|
const modal = useModal()
|
|
const userStore = useUserStore()
|
|
|
|
// Form state
|
|
const mobileNumber = ref('')
|
|
const nickname = ref('')
|
|
const name = ref('')
|
|
const username = ref('')
|
|
const fullname = ref('')
|
|
const userType = ref('')
|
|
const parent = ref('')
|
|
const password = ref('')
|
|
const confirmPassword = ref('')
|
|
|
|
// Validation states
|
|
const isMobileValid = ref(false)
|
|
const isUsernameValid = ref(false)
|
|
const isPasswordValid = ref(false)
|
|
const isConfirmPasswordValid = ref(false)
|
|
|
|
const isNameValid = computed(() => name.value.trim().length > 0)
|
|
const isUserTypeValid = computed(() => userType.value !== '')
|
|
|
|
// Computed for missing fields
|
|
const missingFields = computed(() => {
|
|
const fields = []
|
|
|
|
if (!mobileNumber.value) {
|
|
fields.push('Mobile Number')
|
|
} else if (!isMobileValid.value) {
|
|
if (isCheckingMobile.value) {
|
|
fields.push('Checking Mobile Number...')
|
|
} else if (!hasValidMobileNumberFormat(mobileNumber.value)) {
|
|
fields.push('Valid Mobile Number format (09XXXXXXXXX)')
|
|
} else {
|
|
fields.push('Unique/Available Mobile Number')
|
|
}
|
|
}
|
|
|
|
if (!name.value.trim()) fields.push('Name')
|
|
|
|
if (!username.value.trim()) {
|
|
fields.push('Username')
|
|
} else if (!isUsernameValid.value) {
|
|
if (isCheckingUsername.value) {
|
|
fields.push('Checking Username...')
|
|
} else {
|
|
fields.push('Unique/Available Username')
|
|
}
|
|
}
|
|
|
|
if (!userType.value) fields.push('User Type')
|
|
|
|
if (!parent.value) fields.push('Parent (Upline/Direct)')
|
|
|
|
if (password.value.length < 6) fields.push('Password (min 6 characters)')
|
|
|
|
if (password.value !== confirmPassword.value || !confirmPassword.value) {
|
|
fields.push('Passwords matching')
|
|
}
|
|
|
|
return fields
|
|
})
|
|
|
|
// Data lists
|
|
const userTypeList = ref([])
|
|
const parentList = ref([])
|
|
|
|
// Loading state
|
|
const isLoading = ref(false)
|
|
const isCheckingMobile = ref(false)
|
|
const isCheckingUsername = ref(false)
|
|
const showSuccessState = ref(false)
|
|
const showSuccessAnimation = ref(false)
|
|
|
|
|
|
// Initialize the component
|
|
onMounted(async () => {
|
|
document.title = 'Create New User'
|
|
|
|
// Ensure we have current user profile
|
|
if (!userStore.user) {
|
|
await userStore.fetchCurrentUser()
|
|
}
|
|
|
|
populateUserTypeList()
|
|
await populateParentList()
|
|
|
|
// Default parent to current user's hashkey if available
|
|
if (userStore.user?.hashkey && !parent.value) {
|
|
parent.value = userStore.user.hashkey
|
|
}
|
|
})
|
|
|
|
// Check if current user is ultimate account type
|
|
const isCurrentUserIdentityUltimate = computed(() => userStore.acctType === 'ult')
|
|
|
|
// Validate mobile number format only for non-ultimate accounts (Philippine format: 09XXXXXXXXX)
|
|
const hasValidMobileNumberFormat = (number) => {
|
|
// If current user is ultimate, no validation needed
|
|
if (isCurrentUserIdentityUltimate.value) {
|
|
return true
|
|
}
|
|
|
|
const pattern = /^(09|\+639)\d{9}$/
|
|
return pattern.test(number)
|
|
}
|
|
|
|
// Debounce helper
|
|
const debounce = (fn, delay) => {
|
|
let timeoutId;
|
|
return (...args) => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => fn(...args), delay);
|
|
};
|
|
};
|
|
|
|
// Check if user exists
|
|
const checkUserExists = async () => {
|
|
const number = mobileNumber.value
|
|
if (number === '') {
|
|
isMobileValid.value = false
|
|
return false
|
|
}
|
|
|
|
if (!hasValidMobileNumberFormat(number)) {
|
|
isMobileValid.value = false
|
|
return false
|
|
}
|
|
|
|
try {
|
|
isCheckingMobile.value = true
|
|
const response = await axios.post('/admin/user/number/exists', { mobile_number: number })
|
|
|
|
if (response.data && response.data.exists === true) {
|
|
isMobileValid.value = false
|
|
return false
|
|
} else if (response.data.exists === false) {
|
|
isMobileValid.value = true
|
|
validateAllInputs()
|
|
return true
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking user existence:', error)
|
|
isMobileValid.value = false
|
|
} finally {
|
|
isCheckingMobile.value = false
|
|
}
|
|
}
|
|
|
|
const checkUsernameExists = async () => {
|
|
const usernameValue = username.value
|
|
|
|
// Username required
|
|
if (usernameValue === '') {
|
|
isUsernameValid.value = false
|
|
validateAllInputs()
|
|
return
|
|
}
|
|
|
|
try {
|
|
isCheckingUsername.value = true
|
|
const response = await axios.post('/admin/user/username/exists', { username: usernameValue })
|
|
|
|
if (response.data && response.data.exists === true) {
|
|
isUsernameValid.value = false
|
|
} else if (response.data.exists === false) {
|
|
isUsernameValid.value = true
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking username existence:', error)
|
|
isUsernameValid.value = false
|
|
} finally {
|
|
isCheckingUsername.value = false
|
|
}
|
|
}
|
|
|
|
// Check for missing fields and show modal
|
|
const handleFormSubmit = () => {
|
|
if (missingFields.value.length > 0) {
|
|
modal.open({
|
|
title: 'Missing Requirements',
|
|
body: h('div', { class: 'p-3' }, [
|
|
h('div', { class: 'd-flex align-items-center mb-3 text-warning' }, [
|
|
h('i', { class: 'fas fa-exclamation-circle fa-2x me-3' }),
|
|
h('span', { class: 'fw_7 h5 mb-0' }, 'Required Fields')
|
|
]),
|
|
h('p', { class: 'text-muted mb-3' }, 'Please complete the following missing or invalid fields:'),
|
|
h('div', { class: 'd-flex flex-wrap gap-2' },
|
|
missingFields.value.map(field =>
|
|
h('span', { class: 'badge bg-light text-danger border border-danger-subtle rounded-pill px-3 py-2 fw_6' }, field)
|
|
)
|
|
)
|
|
]),
|
|
footer: h('button', {
|
|
class: 'btn btn-primary w-100 py-3 rounded-xl fw_7 shadow-sm',
|
|
onClick: () => modal.close()
|
|
}, 'I Understand')
|
|
})
|
|
return
|
|
}
|
|
showConfirmationModal()
|
|
}
|
|
|
|
// Validate password
|
|
const validatePassword = () => {
|
|
if (password.value.length < 6) {
|
|
isPasswordValid.value = false
|
|
return false
|
|
}
|
|
isPasswordValid.value = true
|
|
validateAllInputs()
|
|
return true
|
|
}
|
|
|
|
// Validate confirm password
|
|
const validateConfirmPassword = () => {
|
|
if (password.value.length < 6 || confirmPassword.value.length < 6) {
|
|
isConfirmPasswordValid.value = false
|
|
return false
|
|
}
|
|
|
|
if (password.value !== confirmPassword.value) {
|
|
isConfirmPasswordValid.value = false
|
|
return false
|
|
}
|
|
|
|
isConfirmPasswordValid.value = true
|
|
validateAllInputs()
|
|
return true
|
|
}
|
|
|
|
// Validate all inputs
|
|
const validateAllInputs = () => {
|
|
// Logic preserved for reactive updates but button visibility no longer toggled by DOM
|
|
}
|
|
|
|
// Populate user type dropdown
|
|
const populateUserTypeList = async () => {
|
|
try {
|
|
const response = await axios.post('/admin/list/usertype/create', {})
|
|
if (response.data && Array.isArray(response.data)) {
|
|
userTypeList.value = response.data.map(item => ({
|
|
value: item[0],
|
|
label: item[1]
|
|
}))
|
|
}
|
|
} catch (error) {
|
|
console.error('Error populating user type list:', error)
|
|
}
|
|
}
|
|
|
|
// Populate parent dropdown
|
|
const populateParentList = async () => {
|
|
try {
|
|
const response = await axios.post('/admin/user/list/numbers/hash', {})
|
|
if (response.data && Array.isArray(response.data)) {
|
|
parentList.value = response.data.map(user => ({
|
|
value: user.hashkey,
|
|
label: `${user.name} (${user.mobile_number}) [${user.username}] ${user.fullname ?? ''}`
|
|
}))
|
|
}
|
|
} catch (error) {
|
|
console.error('Error populating parent list:', error)
|
|
} finally {
|
|
// Backend now handles including current user in hierarchy
|
|
if (userStore.user?.hashkey && !parent.value) {
|
|
parent.value = userStore.user.hashkey
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show confirmation modal
|
|
const showConfirmationModal = () => {
|
|
modal.yesNoModal({
|
|
title: 'Create New User?',
|
|
body: 'Are you sure you want to Create a New User?',
|
|
onYes: registerUser,
|
|
yesText: 'Register',
|
|
noText: 'Cancel'
|
|
})
|
|
}
|
|
|
|
// Register the user
|
|
const registerUser = async () => {
|
|
// Validate all inputs first
|
|
await checkUserExists()
|
|
|
|
if (!isMobileValid.value) {
|
|
modal.open({
|
|
title: 'Error',
|
|
body: 'Please enter a valid mobile number (09XXXXXXXXX format)',
|
|
footer: null
|
|
})
|
|
return
|
|
}
|
|
|
|
if (!validatePassword() || !validateConfirmPassword()) {
|
|
modal.open({
|
|
title: 'Error',
|
|
body: 'Password must be at least 6 characters and passwords must match',
|
|
footer: null
|
|
})
|
|
return
|
|
}
|
|
|
|
try {
|
|
isLoading.value = true
|
|
|
|
const response = await axios.post('/admin/user/create', {
|
|
mobile_number: String(mobileNumber.value),
|
|
password: password.value,
|
|
nickname: nickname.value,
|
|
type: userType.value,
|
|
parent: parent.value,
|
|
fullname: fullname.value,
|
|
name: name.value,
|
|
username: username.value
|
|
})
|
|
|
|
if (response.data && response.data.success) {
|
|
showSuccessState.value = true
|
|
showSuccessAnimation.value = true
|
|
|
|
userStore.fetchUsers()
|
|
|
|
setTimeout(() => {
|
|
showSuccessAnimation.value = false
|
|
navigate({ page: 'UserList' })
|
|
}, 2000)
|
|
} else {
|
|
showErrorModal('User was not created.')
|
|
}
|
|
} catch (error) {
|
|
console.error('Registration error:', error)
|
|
const data = error.response?.data
|
|
const messages = []
|
|
if (data?.errors && typeof data.errors === 'object') {
|
|
for (const fieldMessages of Object.values(data.errors)) {
|
|
if (Array.isArray(fieldMessages)) {
|
|
messages.push(...fieldMessages)
|
|
} else if (typeof fieldMessages === 'string') {
|
|
messages.push(fieldMessages)
|
|
}
|
|
}
|
|
}
|
|
if (data?.error) messages.push(data.error)
|
|
if (data?.message) messages.push(data.message)
|
|
showErrorModal(messages.length ? messages : ['Error creating user'])
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// Show success modal
|
|
const showSuccessModal = (hashKey) => {
|
|
// Proactively prefetch users list
|
|
userStore.fetchUsers()
|
|
|
|
modal.continueCancelModal({
|
|
title: 'Success',
|
|
body: 'User Created Successfully',
|
|
onContinue: () => {
|
|
navigate({ page: 'UserList' })
|
|
},
|
|
continueText: 'OK',
|
|
continueClass: 'btn btn-primary',
|
|
showCancel: false
|
|
})
|
|
}
|
|
|
|
// Show error modal
|
|
const showErrorModal = (message) => {
|
|
const messages = Array.isArray(message) ? message : [message]
|
|
modal.open({
|
|
title: 'Failed to Create User',
|
|
body: h('div', { class: 'p-3' }, [
|
|
h('div', { class: 'd-flex align-items-center mb-3 text-danger' }, [
|
|
h('i', { class: 'fas fa-exclamation-circle fa-2x me-3' }),
|
|
h('span', { class: 'fw_7 h5 mb-0' }, 'Please fix the following:')
|
|
]),
|
|
h('ul', { class: 'mb-0 ps-3' },
|
|
messages.map(msg => h('li', { class: 'text-danger fw_6 mb-1' }, msg))
|
|
)
|
|
]),
|
|
footer: null
|
|
})
|
|
}
|
|
|
|
// Handle form input changes
|
|
const handleMobileNumberChange = (event) => {
|
|
// Ensure mobile number stays as string to preserve leading zeros
|
|
const input = event.target || event
|
|
if (input && typeof input === 'object') {
|
|
mobileNumber.value = String(mobileNumber.value)
|
|
}
|
|
|
|
// Reset validity while typing if not empty, so the checkmark goes away
|
|
if (mobileNumber.value !== '') {
|
|
isMobileValid.value = false
|
|
}
|
|
|
|
debouncedCheckUserExists()
|
|
validateAllInputs()
|
|
}
|
|
|
|
const handleUsernameChange = () => {
|
|
// Reset validity while typing
|
|
if (username.value !== '') {
|
|
isUsernameValid.value = false
|
|
}
|
|
debouncedCheckUsernameExists()
|
|
}
|
|
|
|
const debouncedCheckUserExists = debounce(checkUserExists, 600)
|
|
const debouncedCheckUsernameExists = debounce(checkUsernameExists, 600)
|
|
|
|
const handlePasswordChange = () => {
|
|
validatePassword()
|
|
validateAllInputs()
|
|
}
|
|
|
|
const handleConfirmPasswordChange = () => {
|
|
validateConfirmPassword()
|
|
validateAllInputs()
|
|
}
|
|
|
|
// Clear form
|
|
const clearForm = () => {
|
|
mobileNumber.value = String('')
|
|
nickname.value = ''
|
|
name.value = ''
|
|
username.value = ''
|
|
fullname.value = ''
|
|
userType.value = ''
|
|
parent.value = userStore.user?.hashkey || ''
|
|
password.value = ''
|
|
confirmPassword.value = ''
|
|
|
|
isMobileValid.value = false
|
|
isUsernameValid.value = false
|
|
isPasswordValid.value = false
|
|
isConfirmPasswordValid.value = false
|
|
|
|
validateAllInputs()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="create-user-page pb-5">
|
|
<br><br>
|
|
|
|
<div class="tf-container">
|
|
<h2 class="fw_6 text-center mb-4">Create New User</h2>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<!-- Mobile Number -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isMobileValid ? 'bg-success' : ''">
|
|
<i v-if="isCheckingMobile" class="fas fa-spinner fa-spin"></i>
|
|
<i v-else-if="isMobileValid" class="fas fa-check text-white"></i>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="usernumber"
|
|
class="form-control"
|
|
placeholder="Mobile Number (e.g., 09123456789)"
|
|
v-model="mobileNumber"
|
|
@input="handleMobileNumberChange"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nickname -->
|
|
<div class="col-12">
|
|
<input
|
|
type="text"
|
|
id="nickname"
|
|
class="form-control"
|
|
placeholder="Nick Name (Optional)"
|
|
v-model.trim="nickname"
|
|
>
|
|
</div>
|
|
|
|
<!-- Name -->
|
|
<div class="col-12">
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
class="form-control"
|
|
placeholder="Name (Required)"
|
|
v-model.trim="name"
|
|
@input="validateAllInputs"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
|
|
<!-- Username -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isUsernameValid && username !== '' ? 'bg-success' : ''">
|
|
<i v-if="isCheckingUsername" class="fas fa-spinner fa-spin"></i>
|
|
<i v-else-if="isUsernameValid && username !== ''" class="fas fa-check text-white"></i>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
class="form-control"
|
|
placeholder="Username (Required)"
|
|
v-model.trim="username"
|
|
@input="handleUsernameChange"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fullname -->
|
|
<div class="col-12">
|
|
<input
|
|
type="text"
|
|
id="fullname"
|
|
class="form-control"
|
|
placeholder="Full Name"
|
|
v-model.trim="fullname"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
|
|
<!-- User Type -->
|
|
<div class="col-12">
|
|
<select
|
|
class="form-select"
|
|
id="usertype"
|
|
v-model="userType"
|
|
required
|
|
@change="validateAllInputs"
|
|
:disabled="isLoading || userTypeList.length === 0"
|
|
>
|
|
<option value="" disabled>Select User Type</option>
|
|
<option v-for="type in userTypeList" :key="type.value" :value="type.value">
|
|
{{ type.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Parent -->
|
|
<div class="col-12">
|
|
<select
|
|
class="form-select"
|
|
id="userparent"
|
|
v-model="parent"
|
|
:disabled="isLoading"
|
|
required
|
|
>
|
|
<option value="" disabled>Select Parent (Required)</option>
|
|
<option v-for="parentUser in parentList" :key="parentUser.value" :value="parentUser.value">
|
|
{{ parentUser.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="col-12">
|
|
<input
|
|
type="password"
|
|
id="userpassword"
|
|
class="form-control"
|
|
placeholder="Password (min 6 characters)"
|
|
v-model.trim="password"
|
|
@input="handlePasswordChange"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
|
|
<!-- Confirm Password -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isConfirmPasswordValid ? 'bg-success' : ''"></span>
|
|
<input
|
|
type="password"
|
|
id="userpasswordconfirm"
|
|
class="form-control"
|
|
placeholder="Confirm Password"
|
|
v-model.trim="confirmPassword"
|
|
@input="handleConfirmPasswordChange"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 mt-3">
|
|
<AnimatedButton
|
|
id="RegisterNowButtonVisible"
|
|
btnClass="btn btn-primary w-100 py-3 shadow-lg rounded-xl fw_7"
|
|
@click="handleFormSubmit"
|
|
:loading="isLoading"
|
|
:success="showSuccessState"
|
|
>
|
|
Create User Account
|
|
</AnimatedButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center mt-4">
|
|
<button class="btn btn-outline-secondary" @click="navigate({ page: 'UserList' })">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Animation Overlay -->
|
|
<div v-if="showSuccessAnimation" class="success-overlay">
|
|
<div class="text-center animate-bounce-in">
|
|
<LottiePlayer
|
|
path="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/11999b7bb57c.json"
|
|
:loop="false"
|
|
width="250px"
|
|
height="250px"
|
|
/>
|
|
<h2 class="fw_8 mt-4 text-primary headline-gradient">Welcome Aboard!</h2>
|
|
<p class="text-muted">The user account has been created successfully.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
.card {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.success-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.98);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
backdrop-filter: blur(15px);
|
|
}
|
|
|
|
:global(.dark-mode) .success-overlay {
|
|
background: rgba(18, 20, 24, 0.98);
|
|
}
|
|
|
|
.animate-bounce-in {
|
|
animation: bounce-in 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
}
|
|
|
|
@keyframes bounce-in {
|
|
0% { transform: scale(0.3); opacity: 0; }
|
|
50% { transform: scale(1.05); opacity: 1; }
|
|
70% { transform: scale(0.9); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
.headline-gradient {
|
|
background: linear-gradient(135deg, #42b983 0%, #2c3e50 100%);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.rounded-xl {
|
|
border-radius: 15px;
|
|
}
|
|
</style> |