199 lines
7.7 KiB
Vue
199 lines
7.7 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import axios from 'axios';
|
|
import { useChapters } from '../composables/useChapters.js';
|
|
import { useNavigate } from '../composables/Core/useNavigate.js';
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
|
|
usePageTitle('Create Member');
|
|
|
|
const { fetchOfficerScope, loading } = useChapters();
|
|
const { navigate } = useNavigate();
|
|
|
|
const ownChapter = ref(null);
|
|
const cooperative = ref(null);
|
|
|
|
const form = ref({
|
|
name: '',
|
|
username: '',
|
|
mobile_number: '',
|
|
password: '',
|
|
});
|
|
|
|
const fieldErrors = ref({});
|
|
const errorMessage = ref('');
|
|
const submitting = ref(false);
|
|
const done = ref(false);
|
|
|
|
const mobileError = ref('');
|
|
const mobileTaken = ref(false);
|
|
const usernameTaken = ref(false);
|
|
|
|
const validateMobile = (val) => {
|
|
if (!val) { mobileError.value = 'Mobile number is required.'; return false; }
|
|
if (!/^(09|\+639)\d{9}$/.test(val)) {
|
|
mobileError.value = 'Must be a valid Philippine mobile number (e.g. 09XXXXXXXXX).';
|
|
return false;
|
|
}
|
|
mobileError.value = '';
|
|
return true;
|
|
};
|
|
|
|
const checkMobile = async () => {
|
|
if (!validateMobile(form.value.mobile_number)) return;
|
|
try {
|
|
const res = await axios.post('/admin/user/number/exists', { mobile_number: form.value.mobile_number });
|
|
mobileTaken.value = !!res.data?.exists;
|
|
} catch (e) { /* ignore */ }
|
|
};
|
|
|
|
const checkUsername = async () => {
|
|
if (!form.value.username) { usernameTaken.value = false; return; }
|
|
try {
|
|
const res = await axios.post('/admin/user/username/exists', { username: form.value.username });
|
|
usernameTaken.value = !!res.data?.exists;
|
|
} catch (e) { /* ignore */ }
|
|
};
|
|
|
|
const canSubmit = computed(() =>
|
|
form.value.name &&
|
|
form.value.username &&
|
|
form.value.mobile_number &&
|
|
form.value.password &&
|
|
!mobileError.value &&
|
|
!mobileTaken.value &&
|
|
!usernameTaken.value &&
|
|
ownChapter.value?.hashkey
|
|
);
|
|
|
|
const submit = async () => {
|
|
if (submitting.value || !canSubmit.value) return;
|
|
fieldErrors.value = {};
|
|
errorMessage.value = '';
|
|
submitting.value = true;
|
|
try {
|
|
const res = await axios.post('/api/public/chapter/register', {
|
|
chapter_hash: ownChapter.value.hashkey,
|
|
name: form.value.name,
|
|
username: form.value.username,
|
|
mobile_number: form.value.mobile_number,
|
|
password: form.value.password,
|
|
});
|
|
if (res.data.success) {
|
|
done.value = true;
|
|
} else {
|
|
errorMessage.value = res.data.message || 'Failed to create member.';
|
|
}
|
|
} catch (err) {
|
|
if (err.response?.data?.errors) fieldErrors.value = err.response.data.errors;
|
|
else errorMessage.value = err.response?.data?.message || 'An error occurred.';
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
};
|
|
|
|
const reset = () => {
|
|
form.value = { name: '', username: '', mobile_number: '', password: '' };
|
|
fieldErrors.value = {};
|
|
errorMessage.value = '';
|
|
mobileTaken.value = false;
|
|
usernameTaken.value = false;
|
|
done.value = false;
|
|
};
|
|
|
|
onMounted(async () => {
|
|
const scope = await fetchOfficerScope();
|
|
ownChapter.value = scope?.own_chapter ?? null;
|
|
cooperative.value = scope?.cooperative ?? null;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container py-4" style="max-width: 560px;">
|
|
<h5 class="fw-bold mb-3"><i class="fas fa-user-plus me-2"></i>Create Member</h5>
|
|
|
|
<div v-if="loading && !ownChapter" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
</div>
|
|
|
|
<div v-else-if="!ownChapter" class="text-center py-5 text-muted">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-warning mb-2"></i>
|
|
<p>You are not assigned to a chapter, so you cannot create members.</p>
|
|
</div>
|
|
|
|
<div v-else-if="done" class="text-center py-5">
|
|
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
|
|
<h5 class="fw-bold">Member Created!</h5>
|
|
<p class="text-muted">The new member was added to <strong>{{ ownChapter.name }}</strong>.</p>
|
|
<button class="btn btn-outline-primary rounded-pill px-4 me-2" @click="reset">Add Another</button>
|
|
<button class="btn btn-primary rounded-pill px-4" @click="navigate({ page: 'Home' })">Done</button>
|
|
</div>
|
|
|
|
<div v-else class="info-card rounded-4 p-4">
|
|
<div class="assign-note rounded-3 p-2 mb-3 small">
|
|
<i class="fas fa-map-marker-alt me-1"></i>
|
|
Will be added to: <strong>{{ ownChapter.name }}</strong>
|
|
<span v-if="cooperative"> · {{ cooperative.name }}</span>
|
|
</div>
|
|
|
|
<div v-if="errorMessage" class="alert alert-danger rounded-3 small py-2">{{ errorMessage }}</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Full Name</label>
|
|
<input v-model="form.name" type="text" class="form-control rounded-pill"
|
|
:class="{ 'is-invalid': fieldErrors.name }" placeholder="Juan Dela Cruz" />
|
|
<div v-if="fieldErrors.name" class="invalid-feedback">{{ fieldErrors.name[0] }}</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Username</label>
|
|
<input v-model="form.username" type="text" class="form-control rounded-pill"
|
|
:class="{ 'is-invalid': usernameTaken || fieldErrors.username }"
|
|
placeholder="juandelacruz" autocomplete="off" @blur="checkUsername" />
|
|
<div v-if="usernameTaken" class="invalid-feedback d-block">Username already taken.</div>
|
|
<div v-else-if="fieldErrors.username" class="invalid-feedback d-block">{{ fieldErrors.username[0] }}</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Mobile Number</label>
|
|
<input v-model="form.mobile_number" type="tel" class="form-control rounded-pill"
|
|
:class="{ 'is-invalid': mobileError || mobileTaken || fieldErrors.mobile_number }"
|
|
placeholder="09XXXXXXXXX" @blur="checkMobile" />
|
|
<div v-if="mobileError" class="invalid-feedback d-block">{{ mobileError }}</div>
|
|
<div v-else-if="mobileTaken" class="invalid-feedback d-block">Mobile number already taken.</div>
|
|
<div v-else-if="fieldErrors.mobile_number" class="invalid-feedback d-block">{{ fieldErrors.mobile_number[0] }}</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-semibold">Password</label>
|
|
<input v-model="form.password" type="password" class="form-control rounded-pill"
|
|
:class="{ 'is-invalid': fieldErrors.password }" placeholder="Min. 6 characters" autocomplete="new-password" />
|
|
<div v-if="fieldErrors.password" class="invalid-feedback">{{ fieldErrors.password[0] }}</div>
|
|
</div>
|
|
|
|
<button class="btn btn-primary rounded-pill w-100 py-2 fw-semibold" :disabled="submitting || !canSubmit" @click="submit">
|
|
<span v-if="submitting" class="spinner-border spinner-border-sm me-2"></span>
|
|
<i v-else class="fas fa-user-plus me-2"></i>
|
|
{{ submitting ? 'Creating...' : 'Create Member' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.info-card {
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.assign-note {
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
}
|
|
:global(.dark-mode) .info-card,
|
|
:global(.dark-mode) .assign-note {
|
|
border-color: rgba(255, 255, 255, 0.08);
|
|
}
|
|
</style>
|