@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.
@@ -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>