@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,276 @@
1
+ <template>
2
+ <div v-if="value" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
3
+ <div class="modal-dialog modal-dialog-scrollable modal-lg">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h5 class="modal-title">
7
+ {{ isEdit ? 'Edit Data Orang Tua/Wali' : 'Tambah Data Orang Tua/Wali' }}
8
+ </h5>
9
+ <button @click="close" type="button" class="btn-close"></button>
10
+ </div>
11
+ <div class="modal-body">
12
+ <div v-if="loading" class="text-center py-4">
13
+ <div class="spinner-border" role="status">
14
+ <span class="visually-hidden">Loading...</span>
15
+ </div>
16
+ </div>
17
+ <div v-else class="row g-3">
18
+ <!-- Hubungan Keluarga -->
19
+ <div class="col-12">
20
+ <label class="form-label fw-semibold">
21
+ Hubungan Keluarga <span class="text-danger">*</span>
22
+ </label>
23
+ <select v-model="formData.family_status" class="form-select" @change="changeFamilyStatus">
24
+ <option value="">Pilih hubungan</option>
25
+ <option value="father">Ayah</option>
26
+ <option value="mother">Ibu</option>
27
+ <option value="guardian">Wali</option>
28
+ </select>
29
+ <small class="form-text text-danger" v-if="errors.family_status">
30
+ {{ errors.family_status }}
31
+ </small>
32
+ </div>
33
+
34
+ <!-- Nama -->
35
+ <div class="col-12">
36
+ <label class="form-label fw-semibold">
37
+ Nama Lengkap <span class="text-danger">*</span>
38
+ </label>
39
+ <input v-model="formData.name" type="text" class="form-control" placeholder="Masukkan nama lengkap"
40
+ style="text-transform: uppercase" @input="formData.name = $event.target.value.toUpperCase()" />
41
+ <small class="form-text text-danger" v-if="errors.name">{{ errors.name }}</small>
42
+ </div>
43
+
44
+ <!-- NIK -->
45
+ <div class="col-12">
46
+ <label class="form-label fw-semibold">
47
+ NIK (Nomor Induk Kependudukan) <span class="text-danger">*</span>
48
+ </label>
49
+ <input v-model="formData.national_id" type="text" class="form-control" placeholder="Masukkan 16 digit NIK"
50
+ maxlength="16">
51
+ <small class="form-text text-danger" v-if="errors.national_id">
52
+ {{ errors.national_id }}
53
+ </small>
54
+ </div>
55
+
56
+ <!-- Status -->
57
+ <div class="col-12">
58
+ <label class="form-label fw-semibold">
59
+ Status <span class="text-danger">*</span>
60
+ </label>
61
+ <select v-model="formData.status" class="form-select">
62
+ <option value="">Pilih status</option>
63
+ <option value="hidup">Masih Hidup</option>
64
+ <option value="meninggal">Sudah Meninggal</option>
65
+ </select>
66
+ <small class="form-text text-danger" v-if="errors.status">{{ errors.status }}</small>
67
+ </div>
68
+
69
+ <!-- Family Relation (for Guardian) -->
70
+ <div class="col-12" v-if="formData.family_status == 'guardian' && formData.status !== 'meninggal'">
71
+ <label class="form-label fw-semibold">
72
+ Hubungan Keluarga <span class="text-danger">*</span>
73
+ </label>
74
+ <input v-model="formData.family_relation" type="text" class="form-control" placeholder="Contoh: Paman">
75
+ <small class="form-text text-danger" v-if="errors.family_relation">
76
+ {{ errors.family_relation }}
77
+ </small>
78
+ </div>
79
+
80
+ <!-- Pendidikan -->
81
+ <div class="col-12" v-if="formData.status !== 'meninggal'">
82
+ <label class="form-label fw-semibold">
83
+ Pendidikan Terakhir <span class="text-danger">*</span>
84
+ </label>
85
+ <select v-model="formData.education" class="form-select">
86
+ <option value="">Pilih pendidikan terakhir</option>
87
+ <option v-for="(label, value) in educations" :key="value" :value="value">
88
+ {{ label }}
89
+ </option>
90
+ </select>
91
+ <small class="form-text text-danger" v-if="errors.education">
92
+ {{ errors.education }}
93
+ </small>
94
+ </div>
95
+
96
+ <!-- Pekerjaan -->
97
+ <div class="col-12" v-if="formData.status !== 'meninggal'">
98
+ <label class="form-label fw-semibold">
99
+ Pekerjaan <span class="text-danger">*</span>
100
+ </label>
101
+ <input v-model="formData.work" type="text" class="form-control" placeholder="Masukan pekerjaan">
102
+ <small class="form-text text-danger" v-if="errors.work">{{ errors.work }}</small>
103
+ </div>
104
+
105
+ <!-- Penghasilan -->
106
+ <div class="col-12" v-if="formData.status !== 'meninggal'">
107
+ <label class="form-label fw-semibold">
108
+ Penghasilan per Bulan <span class="text-danger">*</span>
109
+ </label>
110
+ <select v-model="formData.income" class="form-select">
111
+ <option value="">Pilih range penghasilan</option>
112
+ <option v-for="(label, value) in salaries" :key="value" :value="value">
113
+ {{ label }}
114
+ </option>
115
+ </select>
116
+ <small class="form-text text-danger" v-if="errors.income">{{ errors.income }}</small>
117
+ </div>
118
+
119
+ <!-- No HP -->
120
+ <div class="col-12" v-if="formData.status !== 'meninggal'">
121
+ <label class="form-label fw-semibold">
122
+ Nomor HP <span class="text-danger">*</span>
123
+ </label>
124
+ <input v-model="formData.phone" type="tel" class="form-control" placeholder="Contoh: 081234567890">
125
+ <small class="form-text text-danger" v-if="errors.phone">{{ errors.phone }}</small>
126
+ </div>
127
+
128
+ <!-- Alamat -->
129
+ <div class="col-12" v-if="formData.status !== 'meninggal'">
130
+ <label class="form-label fw-semibold">
131
+ Alamat Lengkap <span class="text-danger">*</span>
132
+ </label>
133
+ <textarea v-model="formData.address" class="form-control" rows="3"
134
+ placeholder="Masukan alamat lengkap"></textarea>
135
+ <small class="form-text text-danger" v-if="errors.address">{{ errors.address }}</small>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="modal-footer">
141
+ <button @click="save" type="button" class="btn btn-primary rounded-pill px-4">
142
+ Simpan
143
+ </button>
144
+ <button @click="close" type="button" class="btn btn-outline-secondary rounded-pill px-4">
145
+ Batal
146
+ </button>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </template>
152
+
153
+ <script>
154
+ import { getParentByStatus, addOrUpdateParent } from '../api/profileApi';
155
+ import { showToast } from '../utils/toast';
156
+
157
+ export default {
158
+ name: 'ParentModal',
159
+ props: {
160
+ value: {
161
+ type: Boolean,
162
+ default: false
163
+ },
164
+ parent: {
165
+ type: Object,
166
+ default: null
167
+ },
168
+ salaries: {
169
+ type: Object,
170
+ required: true
171
+ },
172
+ educations: {
173
+ type: Object,
174
+ required: true
175
+ }
176
+ },
177
+ data() {
178
+ return {
179
+ formData: {
180
+ family_status: '',
181
+ name: '',
182
+ status: '',
183
+ national_id: '',
184
+ education: '',
185
+ work: '',
186
+ income: '',
187
+ phone: '',
188
+ address: '',
189
+ family_relation: ''
190
+ },
191
+ errors: {},
192
+ loading: false
193
+ };
194
+ },
195
+ computed: {
196
+ isEdit() {
197
+ return !!this.parent;
198
+ }
199
+ },
200
+ watch: {
201
+ value(newVal) {
202
+ if (newVal) {
203
+ if (this.parent) {
204
+ this.formData = { ...this.parent };
205
+ } else {
206
+ this.resetForm();
207
+ }
208
+ }
209
+ }
210
+ },
211
+ mounted() {
212
+ this.$root.$on('set-family-status', (status) => {
213
+ this.formData.family_status = status;
214
+ this.changeFamilyStatus();
215
+ });
216
+ },
217
+ beforeDestroy() {
218
+ this.$root.$off('set-family-status');
219
+ },
220
+ methods: {
221
+ resetForm() {
222
+ this.formData = {
223
+
224
+ };
225
+ this.errors = {};
226
+ },
227
+ async changeFamilyStatus() {
228
+ if (!this.formData.family_status) return;
229
+ this.loading = true;
230
+ try {
231
+ const response = await getParentByStatus(this.formData.family_status);
232
+ if (response.data.data) {
233
+ this.formData = response.data.data;
234
+ }
235
+ } catch (error) {
236
+ console.error('Error fetching parent data:', error);
237
+ } finally {
238
+ this.loading = false;
239
+ }
240
+ },
241
+ async save() {
242
+ this.errors = {};
243
+ try {
244
+ await addOrUpdateParent(this.formData.family_status, this.formData);
245
+ showToast({
246
+ icon: 'success',
247
+ title: 'Success',
248
+ text: 'Data orang tua/wali berhasil disimpan.'
249
+ });
250
+ this.$emit('save');
251
+ this.close();
252
+ if (this.$route.query.r) {
253
+ this.$router.push(this.$route.query.r);
254
+ }
255
+ } catch (error) {
256
+ this.errors = error.response?.data?.errors || {};
257
+ showToast({
258
+ icon: 'error',
259
+ title: 'Error',
260
+ text: 'Gagal menyimpan data: ' + error.response?.data?.user_message
261
+ });
262
+ }
263
+ },
264
+ close() {
265
+ this.resetForm();
266
+ this.$emit('input', false);
267
+ }
268
+ }
269
+ };
270
+ </script>
271
+
272
+ <style scoped>
273
+ .modal.show {
274
+ display: block;
275
+ }
276
+ </style>
@@ -0,0 +1,256 @@
1
+ <template>
2
+ <div v-if="value" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
3
+ <div class="modal-dialog modal-dialog-scrollable modal-lg">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h5 class="modal-title">Edit Data Pribadi</h5>
7
+ <button @click="close" type="button" class="btn-close"></button>
8
+ </div>
9
+ <div class="modal-body">
10
+ <div class="row g-3">
11
+ <div class="col-md-6">
12
+ <label class="form-label fw-semibold">Nama Lengkap</label>
13
+ <input type="text" class="form-control" v-model="formData.name" placeholder="Nama Lengkap"
14
+ style="text-transform: uppercase" @input="formData.name = $event.target.value.toUpperCase()" />
15
+ </div>
16
+ <div class="col-md-6">
17
+ <label class="form-label fw-semibold">Nama Panggilan</label>
18
+ <input type="text" class="form-control" v-model="formData.nick_name" placeholder="Nama Panggilan"
19
+ style="text-transform: uppercase" @input="formData.nick_name = $event.target.value.toUpperCase()" />
20
+ </div>
21
+ <div class="col-md-6">
22
+ <label class="form-label fw-semibold">Email</label>
23
+ <input type="email" class="form-control" v-model="formData.email" placeholder="Email" readonly />
24
+ </div>
25
+ <div class="col-md-6">
26
+ <label class="form-label fw-semibold">Phone</label>
27
+ <input type="text" class="form-control" v-model="formData.phone" placeholder="No HP"
28
+ @input="formatPhone" />
29
+ </div>
30
+ <div class="col-md-6">
31
+ <label class="form-label fw-semibold">NIK</label>
32
+ <input type="text" class="form-control" v-model="formData.national_id" placeholder="16 digit NIK"
33
+ maxlength="16" />
34
+ </div>
35
+ <div class="col-md-6">
36
+ <label class="form-label fw-semibold">No KK</label>
37
+ <input type="text" class="form-control" v-model="formData.family_national_id" placeholder="16 digit No KK"
38
+ maxlength="16" />
39
+ </div>
40
+ <div class="col-md-6">
41
+ <label class="form-label fw-semibold">Tempat Lahir</label>
42
+ <input type="text" class="form-control" v-model="formData.place_of_birth" placeholder="Tempat Lahir" />
43
+ </div>
44
+ <div class="col-md-6">
45
+ <label class="form-label fw-semibold">Tanggal Lahir</label>
46
+ <DatePicker v-model="formData.birth_date" format="YYYY-MM-DD" value-type="format" @input="hitungUmur" />
47
+ <div v-if="ageInfo.age !== null" class="mt-2">
48
+ <small :class="isAgeRestricted ? 'text-danger' : 'text-muted'">
49
+ Umur per 31 Juli {{ currentYear }}: <strong>{{ ageInfo.text }}</strong>
50
+ </small>
51
+ <div v-if="isAgeRestricted" class="text-danger mt-1">
52
+ <small><i class="bi bi-exclamation-circle"></i> Tidak dapat menyimpan data untuk usia 21 tahun atau
53
+ lebih</small>
54
+ </div>
55
+ </div>
56
+ <div v-if="loadingAge" class="mt-2">
57
+ <small class="text-muted">Menghitung umur...</small>
58
+ </div>
59
+ </div>
60
+ <div class="col-md-6">
61
+ <label class="form-label fw-semibold">Jenis Kelamin</label>
62
+ <div>
63
+ <input type="radio" v-model="formData.sex" value="1" id="male" />
64
+ <label for="male">Laki-Laki</label>
65
+ <input type="radio" v-model="formData.sex" value="2" id="female" class="ms-3" />
66
+ <label for="female">Perempuan</label>
67
+ </div>
68
+ </div>
69
+ <div class="col-md-12">
70
+ <label class="form-label fw-semibold">Alamat Lengkap</label>
71
+ <textarea class="form-control" v-model="formData.address" placeholder="Alamat Lengkap"
72
+ rows="3"></textarea>
73
+ </div>
74
+ <div class="col-md-12">
75
+ <label class="form-label fw-semibold">Riwayat Penyakit</label>
76
+ <textarea class="form-control" v-model="formData.medical_history" placeholder="Riwayat Penyakit"
77
+ rows="3"></textarea>
78
+ </div>
79
+ <div class="col-md-12">
80
+ <label class="form-label fw-semibold">Kabupaten/Kota</label>
81
+ <v-select v-model="formData.location" :options="locations"
82
+ placeholder="Ketik untuk mencari kabupaten/kota..." @search="handleSearch" :filterable="false">
83
+ <template #no-options>
84
+ Ketik untuk mencari kabupaten/kota...
85
+ </template>
86
+ </v-select>
87
+ </div>
88
+ <div class="col-md-12">
89
+ <SgFileUploader v-model="formData.national_card" label="Upload KTP" />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <div class="modal-footer">
94
+ <button v-if="!isAgeRestricted" @click="save" type="button" class="btn btn-primary rounded-pill px-4">
95
+ Simpan
96
+ </button>
97
+ <button @click="close" type="button" class="btn btn-outline-secondary rounded-pill px-4">
98
+ Batal
99
+ </button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </template>
105
+
106
+ <script>
107
+ import DatePicker from "vue2-datepicker";
108
+ import "vue2-datepicker/index.css";
109
+ import vSelect from 'vue-select';
110
+ import 'vue-select/dist/vue-select.css';
111
+ import SgFileUploader from '../components/SgFileUploader.vue';
112
+ import { checkAge } from '../api/profileApi';
113
+
114
+ export default {
115
+ name: 'PersonalDataEditModal',
116
+ components: {
117
+ DatePicker,
118
+ vSelect,
119
+ SgFileUploader
120
+ },
121
+ props: {
122
+ value: {
123
+ type: Boolean,
124
+ default: false
125
+ },
126
+ user: {
127
+ type: Object,
128
+ required: true
129
+ },
130
+ locations: {
131
+ type: Array,
132
+ default: () => []
133
+ }
134
+ },
135
+ data() {
136
+ return {
137
+ formData: {},
138
+ ageInfo: {
139
+ age: null,
140
+ text: null
141
+ },
142
+ loadingAge: false,
143
+ currentYear: new Date().getFullYear()
144
+ };
145
+ },
146
+ watch: {
147
+ value(newVal) {
148
+ if (newVal) {
149
+ this.formData = { ...this.user };
150
+ if (this.formData.birth_date) {
151
+ this.hitungUmur();
152
+ }
153
+ }
154
+ }
155
+ },
156
+ computed: {
157
+ isAgeRestricted() {
158
+ const res = (Array.isArray(this.user.role) ? this.user.role.includes('user') || this.user.role.includes('c_thalabah') : this.user.role === 'user' || this.user.role === 'c_thalabah') && this.ageInfo.age >= 21;
159
+ return res;
160
+ }
161
+ },
162
+ methods: {
163
+ formatPhone(event) {
164
+ this.formData.phone = event.target.value
165
+ .replace(/[^\d+]/g, '')
166
+ .replace(/(\d{4})/g, '$1 ');
167
+ },
168
+ handleSearch(search, loading) {
169
+ this.$emit('search-locations', search, loading);
170
+ },
171
+ async hitungUmur() {
172
+ if (!this.formData.birth_date) {
173
+ this.ageInfo.age = null;
174
+ return;
175
+ }
176
+
177
+ this.loadingAge = true;
178
+ try {
179
+ const response = await checkAge(this.formData.birth_date);
180
+ if (response.data && response.data.data) {
181
+ this.ageInfo.age = response.data.data.y;
182
+ this.ageInfo.text = response.data.data.text;
183
+ }
184
+ } catch (error) {
185
+ console.error('Error menghitung umur:', error);
186
+ this.ageInfo.age = null;
187
+ this.ageInfo.text = null;
188
+ } finally {
189
+ this.loadingAge = false;
190
+ }
191
+ },
192
+ save() {
193
+ if (this.isAgeRestricted) {
194
+ alert('Tidak dapat menyimpan data untuk usia 21 tahun atau lebih per 31 Juli ' + this.currentYear);
195
+ return;
196
+ }
197
+ this.$emit('save', this.formData);
198
+ },
199
+ close() {
200
+ this.$emit('input', false);
201
+ }
202
+ }
203
+ };
204
+ </script>
205
+
206
+ <style scoped>
207
+ .modal.show {
208
+ display: block;
209
+ }
210
+
211
+ .v-select {
212
+ width: 100%;
213
+ }
214
+
215
+ .v-select .vs__dropdown-toggle {
216
+ padding: 0.375rem 0.75rem;
217
+ border: 1px solid #ced4da;
218
+ border-radius: 0.25rem;
219
+ }
220
+
221
+ .v-select .vs__search {
222
+ margin: 0;
223
+ padding: 0;
224
+ border: none;
225
+ }
226
+
227
+ .v-select .vs__search:focus {
228
+ outline: none;
229
+ }
230
+
231
+ .v-select .vs__dropdown-menu {
232
+ border: 1px solid #ced4da;
233
+ border-radius: 0.25rem;
234
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
235
+ max-height: 250px;
236
+ overflow-y: auto;
237
+ }
238
+
239
+ .v-select .vs__dropdown-option {
240
+ padding: 0.5rem 0.75rem;
241
+ }
242
+
243
+ .v-select .vs__dropdown-option--highlight {
244
+ background: #0d6efd;
245
+ color: white;
246
+ }
247
+
248
+ .v-select .vs__selected {
249
+ margin: 0;
250
+ padding: 0;
251
+ }
252
+
253
+ .v-select .vs__clear {
254
+ margin-right: 5px;
255
+ }
256
+ </style>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="modal fade" :class="{ 'show': value }" :style="{ display: value ? 'block' : 'none' }" tabindex="-1"
3
+ aria-labelledby="previewModalLabel" aria-hidden="true">
4
+ <div class="modal-dialog modal-dialog-centered">
5
+ <div class="modal-content">
6
+ <div class="modal-header">
7
+ <h5 class="modal-title" id="previewModalLabel">Preview Dokumen</h5>
8
+ <button type="button" class="btn-close" @click="close" aria-label="Close"></button>
9
+ </div>
10
+ <div class="modal-body">
11
+ <img v-if="url" :src="url" class="img-fluid" referrerpolicy="no-referrer" alt="Preview" />
12
+ <div v-else class="text-center py-4">
13
+ <p class="text-muted">Tidak ada dokumen untuk ditampilkan</p>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script>
22
+ export default {
23
+ name: 'PreviewModal',
24
+ props: {
25
+ value: {
26
+ type: Boolean,
27
+ default: false
28
+ },
29
+ url: {
30
+ type: String,
31
+ default: null
32
+ }
33
+ },
34
+ mounted () {
35
+ this.$root.$on('show-preview', (url) => {
36
+ this.$emit('input', true);
37
+ this.$emit('update:url', url);
38
+ });
39
+ },
40
+ beforeDestroy () {
41
+ this.$root.$off('show-preview');
42
+ },
43
+ methods: {
44
+ close () {
45
+ this.$emit('input', false);
46
+ }
47
+ }
48
+ };
49
+ </script>
50
+
51
+ <style scoped>
52
+ .modal.show {
53
+ display: block;
54
+ }
55
+ </style>