477 lines
14 KiB
Vue
477 lines
14 KiB
Vue
<script setup>
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
usePageTitle('Edit User');
|
|
|
|
import { ref, computed, onMounted, getCurrentInstance, watch } from 'vue'
|
|
import axios from 'axios'
|
|
import { useNavigate } from '../composables/Core/useNavigate'
|
|
import { useModal } from '../composables/Core/useModal'
|
|
import { useUserStore } from '../stores/user'
|
|
// No imports needed - we use native browser functions for decoding
|
|
|
|
const { navigate } = useNavigate()
|
|
import BackButton from '../Components/Core/BackButton.vue'
|
|
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('');
|
|
|
|
// Validation states
|
|
const isMobileValid = ref(false)
|
|
const isUsernameValid = ref(false)
|
|
const isLoading = ref(false)
|
|
|
|
// Data lists
|
|
const userTypeList = ref([])
|
|
const parentList = ref([])
|
|
|
|
// Route parameters
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const userId = ref(null) // Defined below in onMounted or from props
|
|
|
|
// Function to extract hashkey from URL path
|
|
function getHashkeyFromUrl() {
|
|
const pathParts = window.location.pathname.split('--h:')
|
|
if (pathParts.length >= 2) {
|
|
// The second part contains the encoded hashkey after '--h:'
|
|
return pathParts[1]
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Function to extract and decode hashkey from URL path
|
|
function extractHashkeyFromUrl() {
|
|
// Get the full URL path
|
|
const urlPath = window.location.pathname
|
|
|
|
// Check for hash format: --h:HASHKEY (the hashkey is encoded)
|
|
if (!urlPath.includes('--h:')) return null
|
|
|
|
try {
|
|
// The URL format is /edituser--h:ENCODED_HASHKEY
|
|
// We need to extract the part after --h:
|
|
const parts = urlPath.split('--h:')
|
|
if (parts.length < 2) return null
|
|
|
|
const encodedHash = parts[1]
|
|
|
|
// Decode the hashkey - it was encoded as base64 in useUrlEncoder.js encodeHash
|
|
// The format is h:ENCODED_HASHKEY where ENCODED_HASHKEY is base64 encoded
|
|
if (!encodedHash) return null
|
|
|
|
// Remove 'h:' prefix if present and decode from base64
|
|
let base64 = encodedHash.startsWith('h:') ? encodedHash.substring(2) : encodedHash
|
|
|
|
try {
|
|
const decoded = atob(base64)
|
|
// Decode the URI components
|
|
const result = decodeURIComponent(decoded)
|
|
return result
|
|
} catch (e) {
|
|
console.error('[EditUser] Error decoding hashkey:', e)
|
|
return null
|
|
}
|
|
} catch (e) {
|
|
console.error('[EditUser] Error extracting hashkey from URL:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Initialize component
|
|
onMounted(async () => {
|
|
document.title = 'Edit User'
|
|
|
|
const instance = getCurrentInstance()
|
|
const props = instance?.proxy?.$attrs || {}
|
|
|
|
// Check for hashkey in multiple places:
|
|
// 1. URL path (for direct URL access like /edituser--h:HASHKEY)
|
|
// 2. Props from navigation
|
|
// 3. URL query parameters
|
|
let targetId = extractHashkeyFromUrl()
|
|
|
|
if (!targetId) {
|
|
targetId = props.hashkey || props.id || urlParams.get('userId') || urlParams.get('id')
|
|
}
|
|
|
|
if (!targetId) {
|
|
console.error('User ID not found')
|
|
navigate({ page: 'UserList' })
|
|
return
|
|
}
|
|
|
|
userId.value = targetId
|
|
|
|
populateUserTypeList()
|
|
await populateParentList()
|
|
await loadUserData()
|
|
})
|
|
|
|
// Check if username exists (for validation)
|
|
const checkUsernameExists = async (usernameValue) => {
|
|
if (!usernameValue || usernameValue === '') {
|
|
isUsernameValid.value = true
|
|
return false
|
|
}
|
|
|
|
try {
|
|
isLoading.value = true
|
|
const response = await axios.post('/admin/user/username/exists', { username: usernameValue })
|
|
|
|
if (response.data && response.data.exists === true) {
|
|
isUsernameValid.value = false
|
|
return false
|
|
} else if (response.data.exists === false) {
|
|
isUsernameValid.value = true
|
|
return true
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking username existence:', error)
|
|
isUsernameValid.value = false
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 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', {
|
|
exclude_user: userId.value
|
|
})
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Load user data
|
|
const loadUserData = async () => {
|
|
if (!userId.value) return
|
|
|
|
try {
|
|
isLoading.value = true
|
|
|
|
const response = await axios.post('/admin/user/details', {
|
|
target_user: userId.value
|
|
})
|
|
|
|
// Handle the response - backend returns user data directly without success wrapper
|
|
let userData
|
|
|
|
// Check for different possible response formats
|
|
if (response.data && typeof response.data === 'object') {
|
|
// Response might be wrapped in 'user' key or direct object
|
|
userData = response.data.user || response.data
|
|
|
|
mobileNumber.value = userData.mobile_number || ''
|
|
nickname.value = userData.nickname || ''
|
|
name.value = userData.name || ''
|
|
username.value = userData.username || ''
|
|
fullname.value = userData.fullname || ''
|
|
userType.value = userData.acct_type || ''
|
|
|
|
// Use parent_hashkey from backend to directly set the dropdown value
|
|
if (userData.parent_hashkey) {
|
|
parent.value = userData.parent_hashkey
|
|
} else {
|
|
parent.value = ''
|
|
}
|
|
} else {
|
|
console.error('Failed to load user data')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading user data:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// Check if current user is ultimate account type
|
|
const isCurrentUserIdentityUltimate = computed(() => userStore.acctType === 'ult')
|
|
|
|
// PH-format check (09XXXXXXXXX). Used only to detect PH attempts that look wrong;
|
|
// non-PH values (e.g. internal codes like "777", foreign numbers) are allowed as-is.
|
|
const looksLikePhAttempt = (number) => {
|
|
const digits = String(number).replace(/\D+/g, '')
|
|
// 10+ digits starting with 9 or 0 or "63" — user is clearly trying to enter a PH mobile
|
|
return /^(0?9\d{0,}|639\d{0,})$/.test(digits) && digits.length >= 10
|
|
}
|
|
|
|
const hasValidMobileNumberFormat = (number) => {
|
|
const value = String(number || '').trim()
|
|
if (!value) return false
|
|
|
|
// Admins may store non-PH identifiers as-is.
|
|
if (isCurrentUserIdentityUltimate.value) return true
|
|
|
|
// For non-ultimate editors: if it looks like a PH attempt, enforce canonical 09XXXXXXXXX.
|
|
if (looksLikePhAttempt(value)) {
|
|
return /^09\d{9}$/.test(value)
|
|
}
|
|
|
|
// Otherwise (short codes, foreign numbers), accept as-is.
|
|
return true
|
|
}
|
|
|
|
const validateMobileNumberBeforeUpdate = () => {
|
|
if (!hasValidMobileNumberFormat(mobileNumber.value)) {
|
|
modal.open({
|
|
title: 'Error',
|
|
body: 'Please enter a valid Philippine mobile number (09XXXXXXXXX format) or a non-PH identifier.',
|
|
footer: null
|
|
})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Show confirmation modal for update
|
|
const showConfirmationModal = () => {
|
|
// Validate mobile number format first
|
|
if (!validateMobileNumberBeforeUpdate()) {
|
|
return
|
|
}
|
|
|
|
modal.yesNoModal({
|
|
title: 'Update User?',
|
|
body: 'Are you sure you want to update this user?',
|
|
onYes: updateUser,
|
|
yesText: 'Update',
|
|
noText: 'Cancel'
|
|
})
|
|
}
|
|
|
|
// Update the user
|
|
const updateUser = async () => {
|
|
try {
|
|
isLoading.value = true
|
|
|
|
const response = await axios.post('/admin/user/details/update', {
|
|
target_user: userId.value,
|
|
details: {
|
|
mobile_number: mobileNumber.value,
|
|
nickname: nickname.value,
|
|
name: name.value,
|
|
username: username.value,
|
|
fullname: fullname.value,
|
|
type: userType.value,
|
|
parent: parent.value
|
|
}
|
|
})
|
|
|
|
if (response.data && response.data.success) {
|
|
showSuccessModal()
|
|
} else if (response.data === true) {
|
|
showSuccessModal()
|
|
} else {
|
|
showErrorModal('User was not updated.')
|
|
}
|
|
} catch (error) {
|
|
console.error('Update error:', error)
|
|
showErrorModal(error.response?.data?.message || error.response?.data || 'Error updating user')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// Show success modal with quick dismiss (auto-closes after delay)
|
|
const showSuccessModal = () => {
|
|
// First close any existing modal
|
|
modal.close()
|
|
|
|
// Proactively prefetch users list
|
|
userStore.fetchUsers()
|
|
|
|
// Open success modal without buttons (just title/body)
|
|
modal.open({
|
|
title: 'Success',
|
|
body: 'User Updated Successfully'
|
|
})
|
|
|
|
// Auto-dismiss after 2 seconds
|
|
setTimeout(() => {
|
|
modal.close()
|
|
navigate({ page: 'UserList' })
|
|
}, 2000)
|
|
}
|
|
|
|
// Show error modal
|
|
const showErrorModal = (message) => {
|
|
modal.open({
|
|
title: 'Failed',
|
|
body: `Error Updating User. ${message}`,
|
|
footer: null
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="edit-user-page pb-5">
|
|
<br><br>
|
|
|
|
<div class="tf-container">
|
|
<!-- Back Button -->
|
|
<div class="mb-4">
|
|
<BackButton to="UserList" />
|
|
</div>
|
|
<h2 class="fw_6 text-center mb-4">Edit 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="isLoading && !isMobileValid" class="fas fa-spinner fa-spin"></i>
|
|
<i v-else-if="isMobileValid" class="fas fa-check text-white"></i>
|
|
</span>
|
|
<input
|
|
type="tel"
|
|
inputmode="tel"
|
|
id="usernumber"
|
|
class="form-control"
|
|
placeholder="Mobile Number (e.g., 09123456789)"
|
|
v-model.trim="mobileNumber"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nickname -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isUsernameValid ? 'bg-success' : ''"></span>
|
|
<input
|
|
type="text"
|
|
id="nickname"
|
|
class="form-control"
|
|
placeholder="Nick Name (Optional)"
|
|
v-model.trim="nickname"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Name -->
|
|
<div class="col-12">
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
class="form-control"
|
|
placeholder="Name"
|
|
v-model.trim="name"
|
|
:disabled="isLoading"
|
|
>
|
|
</div>
|
|
|
|
<!-- Username -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text"></span>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
class="form-control"
|
|
placeholder="Username (Optional)"
|
|
v-model.trim="username"
|
|
:disabled="isLoading"
|
|
>
|
|
</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
|
|
: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 || parentList.length === 0"
|
|
>
|
|
<option value="" disabled>Select Parent (Optional)</option>
|
|
<option v-for="parentUser in parentList" :key="parentUser.value" :value="parentUser.value">
|
|
{{ parentUser.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Update Button -->
|
|
<div class="col-12">
|
|
<button
|
|
id="UpdateUserButton"
|
|
class="btn btn-primary w-100 py-2"
|
|
@click="showConfirmationModal"
|
|
:disabled="isLoading"
|
|
>
|
|
Update User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.card {
|
|
border-radius: 12px;
|
|
}
|
|
</style> |