Reports.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <script setup lang="ts">
  2. import { ref, computed, onMounted } from "vue";
  3. import { useStore } from "vuex";
  4. import Toast from "toasters";
  5. import { useModalState, useModalActions } from "@/vuex_helpers";
  6. import ReportInfoItem from "@/components/ReportInfoItem.vue";
  7. const store = useStore();
  8. const { socket } = store.state.websockets;
  9. const props = defineProps({
  10. modalUuid: { type: String, default: "" },
  11. modalModulePath: { type: String, default: "modals/editSong/MODAL_UUID" }
  12. });
  13. const tab = ref("sort-by-report");
  14. const icons = ref({
  15. duration: "timer",
  16. video: "tv",
  17. thumbnail: "image",
  18. artists: "record_voice_over",
  19. title: "title",
  20. custom: "lightbulb"
  21. });
  22. const tabs = ref({});
  23. const { reports } = useModalState("MODAL_MODULE_PATH", {
  24. modalUuid: props.modalUuid,
  25. modalModulePath: props.modalModulePath
  26. });
  27. const sortedByCategory = computed(() => {
  28. const categories = {};
  29. reports.forEach(report =>
  30. report.issues.forEach(issue => {
  31. if (categories[issue.category])
  32. categories[issue.category].push({
  33. ...issue,
  34. reportId: report._id
  35. });
  36. else
  37. categories[issue.category] = [
  38. { ...issue, reportId: report._id }
  39. ];
  40. })
  41. );
  42. return categories;
  43. });
  44. // const closeModal = payload =>
  45. // store.dispatch("modalVisibility/closeModal", payload);
  46. const { resolveReport } = useModalActions(
  47. "MODAL_MODULE_PATH",
  48. ["resolveReport"],
  49. {
  50. modalUuid: props.modalUuid,
  51. modalModulePath: props.modalModulePath
  52. }
  53. );
  54. const showTab = _tab => {
  55. tabs.value[`${_tab}-tab`].scrollIntoView({ block: "nearest" });
  56. tab.value = _tab;
  57. };
  58. const resolve = reportId => {
  59. socket.dispatch("reports.resolve", reportId, res => new Toast(res.message));
  60. };
  61. const toggleIssue = (reportId, issueId) => {
  62. socket.dispatch("reports.toggleIssue", reportId, issueId, res => {
  63. if (res.status !== "success") new Toast(res.message);
  64. });
  65. };
  66. onMounted(() => {
  67. socket.on(
  68. "event:admin.report.created",
  69. res => reports.unshift(res.data.report),
  70. { modalUuid: props.modalUuid }
  71. );
  72. socket.on(
  73. "event:admin.report.resolved",
  74. res => resolveReport(res.data.reportId),
  75. { modalUuid: props.modalUuid }
  76. );
  77. socket.on(
  78. "event:admin.report.issue.toggled",
  79. res => {
  80. reports.forEach((report, index) => {
  81. if (report._id === res.data.reportId) {
  82. const issue = reports[index].issues.find(
  83. issue => issue._id.toString() === res.data.issueId
  84. );
  85. issue.resolved = res.data.resolved;
  86. }
  87. });
  88. },
  89. { modalUuid: props.modalUuid }
  90. );
  91. });
  92. </script>
  93. <template>
  94. <div class="reports-tab tabs-container">
  95. <div class="tab-selection">
  96. <button
  97. class="button is-default"
  98. :ref="el => (tabs['sort-by-report-tab'] = el)"
  99. :class="{ selected: tab === 'sort-by-report' }"
  100. @click="showTab('sort-by-report')"
  101. >
  102. Sort by Report
  103. </button>
  104. <button
  105. class="button is-default"
  106. :ref="el => (tabs['sort-by-category-tab'] = el)"
  107. :class="{ selected: tab === 'sort-by-category' }"
  108. @click="showTab('sort-by-category')"
  109. >
  110. Sort by Category
  111. </button>
  112. </div>
  113. <div class="tab" v-if="tab === 'sort-by-category'">
  114. <div class="report-items" v-if="reports.length > 0">
  115. <div
  116. class="report-item"
  117. v-for="(issues, category) in sortedByCategory"
  118. :key="category"
  119. >
  120. <div class="report-item-header universal-item">
  121. <i
  122. class="material-icons"
  123. :content="category"
  124. v-tippy="{ theme: 'info' }"
  125. >
  126. {{ icons[category] }}
  127. </i>
  128. <p>{{ category }} Issues</p>
  129. </div>
  130. <div class="report-sub-items">
  131. <div
  132. class="report-sub-item report-sub-item-unresolved"
  133. :class="[
  134. 'report',
  135. issue.resolved
  136. ? 'report-sub-item-resolved'
  137. : 'report-sub-item-unresolved'
  138. ]"
  139. v-for="(issue, issueIndex) in issues"
  140. :key="issueIndex"
  141. >
  142. <i
  143. class="material-icons duration-icon report-sub-item-left-icon"
  144. :content="issue.category"
  145. v-tippy
  146. >
  147. {{ icons[category] }}
  148. </i>
  149. <p class="report-sub-item-info">
  150. <span class="report-sub-item-title">
  151. {{ issue.title }}
  152. </span>
  153. <span
  154. class="report-sub-item-description"
  155. v-if="issue.description"
  156. >
  157. {{ issue.description }}
  158. </span>
  159. </p>
  160. <div
  161. class="report-sub-item-actions universal-item-actions"
  162. >
  163. <i
  164. class="material-icons resolve-icon"
  165. content="Resolve"
  166. v-tippy
  167. v-if="!issue.resolved"
  168. @click="
  169. toggleIssue(issue.reportId, issue._id)
  170. "
  171. >
  172. done
  173. </i>
  174. <i
  175. class="material-icons unresolve-icon"
  176. content="Unresolve"
  177. v-tippy
  178. v-else
  179. @click="
  180. toggleIssue(issue.reportId, issue._id)
  181. "
  182. >
  183. remove
  184. </i>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </div>
  190. <p class="no-reports" v-else>There are no reports for this song.</p>
  191. </div>
  192. <div class="tab" v-if="tab === 'sort-by-report'">
  193. <div class="report-items" v-if="reports.length > 0">
  194. <div
  195. class="report-item"
  196. v-for="report in reports"
  197. :key="report._id"
  198. >
  199. <report-info-item
  200. :created-at="report.createdAt"
  201. :created-by="report.createdBy"
  202. >
  203. <template #actions>
  204. <i
  205. class="material-icons resolve-icon"
  206. content="Resolve all"
  207. v-tippy
  208. @click="resolve(report._id)"
  209. >
  210. done_all
  211. </i>
  212. </template>
  213. </report-info-item>
  214. <div class="report-sub-items">
  215. <div
  216. class="report-sub-item report-sub-item-unresolved"
  217. :class="[
  218. 'report',
  219. issue.resolved
  220. ? 'report-sub-item-resolved'
  221. : 'report-sub-item-unresolved'
  222. ]"
  223. v-for="(issue, issueIndex) in report.issues"
  224. :key="issueIndex"
  225. >
  226. <i
  227. class="material-icons duration-icon report-sub-item-left-icon"
  228. :content="issue.category"
  229. v-tippy
  230. >
  231. {{ icons[issue.category] }}
  232. </i>
  233. <p class="report-sub-item-info">
  234. <span class="report-sub-item-title">
  235. {{ issue.title }}
  236. </span>
  237. <span
  238. class="report-sub-item-description"
  239. v-if="issue.description"
  240. >
  241. {{ issue.description }}
  242. </span>
  243. </p>
  244. <div
  245. class="report-sub-item-actions universal-item-actions"
  246. >
  247. <i
  248. class="material-icons resolve-icon"
  249. content="Resolve"
  250. v-tippy
  251. v-if="!issue.resolved"
  252. @click="toggleIssue(report._id, issue._id)"
  253. >
  254. done
  255. </i>
  256. <i
  257. class="material-icons unresolve-icon"
  258. content="Unresolve"
  259. v-tippy
  260. v-else
  261. @click="toggleIssue(report._id, issue._id)"
  262. >
  263. remove
  264. </i>
  265. </div>
  266. </div>
  267. </div>
  268. </div>
  269. </div>
  270. <p class="no-reports" v-else>There are no reports for this song.</p>
  271. </div>
  272. </div>
  273. </template>
  274. <style lang="less" scoped>
  275. .night-mode {
  276. .report-items .report-item {
  277. background-color: var(--dark-grey-3) !important;
  278. }
  279. .report-items .report-item .report-item-header {
  280. background-color: var(--dark-grey-2) !important;
  281. }
  282. .label,
  283. p,
  284. strong {
  285. color: var(--light-grey-2);
  286. }
  287. .tabs-container .tab-selection .button {
  288. background: var(--dark-grey) !important;
  289. color: var(--white) !important;
  290. }
  291. }
  292. .reports-tab.tabs-container {
  293. .tab-selection {
  294. display: flex;
  295. overflow-x: auto;
  296. .button {
  297. border-radius: 0;
  298. border: 0;
  299. text-transform: uppercase;
  300. font-size: 14px;
  301. color: var(--dark-grey-3);
  302. background-color: var(--light-grey-2);
  303. flex-grow: 1;
  304. height: 32px;
  305. &:not(:first-of-type) {
  306. margin-left: 5px;
  307. }
  308. }
  309. .selected {
  310. background-color: var(--primary-color) !important;
  311. color: var(--white) !important;
  312. font-weight: 600;
  313. }
  314. }
  315. .tab {
  316. padding: 15px 0;
  317. border-radius: 0;
  318. }
  319. }
  320. .no-reports {
  321. text-align: center;
  322. }
  323. .report-items {
  324. .report-item {
  325. background-color: var(--white);
  326. border: 0.5px solid var(--primary-color);
  327. border-radius: @border-radius;
  328. padding: 8px;
  329. &:not(:first-of-type) {
  330. margin-bottom: 16px;
  331. }
  332. .report-item-header {
  333. justify-content: center;
  334. text-transform: capitalize;
  335. i {
  336. margin-right: 5px;
  337. }
  338. }
  339. .report-sub-items {
  340. .report-sub-item {
  341. border: 0.5px solid var(--black);
  342. margin-top: -1px;
  343. line-height: 24px;
  344. display: flex;
  345. padding: 4px;
  346. display: flex;
  347. &:first-child {
  348. border-radius: @border-radius @border-radius 0 0;
  349. }
  350. &:last-child {
  351. border-radius: 0 0 @border-radius @border-radius;
  352. }
  353. &.report-sub-item-resolved {
  354. .report-sub-item-description,
  355. .report-sub-item-title {
  356. text-decoration: line-through;
  357. }
  358. }
  359. .report-sub-item-left-icon {
  360. margin-right: 8px;
  361. margin-top: auto;
  362. margin-bottom: auto;
  363. }
  364. .report-sub-item-info {
  365. flex: 1;
  366. display: flex;
  367. flex-direction: column;
  368. .report-sub-item-title {
  369. font-size: 14px;
  370. }
  371. .report-sub-item-description {
  372. font-size: 12px;
  373. line-height: 16px;
  374. }
  375. }
  376. .report-sub-item-actions {
  377. height: 24px;
  378. margin-left: 8px;
  379. margin-top: auto;
  380. margin-bottom: auto;
  381. }
  382. }
  383. }
  384. .resolve-icon {
  385. color: var(--green);
  386. cursor: pointer;
  387. }
  388. .unresolve-icon {
  389. color: var(--dark-red);
  390. cursor: pointer;
  391. }
  392. }
  393. }
  394. </style>