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

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>