Files
BarangaySystem/resources/js/Pages/CreateUser.vue
2026-06-06 18:43:00 +08:00

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>