ProfilePage.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <div>
  3. <DefaultHeaderPage />
  4. <div class="q-pa-md">
  5. <q-card flat class="profile-card">
  6. <div class="profile-header row items-center q-pa-lg">
  7. <div class="profile-avatar-wrap q-mr-md">
  8. <q-avatar size="76px" class="profile-avatar">
  9. <img v-if="user?.photo_url" :src="user.photo_url" style="object-fit: cover; width: 100%; height: 100%;" />
  10. <q-icon v-else name="mdi-account" size="44px" color="white" />
  11. </q-avatar>
  12. <q-btn
  13. round
  14. unelevated
  15. size="xs"
  16. icon="mdi-pencil-outline"
  17. class="profile-avatar-edit"
  18. color="white"
  19. text-color="violet-normal"
  20. :loading="uploadingAvatar"
  21. @click="triggerAvatarUpload"
  22. />
  23. <input
  24. ref="avatarInputRef"
  25. type="file"
  26. accept="image/*"
  27. style="display: none"
  28. @change="onAvatarSelected"
  29. />
  30. </div>
  31. <div class="column q-gutter-xs">
  32. <span class="text-white text-h6 text-weight-bold">{{ user?.name || '—' }}</span>
  33. <div class="row items-center q-gutter-xs">
  34. <q-badge
  35. :color="statusBadgeColor"
  36. class="text-capitalize"
  37. style="font-size:11px; padding: 3px 8px; border-radius: 20px;"
  38. >
  39. {{ statusLabel }}
  40. </q-badge>
  41. <q-btn
  42. v-if="user?.photo_url"
  43. flat
  44. round
  45. dense
  46. size="xs"
  47. icon="mdi-image-remove"
  48. color="white"
  49. :loading="removingAvatar"
  50. @click="onRemoveAvatar"
  51. />
  52. </div>
  53. <span class="text-white profile-validity text-caption">
  54. {{ $t('associado.validity') }} {{ user?.expiry_date || '—' }}
  55. </span>
  56. </div>
  57. </div>
  58. <div class="q-pa-md column">
  59. <div
  60. v-for="field in infoFields"
  61. :key="field.label"
  62. class="info-row row items-center no-wrap q-my-sm q-px-md"
  63. >
  64. <q-icon :name="field.icon" size="22px" class="info-icon q-mr-md" />
  65. <div class="column">
  66. <span class="info-label">{{ field.label }}</span>
  67. <span class="info-value">{{ field.value || '—' }}</span>
  68. </div>
  69. </div>
  70. </div>
  71. <div class="q-px-md">
  72. <div class="text-h6">{{ $t('associado.dependents') }}</div>
  73. <q-list v-if="dependents.length > 0" class="column q-mb-md">
  74. <div
  75. v-for="dep in dependents"
  76. :key="dep.id"
  77. class="dependent-row row items-center no-wrap q-my-md"
  78. >
  79. <q-avatar size="40px" class="dependent-avatar q-mr-md" text-color="white">
  80. {{ initials(dep.name) }}
  81. </q-avatar>
  82. <div class="col column">
  83. <span class="dependent-name">{{ dep.name }}</span>
  84. <span class="dependent-kinship">
  85. {{ $t(`associado.kinship_options.${dep.kinship}`) }}
  86. </span>
  87. </div>
  88. <q-badge
  89. :color="dependentStatusColor(dep.status)"
  90. class="q-mr-sm text-capitalize"
  91. style="font-size:11px; padding: 3px 8px; border-radius: 20px;"
  92. >
  93. {{ dependentStatusLabel(dep.status) }}
  94. </q-badge>
  95. </div>
  96. </q-list>
  97. <div v-else class="text-center q-py-md text-grey">
  98. {{ $t('associado.no_dependents') }}
  99. </div>
  100. <div class="flex justify-end q-pb-md">
  101. <q-btn
  102. unelevated
  103. icon="mdi-plus"
  104. :label="$t('common.actions.add').toUpperCase()"
  105. color="violet-normal"
  106. text-color="white"
  107. style="border-radius: 8px; padding: 8px 20px;"
  108. @click="onAddDependent"
  109. />
  110. </div>
  111. </div>
  112. </q-card>
  113. </div>
  114. </div>
  115. </template>
  116. <script setup>
  117. import { ref, computed, onMounted, defineAsyncComponent, useTemplateRef } from "vue";
  118. import { useQuasar } from "quasar";
  119. import { useI18n } from "vue-i18n";
  120. import { userStore } from "src/stores/user";
  121. import { getDependentsByUser } from "src/api/profile";
  122. import { uploadMyAvatar, deleteMyAvatar } from "src/api/user";
  123. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  124. const AddEditDependentDialog = defineAsyncComponent(
  125. () => import("src/pages/associado/profile/components/AddEditDependentDialog.vue"),
  126. );
  127. const $q = useQuasar();
  128. const { t } = useI18n();
  129. const store = userStore();
  130. const user = ref(null);
  131. const dependents = ref([]);
  132. const uploadingAvatar = ref(false);
  133. const removingAvatar = ref(false);
  134. const avatarInputRef = useTemplateRef("avatarInputRef");
  135. const statusBadgeColor = computed(() => {
  136. switch (user.value?.status) {
  137. case "active": return "positive";
  138. case "inactive": return "grey";
  139. case "canceled": return "negative";
  140. default: return "grey";
  141. }
  142. });
  143. const statusLabel = computed(() => {
  144. const map = { active: t("common.status.active"), inactive: t("common.status.inactive"), canceled: t("common.status.canceled") };
  145. return map[user.value?.status] ?? user.value?.status ?? "—";
  146. });
  147. const infoFields = computed(() => [
  148. { icon: "mdi-card-account-details-outline", label: t("common.terms.cpf"), value: user.value?.cpf },
  149. { icon: "mdi-email-outline", label: "E-mail", value: user.value?.email },
  150. { icon: "mdi-briefcase-outline", label: t("associado.position"), value: user.value?.position?.name },
  151. { icon: "mdi-domain", label: t("associado.sector"), value: user.value?.sector?.name },
  152. { icon: "mdi-card-text-outline", label: t("associado.registration"), value: user.value?.registration },
  153. { icon: "mdi-calendar-outline", label: t("associado.admission_date"), value: user.value?.admission_date },
  154. ]);
  155. const initials = (name) => {
  156. if (!name) return "?";
  157. return name.split(" ").slice(0, 2).map((w) => w[0]).join("").toUpperCase();
  158. };
  159. const dependentStatusColor = (status) => {
  160. switch (status) {
  161. case "approved": return "positive";
  162. case "refused": return "negative";
  163. case "pending": return "warning-light";
  164. default: return "grey";
  165. }
  166. };
  167. const dependentStatusLabel = (status) => {
  168. const map = {
  169. approved: t("associado.dependent_statuses.approved"),
  170. refused: t("associado.dependent_statuses.refused"),
  171. pending: t("associado.dependent_statuses.pending"),
  172. };
  173. return map[status] ?? status ?? "—";
  174. };
  175. const loadUser = async () => {
  176. await store.fetchUser();
  177. user.value = store.user;
  178. };
  179. const loadDependents = async () => {
  180. if (!user.value?.id) return;
  181. try {
  182. dependents.value = await getDependentsByUser(user.value.id);
  183. } catch {
  184. dependents.value = [];
  185. }
  186. };
  187. onMounted(async () => {
  188. await loadUser();
  189. await loadDependents();
  190. });
  191. const triggerAvatarUpload = () => {
  192. avatarInputRef.value?.click();
  193. };
  194. const onAvatarSelected = async (event) => {
  195. const file = event.target.files?.[0];
  196. if (!file) return;
  197. uploadingAvatar.value = true;
  198. try {
  199. const updated = await uploadMyAvatar(file);
  200. store.user = updated;
  201. user.value = updated;
  202. $q.notify({ type: "positive", message: t("http.success") });
  203. } catch {
  204. $q.notify({ type: "negative", message: t("http.errors.failed") });
  205. } finally {
  206. uploadingAvatar.value = false;
  207. event.target.value = "";
  208. }
  209. };
  210. const onRemoveAvatar = () => {
  211. $q.dialog({
  212. title: t("common.ui.messages.confirm_action"),
  213. message: t("associado.remove_photo"),
  214. cancel: true,
  215. persistent: true,
  216. }).onOk(async () => {
  217. removingAvatar.value = true;
  218. try {
  219. const updated = await deleteMyAvatar();
  220. store.user = updated;
  221. user.value = updated;
  222. $q.notify({ type: "positive", message: t("http.success") });
  223. } catch {
  224. $q.notify({ type: "negative", message: t("http.errors.failed") });
  225. } finally {
  226. removingAvatar.value = false;
  227. }
  228. });
  229. };
  230. const onAddDependent = () => {
  231. $q.dialog({
  232. component: AddEditDependentDialog,
  233. componentProps: {
  234. userId: user.value.id,
  235. title: () => t("common.actions.add") + " " + t("associado.dependent"),
  236. },
  237. }).onOk(() => loadDependents());
  238. };
  239. </script>
  240. <style lang="scss" scoped>
  241. @use "src/css/quasar.variables.scss" as *;
  242. .profile-card {
  243. border-radius: 12px;
  244. overflow: hidden;
  245. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  246. }
  247. .profile-header {
  248. background: linear-gradient(135deg, $violet-normal 0%, $violet-dark 100%);
  249. }
  250. .profile-avatar-wrap {
  251. position: relative;
  252. flex-shrink: 0;
  253. }
  254. .profile-avatar {
  255. background: rgba(255, 255, 255, 0.18) !important;
  256. // border: 2px solid rgba(255, 255, 255, 0.45);
  257. }
  258. .profile-avatar-edit {
  259. position: absolute;
  260. bottom: -2px;
  261. right: -2px;
  262. width: 22px;
  263. height: 22px;
  264. min-height: 22px;
  265. }
  266. .profile-validity {
  267. opacity: 0.85;
  268. }
  269. .info-row {
  270. background: $violet-light;
  271. border-radius: 8px;
  272. padding: 12px 16px;
  273. }
  274. .info-icon {
  275. color: $violet-normal;
  276. flex-shrink: 0;
  277. }
  278. .info-label {
  279. font-size: 11px;
  280. color: $color-text-2;
  281. line-height: 1.2;
  282. }
  283. .info-value {
  284. font-size: 14px;
  285. color: $color-text;
  286. font-weight: 500;
  287. line-height: 1.4;
  288. }
  289. .dependent-row {
  290. background: $violet-light;
  291. border-radius: 8px;
  292. padding: 10px 12px;
  293. }
  294. .dependent-avatar {
  295. background: $violet-normal !important;
  296. font-size: 14px;
  297. font-weight: 700;
  298. flex-shrink: 0;
  299. }
  300. .dependent-name {
  301. font-size: 14px;
  302. font-weight: 600;
  303. color: $color-text;
  304. line-height: 1.3;
  305. }
  306. .dependent-kinship {
  307. font-size: 12px;
  308. color: $color-text-2;
  309. line-height: 1.2;
  310. }
  311. </style>