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

494 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { usePageTitle } from '../composables/Core/usePageTitle';
const props = defineProps({ target: String });
usePageTitle('Join Cooperative');
const goToLogin = () => { window.location.href = '/login'; };
const cooperative = ref(null);
const prioritySectors = ref([]);
const loading = ref(true);
const errorMessage = ref('');
const fieldErrors = ref({});
const step = ref(1);
const submitting = ref(false);
const userHashkey = ref(null);
// ── Step 1 ──
const accountForm = ref({
name: '',
username: '',
mobile_number: '',
password: '',
});
const mobileError = ref('');
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;
};
// ── Step 2 ──
const membershipTypes = ['REGULAR', 'ASSOCIATE', 'HONORARY', 'LABORATORY'];
const membershipLevels = ['PRIMARY', 'SECONDARY', 'TERTIARY'];
const commonBonds = ['Residential', 'Institutional', 'Occupational', 'Associational'];
const employmentStatuses = ['Employed', 'Underemployed', 'Unemployed', 'Self-employed'];
const slpTracks = [{ value: 'MD', label: 'Microenterprise Development (MD)' }, { value: 'EF', label: 'Employment Facilitation (EF)' }];
const tupadCategories = ['Underemployed', 'Displaced Worker', 'Senior Citizen (fit to work)', 'PWD', 'Solo Parent', 'Indigenous Person', 'Former Rebel'];
const vulnerabilityOptions = ['Indigenous People (IP)', 'Person with Disability (PWD)', 'Senior Citizen', 'Solo Parent', 'Out-of-School Youth (OSY)', 'Internally Displaced Person (IDP)', 'Distressed OFW', 'Former Rebel'];
const programOptions = ['SLP', 'TUPAD', 'OSEC/NSRP', '4Ps/Pantawid Pamilya', 'Listahanan'];
const memberForm = ref({
membership_type: 'REGULAR',
membership_level: 'PRIMARY',
year_beginning: new Date().getFullYear(),
officer_position: '',
officer_level: '',
concurrent_position: '',
concurrent_level: '',
cooperative_position: '',
cooperative_name_alt: '',
// Classification
priority_sector: [],
common_bond: '',
vulnerability_classifications: [],
// Gov IDs
philsys_id: '',
sss_number: '',
pagibig_number: '',
// SLP
slp_track: '',
slp_association_name: '',
listahanan_id: '',
fourtps_household_id: '',
// TUPAD
tupad_category: '',
tupad_insurance_beneficiary_name: '',
tupad_insurance_beneficiary_relation: '',
// OSEC/NSRP
preferred_occupation: '',
nsrp_skills: [],
employment_status: '',
// Programs
program_participation: [],
});
const newSkill = ref('');
const addSkill = () => {
const s = newSkill.value.trim();
if (s && !memberForm.value.nsrp_skills.includes(s)) {
memberForm.value.nsrp_skills.push(s);
}
newSkill.value = '';
};
const removeSkill = (i) => memberForm.value.nsrp_skills.splice(i, 1);
const fetchCooperative = async () => {
if (!props.target) {
errorMessage.value = 'No cooperative identifier provided.';
loading.value = false;
return;
}
try {
const [coopRes, settingsRes] = await Promise.all([
axios.get(`/api/public/cooperative/${props.target}`),
axios.get('/api/public/system-settings'),
]);
if (coopRes.data.success) cooperative.value = coopRes.data.data;
else errorMessage.value = coopRes.data.message || 'Cooperative not found.';
if (settingsRes.data?.priority_sectors) {
prioritySectors.value = settingsRes.data.priority_sectors;
}
} catch (err) {
errorMessage.value = err.response?.status === 404
? 'Cooperative not found. Please check the link.'
: 'Failed to load cooperative information.';
} finally {
loading.value = false;
}
};
const submitAccount = async () => {
if (submitting.value) return;
if (!validateMobile(accountForm.value.mobile_number)) return;
fieldErrors.value = {};
errorMessage.value = '';
submitting.value = true;
try {
const res = await axios.post('/api/public/cooperative/register', {
...accountForm.value,
cooperative_hash: props.target,
});
if (res.data.success) {
userHashkey.value = res.data.user_hashkey;
step.value = 2;
} else {
errorMessage.value = res.data.message || 'Registration failed.';
}
} 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 submitMembership = async () => {
if (submitting.value) return;
errorMessage.value = '';
submitting.value = true;
try {
const res = await axios.post('/api/public/cooperative/complete-membership', {
...memberForm.value,
user_hashkey: userHashkey.value,
cooperative_hash: props.target,
});
if (res.data.success) step.value = 3;
else errorMessage.value = res.data.message || 'Submission failed.';
} catch (err) {
errorMessage.value = err.response?.data?.message || 'An error occurred.';
} finally {
submitting.value = false;
}
};
onMounted(fetchCooperative);
</script>
<template>
<div class="container py-4" style="max-width:620px;">
<!-- Loading -->
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
<div v-else-if="!cooperative && errorMessage" class="text-center py-5 animate-fade-in">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<p class="text-danger fw-semibold">{{ errorMessage }}</p>
</div>
<template v-else-if="cooperative">
<!-- Coop Header -->
<div class="text-center mb-4 animate-fade-in">
<div class="bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-2" style="width:56px;height:56px;">
<i class="fas fa-users fa-lg"></i>
</div>
<h5 class="fw-bold mb-0">{{ cooperative.name }}</h5>
<span class="badge bg-primary-subtle text-primary rounded-pill px-3 mt-1 small">
{{ cooperative.cooperative_type || 'Cooperative' }}
</span>
<p v-if="cooperative.address" class="text-muted small mt-1 mb-0">
<i class="fas fa-map-marker-alt me-1"></i>{{ cooperative.address }}
</p>
</div>
<!-- Step Indicator -->
<div class="d-flex align-items-center justify-content-center gap-2 mb-4">
<template v-for="s in 2" :key="s">
<div class="rounded-circle d-inline-flex align-items-center justify-content-center fw-bold"
:class="step > s ? 'bg-success text-white' : step === s ? 'bg-primary text-white' : 'bg-light text-muted'"
style="width:32px;height:32px;font-size:13px;">
<i v-if="step > s" class="fas fa-check" style="font-size:11px;"></i>
<span v-else>{{ s }}</span>
</div>
<span class="small" :class="step >= s ? 'fw-semibold text-dark' : 'text-muted'">
{{ s === 1 ? 'Account' : 'Membership' }}
</span>
<i v-if="s < 2" class="fas fa-chevron-right text-muted small mx-1"></i>
</template>
</div>
<!-- STEP 1: Account -->
<div v-if="step === 1" class="card border-0 shadow-sm rounded-4 p-4 animate-fade-in">
<h6 class="fw-semibold mb-3">Create your account</h6>
<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="accountForm.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="accountForm.username" type="text" class="form-control rounded-pill"
:class="{ 'is-invalid': fieldErrors.username }" placeholder="juandelacruz" autocomplete="username" />
<div v-if="fieldErrors.username" class="invalid-feedback">{{ fieldErrors.username[0] }}</div>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Mobile Number</label>
<input v-model="accountForm.mobile_number" type="tel"
class="form-control rounded-pill"
:class="{ 'is-invalid': mobileError || fieldErrors.mobile_number }"
placeholder="09XXXXXXXXX"
@blur="validateMobile(accountForm.mobile_number)" />
<div class="invalid-feedback">{{ mobileError || fieldErrors.mobile_number?.[0] }}</div>
<div class="form-text small text-muted">Philippine mobile number (09XXXXXXXXX or +639XXXXXXXXX)</div>
</div>
<div class="mb-4">
<label class="form-label small fw-semibold">Password</label>
<input v-model="accountForm.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 @click="submitAccount" :disabled="submitting" class="btn btn-primary rounded-pill w-100 py-2 fw-semibold">
<span v-if="submitting" class="spinner-border spinner-border-sm me-2"></span>
<i v-else class="fas fa-arrow-right me-2"></i>
{{ submitting ? 'Creating account...' : 'Continue to Membership' }}
</button>
<div class="text-center mt-3">
<small class="text-muted">Already have an account?
<a href="#" @click.prevent="goToLogin" class="text-primary fw-semibold">Log in</a>
</small>
</div>
</div>
<!-- STEP 2: Membership -->
<div v-else-if="step === 2" class="animate-fade-in">
<div class="alert alert-success rounded-3 small py-2 mb-3">
<i class="fas fa-check-circle me-2"></i>Account created! Complete your membership application below.
</div>
<div v-if="errorMessage" class="alert alert-danger rounded-3 small py-2">{{ errorMessage }}</div>
<!-- Membership Info -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3">
<h6 class="fw-semibold mb-3"><i class="fas fa-id-card text-primary me-2"></i>Membership Information</h6>
<div class="row g-3">
<div class="col-6">
<label class="form-label small fw-semibold">Type</label>
<select v-model="memberForm.membership_type" class="form-select rounded-pill">
<option v-for="t in membershipTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Level</label>
<select v-model="memberForm.membership_level" class="form-select rounded-pill">
<option v-for="l in membershipLevels" :key="l" :value="l">{{ l }}</option>
</select>
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Year Joined</label>
<input v-model="memberForm.year_beginning" type="number" class="form-control rounded-pill" :min="1990" :max="new Date().getFullYear()" />
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Common Bond</label>
<select v-model="memberForm.common_bond" class="form-select rounded-pill">
<option value=""> Select </option>
<option v-for="b in commonBonds" :key="b" :value="b">{{ b }}</option>
</select>
</div>
<div class="col-12" v-if="prioritySectors.length">
<label class="form-label small fw-semibold">Priority Sector <span class="text-muted fw-normal small">(select all that apply)</span></label>
<div class="row g-2 mt-1">
<div class="col-6 col-md-4" v-for="s in prioritySectors" :key="s">
<div class="form-check">
<input class="form-check-input" type="checkbox" :id="'ps-' + s" :value="s" v-model="memberForm.priority_sector">
<label class="form-check-label small" :for="'ps-' + s">{{ s }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Vulnerability Classifications -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3">
<h6 class="fw-semibold mb-3"><i class="fas fa-shield-alt text-warning me-2"></i>Vulnerability Classification <span class="text-muted fw-normal small">(check all that apply)</span></h6>
<div class="row g-2">
<div class="col-6" v-for="opt in vulnerabilityOptions" :key="opt">
<div class="form-check">
<input class="form-check-input" type="checkbox" :id="'vuln-'+opt"
:value="opt" v-model="memberForm.vulnerability_classifications" />
<label class="form-check-label small" :for="'vuln-'+opt">{{ opt }}</label>
</div>
</div>
</div>
</div>
<!-- Program Participation -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3">
<h6 class="fw-semibold mb-3"><i class="fas fa-list-check text-info me-2"></i>Government Program Participation <span class="text-muted fw-normal small">(check all that apply)</span></h6>
<div class="row g-2">
<div class="col-6" v-for="prog in programOptions" :key="prog">
<div class="form-check">
<input class="form-check-input" type="checkbox" :id="'prog-'+prog"
:value="prog" v-model="memberForm.program_participation" />
<label class="form-check-label small" :for="'prog-'+prog">{{ prog }}</label>
</div>
</div>
</div>
</div>
<!-- Gov IDs -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3">
<h6 class="fw-semibold mb-3"><i class="fas fa-id-badge text-secondary me-2"></i>Government ID Numbers <span class="text-muted fw-normal small">(Optional)</span></h6>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-semibold">PhilSys ID</label>
<input v-model="memberForm.philsys_id" type="text" class="form-control rounded-pill" placeholder="National ID" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">SSS Number</label>
<input v-model="memberForm.sss_number" type="text" class="form-control rounded-pill" placeholder="00-0000000-0" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Pag-IBIG Number</label>
<input v-model="memberForm.pagibig_number" type="text" class="form-control rounded-pill" placeholder="0000-0000-0000" />
</div>
</div>
</div>
<!-- SLP -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3" v-if="memberForm.program_participation.includes('SLP')">
<h6 class="fw-semibold mb-3"><i class="fas fa-seedling text-success me-2"></i>SLP Sustainable Livelihood Program</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-semibold">SLP Track</label>
<select v-model="memberForm.slp_track" class="form-select rounded-pill">
<option value=""> Select </option>
<option v-for="t in slpTracks" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">SLPA / Association Name</label>
<input v-model="memberForm.slp_association_name" type="text" class="form-control rounded-pill" placeholder="Association name" />
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Listahanan (NHTO) Household ID</label>
<input v-model="memberForm.listahanan_id" type="text" class="form-control rounded-pill" />
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">4Ps Household ID</label>
<input v-model="memberForm.fourtps_household_id" type="text" class="form-control rounded-pill" />
</div>
</div>
</div>
<!-- TUPAD -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3" v-if="memberForm.program_participation.includes('TUPAD')">
<h6 class="fw-semibold mb-3"><i class="fas fa-hard-hat text-warning me-2"></i>TUPAD Tulong Panghanapbuhay sa Ating Disadvantaged Workers</h6>
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-semibold">Beneficiary Category</label>
<select v-model="memberForm.tupad_category" class="form-select rounded-pill">
<option value=""> Select </option>
<option v-for="c in tupadCategories" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Micro-insurance Beneficiary Name</label>
<input v-model="memberForm.tupad_insurance_beneficiary_name" type="text" class="form-control rounded-pill" placeholder="Full name" />
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Relationship to Beneficiary</label>
<input v-model="memberForm.tupad_insurance_beneficiary_relation" type="text" class="form-control rounded-pill" placeholder="e.g. Spouse, Child" />
</div>
</div>
</div>
<!-- OSEC / NSRP -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3" v-if="memberForm.program_participation.includes('OSEC/NSRP')">
<h6 class="fw-semibold mb-3"><i class="fas fa-briefcase text-primary me-2"></i>OSEC / NSRP Employment Profile</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-semibold">Employment Status</label>
<select v-model="memberForm.employment_status" class="form-select rounded-pill">
<option value=""> Select </option>
<option v-for="s in employmentStatuses" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Preferred Occupation</label>
<input v-model="memberForm.preferred_occupation" type="text" class="form-control rounded-pill" placeholder="e.g. Farmer, Welder" />
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Technical Skills</label>
<div class="d-flex gap-2 mb-2">
<input v-model="newSkill" type="text" class="form-control rounded-pill"
placeholder="Add a skill and press +" @keyup.enter="addSkill" />
<button @click="addSkill" class="btn btn-outline-primary rounded-pill px-3">+</button>
</div>
<div class="d-flex flex-wrap gap-1">
<span v-for="(sk, i) in memberForm.nsrp_skills" :key="i"
class="badge bg-primary-subtle text-primary rounded-pill px-3 py-2">
{{ sk }} <i class="fas fa-times ms-1 cursor-pointer" @click="removeSkill(i)"></i>
</span>
</div>
</div>
</div>
</div>
<!-- Position Details -->
<div class="card border-0 shadow-sm rounded-4 p-4 mb-3">
<h6 class="fw-semibold mb-3"><i class="fas fa-user-tie text-primary me-2"></i>Position Details <span class="text-muted fw-normal small">(Optional)</span></h6>
<div class="row g-3">
<div class="col-8">
<label class="form-label small fw-semibold">Officer Position</label>
<input v-model="memberForm.officer_position" type="text" class="form-control rounded-pill" placeholder="e.g. Board Member" />
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Level</label>
<select v-model="memberForm.officer_level" class="form-select rounded-pill">
<option value=""></option>
<option v-for="l in membershipLevels" :key="l" :value="l">{{ l }}</option>
</select>
</div>
<div class="col-8">
<label class="form-label small fw-semibold">Concurrent Position</label>
<input v-model="memberForm.concurrent_position" type="text" class="form-control rounded-pill" placeholder="e.g. Treasurer" />
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Level</label>
<select v-model="memberForm.concurrent_level" class="form-select rounded-pill">
<option value=""></option>
<option v-for="l in membershipLevels" :key="l" :value="l">{{ l }}</option>
</select>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Cooperative Position</label>
<input v-model="memberForm.cooperative_position" type="text" class="form-control rounded-pill" placeholder="e.g. Chairperson" />
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Alternative Cooperative Name</label>
<input v-model="memberForm.cooperative_name_alt" type="text" class="form-control rounded-pill" placeholder="Former or alternate name" />
</div>
</div>
</div>
<button @click="submitMembership" :disabled="submitting" class="btn btn-success rounded-pill w-100 py-2 fw-semibold mb-4">
<span v-if="submitting" class="spinner-border spinner-border-sm me-2"></span>
<i v-else class="fas fa-paper-plane me-2"></i>
{{ submitting ? 'Submitting...' : 'Submit Membership Application' }}
</button>
</div>
<!-- STEP 3: Done -->
<div v-else-if="step === 3" class="text-center py-5 animate-fade-in">
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
<h4 class="fw-bold">Application Submitted!</h4>
<p class="text-muted">You are now registered as a member of <strong>{{ cooperative?.name }}</strong>.<br>You may log in with your credentials.</p>
<button @click="goToLogin" class="btn btn-primary rounded-pill px-4 mt-2">
<i class="fas fa-sign-in-alt me-2"></i> Go to Login
</button>
</div>
</template>
</div>
</template>