initial: bootstrap from BukidBountyApp base
This commit is contained in:
477
resources/js/Pages/EditUser.vue
Normal file
477
resources/js/Pages/EditUser.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user