@muflih_kh/profile-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/profile-ui.es.js +9919 -0
- package/dist/profile-ui.es.js.map +1 -0
- package/dist/profile-ui.umd.js +95 -0
- package/dist/profile-ui.umd.js.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +53 -0
- package/src/components/EducationModal.vue +126 -0
- package/src/components/ImageCropModal.vue +70 -0
- package/src/components/OtpModal.vue +166 -0
- package/src/components/ParentModal.vue +276 -0
- package/src/components/PersonalDataEditModal.vue +256 -0
- package/src/components/PreviewModal.vue +55 -0
- package/src/components/Profile.vue +171 -0
- package/src/components/ProfileEducation.vue +116 -0
- package/src/components/ProfileHeader.vue +182 -0
- package/src/components/ProfileParents.vue +173 -0
- package/src/components/ProfilePersonalData.vue +135 -0
- package/src/components/ProfileVerification.vue +118 -0
- package/src/components/SgFileUploader.vue +355 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-vh-100 bg-light">
|
|
3
|
+
<ProfileHeader :user="me" @update-profile="handleProfileUpdate" @update-photo="handlePhotoUpdate"
|
|
4
|
+
@reset-password="handleResetPassword" />
|
|
5
|
+
|
|
6
|
+
<ProfilePersonalData :user="me" :locations="locations" @update="handleProfileUpdate"
|
|
7
|
+
@search-locations="searchLocations" />
|
|
8
|
+
|
|
9
|
+
<ProfileVerification :user="me" @verified="handleVerified" />
|
|
10
|
+
|
|
11
|
+
<ProfileEducation :educations="userEducation" @refresh="getUserEducation" />
|
|
12
|
+
|
|
13
|
+
<ProfileParents :parents="parentsData" @refresh="getParents" />
|
|
14
|
+
|
|
15
|
+
<PreviewModal v-model="showPreview" :url="previewUrl" />
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import {
|
|
21
|
+
getProfile,
|
|
22
|
+
getParents as apiGetParents,
|
|
23
|
+
getEducations,
|
|
24
|
+
getLocations,
|
|
25
|
+
updateProfile,
|
|
26
|
+
changePassword
|
|
27
|
+
} from '../api/profileApi';
|
|
28
|
+
import { showToast } from '../utils/toast';
|
|
29
|
+
import ProfileHeader from './ProfileHeader.vue';
|
|
30
|
+
import ProfilePersonalData from './ProfilePersonalData.vue';
|
|
31
|
+
import ProfileVerification from './ProfileVerification.vue';
|
|
32
|
+
import ProfileEducation from './ProfileEducation.vue';
|
|
33
|
+
import ProfileParents from './ProfileParents.vue';
|
|
34
|
+
import PreviewModal from './PreviewModal.vue';
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
name: 'ProfilePage',
|
|
38
|
+
components: {
|
|
39
|
+
ProfileHeader,
|
|
40
|
+
ProfilePersonalData,
|
|
41
|
+
ProfileVerification,
|
|
42
|
+
ProfileEducation,
|
|
43
|
+
ProfileParents,
|
|
44
|
+
PreviewModal
|
|
45
|
+
},
|
|
46
|
+
data() {
|
|
47
|
+
return {
|
|
48
|
+
me: {},
|
|
49
|
+
userEducation: null,
|
|
50
|
+
parentsData: null,
|
|
51
|
+
locations: [],
|
|
52
|
+
showPreview: false,
|
|
53
|
+
previewUrl: null
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
async created() {
|
|
57
|
+
await this.getMe();
|
|
58
|
+
await this.getParents();
|
|
59
|
+
await this.getUserEducation();
|
|
60
|
+
this.handleQueryParams();
|
|
61
|
+
},
|
|
62
|
+
methods: {
|
|
63
|
+
async getMe() {
|
|
64
|
+
try {
|
|
65
|
+
const response = await getProfile();
|
|
66
|
+
this.me = response.data.data;
|
|
67
|
+
localStorage.setItem("__putm.sia.user", JSON.stringify(this.me));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(error);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async getParents() {
|
|
73
|
+
try {
|
|
74
|
+
const response = await apiGetParents();
|
|
75
|
+
this.parentsData = response.data.data;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(error);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async getUserEducation() {
|
|
81
|
+
try {
|
|
82
|
+
const response = await getEducations();
|
|
83
|
+
this.userEducation = response.data;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(error);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async searchLocations(search, loading) {
|
|
89
|
+
if (search.length < 3) return;
|
|
90
|
+
loading(true);
|
|
91
|
+
try {
|
|
92
|
+
// getLocations tidak menerima parameter, jadi gunakan axios manual jika perlu search param
|
|
93
|
+
const response = await getLocations();
|
|
94
|
+
// Jika API mendukung query param, ubah getLocations agar menerima param search
|
|
95
|
+
// const response = await getLocations(search);
|
|
96
|
+
this.locations = response.data.data || [];
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error searching locations:', error);
|
|
99
|
+
} finally {
|
|
100
|
+
loading(false);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async handleProfileUpdate(updatedData) {
|
|
104
|
+
try {
|
|
105
|
+
await updateProfile(updatedData);
|
|
106
|
+
showToast({
|
|
107
|
+
icon: 'success',
|
|
108
|
+
title: 'Success',
|
|
109
|
+
text: 'Data profile berhasil disimpan.'
|
|
110
|
+
});
|
|
111
|
+
await this.getMe();
|
|
112
|
+
await this.redirectIfNeeded();
|
|
113
|
+
if (this.$route.query.r) {
|
|
114
|
+
this.$router.push(this.$route.query.r);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
showToast({
|
|
118
|
+
icon: 'error',
|
|
119
|
+
title: 'Error',
|
|
120
|
+
text: 'Gagal menyimpan data profile: ' + error.response.data.user_message
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async handlePhotoUpdate(photo) {
|
|
125
|
+
this.me.photo = photo;
|
|
126
|
+
await this.handleProfileUpdate(this.me);
|
|
127
|
+
if (this.$route.query.r) {
|
|
128
|
+
this.$router.push(this.$route.query.r);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
handleVerified() {
|
|
132
|
+
this.getMe();
|
|
133
|
+
},
|
|
134
|
+
handleQueryParams() {
|
|
135
|
+
// Handle query parameters untuk auto-open modal
|
|
136
|
+
const { data, type } = this.$route.query;
|
|
137
|
+
if (data) {
|
|
138
|
+
this.$nextTick(() => {
|
|
139
|
+
// Emit event ke child components
|
|
140
|
+
this.$root.$emit('open-modal', { type: data, subType: type });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async redirectIfNeeded() {
|
|
145
|
+
if (this.$route.query.r) {
|
|
146
|
+
this.$router.push(this.$route.query.r);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
async handleResetPassword(passwordForm) {
|
|
150
|
+
try {
|
|
151
|
+
await changePassword(passwordForm);
|
|
152
|
+
showToast({
|
|
153
|
+
icon: 'success',
|
|
154
|
+
title: 'Success',
|
|
155
|
+
text: 'Password berhasil diubah.'
|
|
156
|
+
});
|
|
157
|
+
await this.getMe();
|
|
158
|
+
await this.redirectIfNeeded();
|
|
159
|
+
if (this.$route.query.r) {
|
|
160
|
+
this.$router.push(this.$route.query.r);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
showToast({
|
|
164
|
+
icon: 'error',
|
|
165
|
+
title: 'Gagal mengubah password: ' + error.response.data.user_message
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
</script>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="card shadow-sm">
|
|
3
|
+
<div class="card-body border-top">
|
|
4
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
5
|
+
<h3>Pendidikan</h3>
|
|
6
|
+
<div class="d-flex gap-2">
|
|
7
|
+
<button class="btn btn-light rounded-circle" @click="showAddModal = true">
|
|
8
|
+
<i class="uil uil-plus"></i>
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="row g-4">
|
|
14
|
+
<div class="col-12" v-for="(edu, k) in educations?.data" :key="k">
|
|
15
|
+
<div class="d-flex gap-3">
|
|
16
|
+
<div class="flex-shrink-0 d-flex align-items-center justify-content-center"
|
|
17
|
+
style="width: 48px; height: 48px;">
|
|
18
|
+
<a class="link" @click="showPreview(edu.certificate?.url)">
|
|
19
|
+
<img v-if="edu.certificate" :src="edu.certificate?.url" alt="Ijazah"
|
|
20
|
+
style="width: 40px; height: 60px; object-fit: cover; object-position: center;" />
|
|
21
|
+
<span v-else
|
|
22
|
+
style="width: 40px; height: 60px; display: flex; align-items: center; justify-content: center;">
|
|
23
|
+
<i class="uil uil-file-alt"></i>
|
|
24
|
+
</span>
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="flex-fill">
|
|
28
|
+
<h5 class="mb-1">{{ edu.previous_school }}</h5>
|
|
29
|
+
<p class="text-muted small mb-1">Rata-rata: {{ edu.score }}</p>
|
|
30
|
+
<p class="text-muted small mb-1">
|
|
31
|
+
Jurusan: {{ edu.major }} - {{ edu.nisn }} · {{ edu.year_graduated }}
|
|
32
|
+
</p>
|
|
33
|
+
<span class="text-muted small" v-if="edu.degree">{{ edu.degree }}</span>
|
|
34
|
+
<span v-if="edu.degree && edu.gpa">-</span>
|
|
35
|
+
<span class="text-muted small" v-if="edu.gpa">IPK: {{ edu.gpa }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<button class="btn" @click="editEducation(edu)">
|
|
38
|
+
<i class="uil uil-edit"></i>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<button class="btn btn-light w-100 mt-4" v-if="educations?.meta?.has_next_page" @click="loadMore">
|
|
45
|
+
Lihat Lebih Banyak
|
|
46
|
+
<i class="uil uil-angle-down ms-2"></i>
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Add/Edit Education Modal -->
|
|
51
|
+
<EducationModal v-model="showAddModal" :education="selectedEducation" @save="handleSave"
|
|
52
|
+
@show-preview="showPreview" />
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
import EducationModal from './EducationModal.vue';
|
|
58
|
+
|
|
59
|
+
export default {
|
|
60
|
+
name: 'ProfileEducation',
|
|
61
|
+
components: {
|
|
62
|
+
EducationModal
|
|
63
|
+
},
|
|
64
|
+
props: {
|
|
65
|
+
educations: {
|
|
66
|
+
type: Object,
|
|
67
|
+
default: null
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
data () {
|
|
71
|
+
return {
|
|
72
|
+
showAddModal: false,
|
|
73
|
+
selectedEducation: null
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
mounted () {
|
|
77
|
+
this.$root.$on('open-modal', ({ type }) => {
|
|
78
|
+
if (type === 'education') {
|
|
79
|
+
this.showAddModal = true;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
beforeDestroy () {
|
|
84
|
+
this.$root.$off('open-modal');
|
|
85
|
+
},
|
|
86
|
+
methods: {
|
|
87
|
+
editEducation (edu) {
|
|
88
|
+
this.selectedEducation = edu;
|
|
89
|
+
this.showAddModal = true;
|
|
90
|
+
},
|
|
91
|
+
handleSave () {
|
|
92
|
+
this.selectedEducation = null;
|
|
93
|
+
this.showAddModal = false;
|
|
94
|
+
this.$emit('refresh');
|
|
95
|
+
},
|
|
96
|
+
loadMore () {
|
|
97
|
+
this.$emit('refresh', true);
|
|
98
|
+
},
|
|
99
|
+
showPreview (url) {
|
|
100
|
+
this.$root.$emit('show-preview', url);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.link {
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
color: #0d6efd;
|
|
110
|
+
text-decoration: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.link:hover {
|
|
114
|
+
text-decoration: underline;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="card shadow-sm">
|
|
3
|
+
<!-- Cover Photo -->
|
|
4
|
+
<div class="position-relative"
|
|
5
|
+
style="height: 200px; background: linear-gradient(to right, rgb(11, 171, 224), rgb(6, 10, 107));">
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Profile Section -->
|
|
9
|
+
<div class="card-body">
|
|
10
|
+
<!-- Profile Picture -->
|
|
11
|
+
<div class="position-relative" style="margin-top: -190px; margin-bottom: 20px;">
|
|
12
|
+
<div class="d-inline-block bg-white p-2 shadow-lg position-relative">
|
|
13
|
+
<div class="d-flex align-items-center justify-center position-relative"
|
|
14
|
+
style="width: 180px; height: 230px; background: linear-gradient(135deg, #64748b, #64748b);">
|
|
15
|
+
<img v-if="user.photo" :src="user.photo?.url" alt="Profile Picture"
|
|
16
|
+
style="width: 180px; height: 230px; object-fit: cover; object-position: center;" />
|
|
17
|
+
<img v-else
|
|
18
|
+
:src="`https://ui-avatars.com/api/?name=${user.name}&size=180&background=64748b&color=ffffff&rounded=false`"
|
|
19
|
+
alt="Profile Picture" style="width: 180px; height: 230px; object-fit: cover; object-position: center;" />
|
|
20
|
+
<button class="btn btn-light position-absolute rounded-circle border border-white shadow-sm"
|
|
21
|
+
style="bottom: 10px; right: 10px; z-index: 2;" @click="showImageCropModal = true">
|
|
22
|
+
<i class="uil uil-camera"></i>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Name and Title -->
|
|
29
|
+
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
30
|
+
<div>
|
|
31
|
+
<div class="d-flex align-items-center gap-2 mb-2">
|
|
32
|
+
<h2 class="mb-0">{{ user.name }}</h2>
|
|
33
|
+
</div>
|
|
34
|
+
<p class="text-dark mb-2">{{ user?.student?.nim }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Education and Work -->
|
|
39
|
+
<div class="mb-4">
|
|
40
|
+
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
|
41
|
+
<div class="d-flex align-items-center gap-2">
|
|
42
|
+
<div class="bg-primary rounded d-flex align-items-center justify-content-center"
|
|
43
|
+
style="width: 30px; height: 30px;">
|
|
44
|
+
<i class="bg-primary rounded" style="width: 20px; height: 20px;"></i>
|
|
45
|
+
</div>
|
|
46
|
+
<span class="fw-medium">{{ user?.student?.campus?.label }}</span>
|
|
47
|
+
<div class="bg-warning rounded d-flex align-items-center justify-content-center"
|
|
48
|
+
style="width: 30px; height: 30px;">
|
|
49
|
+
<span class="text-white fw-bold small"></span>
|
|
50
|
+
</div>
|
|
51
|
+
<span class="fw-medium">{{ user?.student?.school_year?.label }}</span>
|
|
52
|
+
<div class="bg-success rounded d-flex align-items-center justify-content-center"
|
|
53
|
+
style="width: 30px; height: 30px;">
|
|
54
|
+
<span class="text-white fw-bold small"></span>
|
|
55
|
+
</div>
|
|
56
|
+
<span class="fw-medium">{{ user?.student?.semester?.slug }}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<button class="btn btn-outline-primary btn-sm" @click="handleResetPassword">
|
|
59
|
+
<i class="uil uil-lock-alt me-1"></i>
|
|
60
|
+
Reset Password
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Image Crop Modal -->
|
|
67
|
+
<ImageCropModal v-model="showImageCropModal" :photo="user.photo" @save="handlePhotoSave" />
|
|
68
|
+
|
|
69
|
+
<!-- Reset Password Modal -->
|
|
70
|
+
<div class="modal fade" :class="{ 'show d-block': showResetPasswordModal }" tabindex="-1"
|
|
71
|
+
style="background-color: rgba(0,0,0,0.5);" v-if="showResetPasswordModal">
|
|
72
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
73
|
+
<div class="modal-content">
|
|
74
|
+
<div class="modal-header">
|
|
75
|
+
<h5 class="modal-title">Reset Password</h5>
|
|
76
|
+
<button type="button" class="btn-close" @click="closeResetPasswordModal"></button>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="modal-body">
|
|
79
|
+
<form @submit.prevent="submitResetPassword">
|
|
80
|
+
<div class="mb-3" v-if="user && user.password">
|
|
81
|
+
<label for="current_password" class="form-label">Old Password</label>
|
|
82
|
+
<div class="input-group">
|
|
83
|
+
<input :type="showcurrent_password ? 'text' : 'password'" class="form-control" id="current_password"
|
|
84
|
+
v-model="passwordForm.current_password" required placeholder="Enter old password">
|
|
85
|
+
<button class="btn btn-outline-secondary" type="button"
|
|
86
|
+
@click="showcurrent_password = !showcurrent_password">
|
|
87
|
+
<i :class="showcurrent_password ? 'uil uil-eye-slash' : 'uil uil-eye'"></i>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="mb-3">
|
|
92
|
+
<label for="new_password" class="form-label">New Password</label>
|
|
93
|
+
<div class="input-group">
|
|
94
|
+
<input :type="shownew_password ? 'text' : 'password'" class="form-control" id="new_password"
|
|
95
|
+
v-model="passwordForm.new_password" required placeholder="Enter new password" minlength="8">
|
|
96
|
+
<button class="btn btn-outline-secondary" type="button" @click="shownew_password = !shownew_password">
|
|
97
|
+
<i :class="shownew_password ? 'uil uil-eye-slash' : 'uil uil-eye'"></i>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
<small class="text-muted">Password must be at least 8 characters</small>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="mb-3">
|
|
103
|
+
<label for="new_password_confirmation" class="form-label">Confirm New Password</label>
|
|
104
|
+
<div class="input-group">
|
|
105
|
+
<input :type="shownew_password_confirmation ? 'text' : 'password'" class="form-control"
|
|
106
|
+
id="new_password_confirmation" v-model="passwordForm.new_password_confirmation" required
|
|
107
|
+
placeholder="Confirm new password">
|
|
108
|
+
<button class="btn btn-outline-secondary" type="button"
|
|
109
|
+
@click="shownew_password_confirmation = !shownew_password_confirmation">
|
|
110
|
+
<i :class="shownew_password_confirmation ? 'uil uil-eye-slash' : 'uil uil-eye'"></i>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
<small
|
|
114
|
+
v-if="passwordForm.new_password && passwordForm.new_password_confirmation && passwordForm.new_password !== passwordForm.new_password_confirmation"
|
|
115
|
+
class="text-danger">Passwords do not match</small>
|
|
116
|
+
</div>
|
|
117
|
+
</form>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="modal-footer">
|
|
120
|
+
<button type="button" class="btn btn-secondary" @click="closeResetPasswordModal">Cancel</button>
|
|
121
|
+
<button type="button" class="btn btn-primary" @click="submitResetPassword" :disabled="!isPasswordFormValid">
|
|
122
|
+
Reset Password
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
|
|
131
|
+
<script>
|
|
132
|
+
import ImageCropModal from './ImageCropModal.vue';
|
|
133
|
+
|
|
134
|
+
export default {
|
|
135
|
+
name: 'ProfileHeader',
|
|
136
|
+
components: {
|
|
137
|
+
ImageCropModal
|
|
138
|
+
},
|
|
139
|
+
props: {
|
|
140
|
+
user: {
|
|
141
|
+
type: Object,
|
|
142
|
+
required: true
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
data() {
|
|
146
|
+
return {
|
|
147
|
+
showImageCropModal: false,
|
|
148
|
+
showResetPasswordModal: false,
|
|
149
|
+
passwordForm: {
|
|
150
|
+
current_password: '',
|
|
151
|
+
new_password: '',
|
|
152
|
+
new_password_confirmation: ''
|
|
153
|
+
},
|
|
154
|
+
showcurrent_password: false,
|
|
155
|
+
shownew_password: false,
|
|
156
|
+
shownew_password_confirmation: false
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
computed: {
|
|
160
|
+
isPasswordFormValid() {
|
|
161
|
+
return this.passwordForm.new_password && this.passwordForm.new_password_confirmation && this.passwordForm.new_password == this.passwordForm.new_password_confirmation;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
methods: {
|
|
165
|
+
handlePhotoSave(photo) {
|
|
166
|
+
this.$emit('update-photo', photo);
|
|
167
|
+
this.showImageCropModal = false;
|
|
168
|
+
},
|
|
169
|
+
handleResetPassword() {
|
|
170
|
+
this.showResetPasswordModal = true;
|
|
171
|
+
},
|
|
172
|
+
closeResetPasswordModal() {
|
|
173
|
+
this.showResetPasswordModal = false;
|
|
174
|
+
},
|
|
175
|
+
submitResetPassword() {
|
|
176
|
+
this.passwordForm.pass_available = this.user.password ? true : false;
|
|
177
|
+
this.$emit('reset-password', this.passwordForm);
|
|
178
|
+
this.closeResetPasswordModal();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
</script>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="card shadow-sm">
|
|
3
|
+
<div class="card-body border-top">
|
|
4
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
5
|
+
<h3>Data Orang Tua / Wali</h3>
|
|
6
|
+
<div class="d-flex gap-2">
|
|
7
|
+
<button @click="openDialog()" class="btn btn-success rounded-circle" title="Tambah Orang Tua/Wali">
|
|
8
|
+
<i class="uil uil-plus"></i>
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="row g-3">
|
|
14
|
+
<div v-for="parent in parents" :key="parent.id" class="col-12 mb-3">
|
|
15
|
+
<div>
|
|
16
|
+
<div class="mb-3">
|
|
17
|
+
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
18
|
+
<div class="d-flex align-items-center gap-3">
|
|
19
|
+
<div class="d-flex align-items-center justify-content-center flex-shrink-0"
|
|
20
|
+
style="width: 48px; height: 48px; background: linear-gradient(135deg, #3b82f6, #2563eb);">
|
|
21
|
+
<i class="uil uil-user text-white fs-5"></i>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<div class="d-flex align-items-center gap-2 mb-1">
|
|
25
|
+
<h5 class="mb-0">{{ parent.name }}</h5>
|
|
26
|
+
<span class="badge bg-primary">{{ parent.family_status | parentStatus }}</span>
|
|
27
|
+
<span class="badge bg-primary"
|
|
28
|
+
v-if="parent.status == 'hidup' && parent.family_status == 'father'">Wali</span>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="text-muted small mb-0">{{ parent.work }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<button @click="openDialog(parent)" class="btn" title="Edit Data">
|
|
34
|
+
<i class="uil uil-edit"></i>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="row g-2 small">
|
|
39
|
+
<div class="col-md-6">
|
|
40
|
+
<div class="d-flex">
|
|
41
|
+
<span class="text-muted" style="width: 130px;">Status:</span>
|
|
42
|
+
<span class="fw-medium">{{ parent.status }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="col-md-6">
|
|
46
|
+
<div class="d-flex">
|
|
47
|
+
<span class="text-muted" style="width: 130px;">NIK:</span>
|
|
48
|
+
<span class="fw-medium">{{ parent.national_id | nationalIdFormat }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="col-md-6">
|
|
52
|
+
<div class="d-flex">
|
|
53
|
+
<span class="text-muted" style="width: 130px;">Pendidikan:</span>
|
|
54
|
+
<span class="fw-medium">{{ educations[parent.education] }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="col-md-6">
|
|
58
|
+
<div class="d-flex">
|
|
59
|
+
<span class="text-muted" style="width: 130px;">Penghasilan:</span>
|
|
60
|
+
<span class="fw-medium">{{ salaries[parent.income] }}</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="col-md-6">
|
|
64
|
+
<div class="d-flex">
|
|
65
|
+
<span class="text-muted" style="width: 130px;">No. HP:</span>
|
|
66
|
+
<span class="fw-medium">{{ parent.phone | formatPhone }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="col-12">
|
|
70
|
+
<div class="d-flex">
|
|
71
|
+
<span class="text-muted flex-shrink-0" style="width: 130px;">Alamat:</span>
|
|
72
|
+
<span class="fw-medium">{{ parent.address }}</span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Info Message -->
|
|
82
|
+
<div class="alert alert-info d-flex gap-3 mt-3" role="alert">
|
|
83
|
+
<i class="uil uil-info-circle flex-shrink-0"></i>
|
|
84
|
+
<div>
|
|
85
|
+
<p class="fw-semibold mb-1">Informasi Penting:</p>
|
|
86
|
+
<p class="mb-0 small">Data orang tua/wali yang Anda masukkan akan dijaga kerahasiaannya
|
|
87
|
+
dan hanya digunakan untuk keperluan administrasi.</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Parent Modal -->
|
|
93
|
+
<ParentModal v-model="showParentDialog" :parent="editingParent" :salaries="salaries" :educations="educations"
|
|
94
|
+
@save="handleSave" />
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
import ParentModal from './ParentModal.vue';
|
|
100
|
+
|
|
101
|
+
export default {
|
|
102
|
+
name: 'ProfileParents',
|
|
103
|
+
components: {
|
|
104
|
+
ParentModal
|
|
105
|
+
},
|
|
106
|
+
props: {
|
|
107
|
+
parents: {
|
|
108
|
+
type: Array,
|
|
109
|
+
default: () => []
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
data () {
|
|
113
|
+
return {
|
|
114
|
+
showParentDialog: false,
|
|
115
|
+
editingParent: null,
|
|
116
|
+
salaries: {
|
|
117
|
+
0: 'Kurang dari Rp 1.000.000',
|
|
118
|
+
1000000: 'Rp 1.000.000 - Rp 2.000.000',
|
|
119
|
+
2000000: 'Rp 2.000.000 - Rp 5.000.000',
|
|
120
|
+
5000000: 'Rp 5.000.000 - Rp 10.000.000',
|
|
121
|
+
10000000: 'Rp 10.000.000 - Rp 15.000.000',
|
|
122
|
+
15000000: 'Rp 15.000.000 - Rp 20.000.000',
|
|
123
|
+
20000000: 'Rp 20.000.000 - Rp 25.000.000',
|
|
124
|
+
25000000: 'Rp 25.000.000 - Rp 30.000.000',
|
|
125
|
+
30000000: 'Rp 30.000.000 - Rp 40.000.000',
|
|
126
|
+
40000000: 'Rp 40.000.000 - Rp 50.000.000',
|
|
127
|
+
50000000: 'Rp 50.000.000 - Rp 60.000.000',
|
|
128
|
+
60000000: 'Rp 60.000.000 - Rp 70.000.000',
|
|
129
|
+
70000000: 'Rp 70.000.000 - Rp 80.000.000',
|
|
130
|
+
80000000: 'Rp 80.000.000 - Rp 90.000.000',
|
|
131
|
+
90000000: 'Rp 90.000.000 - Rp 100.000.000',
|
|
132
|
+
100000000: 'Lebih dari Rp 100.000.000'
|
|
133
|
+
},
|
|
134
|
+
educations: {
|
|
135
|
+
sd: 'SD/Sederajat',
|
|
136
|
+
smp: 'SMP/Sederajat',
|
|
137
|
+
sma: 'SMA/Sederajat',
|
|
138
|
+
d3: 'Diploma (D3)',
|
|
139
|
+
d4: 'Diploma (D4)',
|
|
140
|
+
s1: 'Sarjana (S1)',
|
|
141
|
+
s2: 'Magister (S2)',
|
|
142
|
+
s3: 'Doktor (S3)'
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
mounted () {
|
|
147
|
+
this.$root.$on('open-modal', ({ type, subType }) => {
|
|
148
|
+
if (type === 'parent') {
|
|
149
|
+
this.showParentDialog = true;
|
|
150
|
+
if (subType) {
|
|
151
|
+
this.$nextTick(() => {
|
|
152
|
+
this.$root.$emit('set-family-status', subType);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
beforeDestroy () {
|
|
159
|
+
this.$root.$off('open-modal');
|
|
160
|
+
},
|
|
161
|
+
methods: {
|
|
162
|
+
openDialog (parent = null) {
|
|
163
|
+
this.editingParent = parent;
|
|
164
|
+
this.showParentDialog = true;
|
|
165
|
+
},
|
|
166
|
+
handleSave () {
|
|
167
|
+
this.editingParent = null;
|
|
168
|
+
this.showParentDialog = false;
|
|
169
|
+
this.$emit('refresh');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
</script>
|