You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

664 lines
19 KiB

  1. <script>
  2. import Layout from "@/layout/custom.vue";
  3. import ApiServiece from "@/services/ApiService";
  4. import moment from "jalali-moment";
  5. import VueSelect from "vue3-select-component";
  6. import { onMounted, ref, watch, computed } from "vue";
  7. import { toast } from "vue3-toastify";
  8. import "vue3-toastify/dist/index.css";
  9. import Swal from "sweetalert2";
  10. import showDescription from "@/components/modals/commonModals/showDescription.vue";
  11. export default {
  12. name: "BORDER",
  13. components: {
  14. Layout,
  15. VueSelect,
  16. showDescription,
  17. },
  18. setup() {
  19. const products = ref([]);
  20. const selectedProduct = ref();
  21. const blogs = ref([]);
  22. const selectedBlog = ref();
  23. const selectedCommentType = ref("");
  24. const comment = ref();
  25. const searchPage = ref();
  26. const currentPage = ref(1);
  27. const totalPages = ref(1);
  28. const paginate = ref(20);
  29. const page = ref(1);
  30. const selectedStatus = ref("");
  31. const filterLoading = ref(false);
  32. const searchQuery = ref("");
  33. const comments = ref();
  34. const convertToJalali = (date) => {
  35. return moment(date, "YYYY-MM-DD HH:mm:ss")
  36. .locale("fa")
  37. .format("YYYY/MM/DD");
  38. };
  39. watch(searchQuery, () => {
  40. getComments();
  41. page.value = 1;
  42. });
  43. const getComments = () => {
  44. filterLoading.value = true;
  45. ApiServiece.get(
  46. `admin/comments?type=${selectedCommentType.value || ""}&status=${
  47. selectedStatus.value || ""
  48. }&commentable_id=${
  49. selectedBlog.value ?? selectedProduct.value ?? ""
  50. }&paginate=${paginate.value || 10}&page=${page.value || 1}`
  51. )
  52. .then((resp) => {
  53. filterLoading.value = false;
  54. comments.value = resp.data.data.data;
  55. console.log(comments.value);
  56. currentPage.value = resp.data.data.current_page;
  57. totalPages.value = resp.data.data.last_page;
  58. })
  59. .catch(() => {
  60. filterLoading.value = false;
  61. });
  62. };
  63. const handleBlogSearch = async (searchTerm) => {
  64. if (searchTerm.length < 3) return;
  65. try {
  66. const response = await ApiServiece.get(
  67. `admin/blogs?title=${searchTerm}`
  68. );
  69. blogs.value = response.data.data;
  70. } catch (error) {
  71. blogs.value = [];
  72. }
  73. };
  74. const formattedBlog = computed(() =>
  75. Array.isArray(blogs.value)
  76. ? blogs.value.map((blog) => ({
  77. value: blog.id,
  78. label: blog.title,
  79. }))
  80. : []
  81. );
  82. const handleProductsSearch = async (searchTerm) => {
  83. if (searchTerm.length < 3) return;
  84. try {
  85. const response = await ApiServiece.get(
  86. `admin/products?title=${searchTerm}`
  87. );
  88. products.value = response.data.data;
  89. } catch (error) {
  90. products.value = [];
  91. }
  92. };
  93. const formattedProducts = computed(() =>
  94. Array.isArray(products.value)
  95. ? products.value.map((product) => ({
  96. value: product.id,
  97. label: product.title,
  98. }))
  99. : []
  100. );
  101. const modalData = (text) => {
  102. comment.value = text;
  103. };
  104. const visiblePages = computed(() => {
  105. const pages = [];
  106. if (totalPages.value <= 5) {
  107. for (let i = 1; i <= totalPages.value; i++) {
  108. pages.push(i);
  109. }
  110. } else {
  111. let start = currentPage.value - 2;
  112. let end = currentPage.value + 2;
  113. if (start < 1) start = 1;
  114. if (end > totalPages.value) end = totalPages.value;
  115. for (let i = start; i <= end; i++) {
  116. pages.push(i);
  117. }
  118. }
  119. return pages;
  120. });
  121. const deleteDiscount = (id, title) => {
  122. Swal.fire({
  123. text: `می خواهید تخفیف ${title} را حذف کنید؟`,
  124. icon: "warning",
  125. showCancelButton: true,
  126. confirmButtonColor: "#3085d6",
  127. cancelButtonColor: "#d33",
  128. confirmButtonText: "بله!",
  129. cancelButtonText: "خیر",
  130. }).then((result) => {
  131. if (result.isConfirmed) {
  132. ApiServiece.delete(`admin/discounts/${id}`)
  133. .then(() => {
  134. toast.success("!تخفیف با موفقیت حذف شد", {
  135. position: "top-right",
  136. autoClose: 3000,
  137. });
  138. comments.value = comments.value.filter(
  139. (comment) => comment.id !== id
  140. );
  141. })
  142. .catch((err) => {
  143. console.log(err);
  144. toast.error("!مشکلی در حذف کردن تخفیف پیش آمد", {
  145. position: "top-right",
  146. autoClose: 3000,
  147. });
  148. });
  149. }
  150. });
  151. };
  152. function handlePageInput() {
  153. if (searchPage.value < 1) {
  154. searchPage.value = 1;
  155. } else if (searchPage.value > totalPages.value) {
  156. searchPage.value = totalPages.value;
  157. }
  158. if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
  159. page.value = searchPage.value;
  160. }
  161. }
  162. const changeCommentStatus = (id, op) => {
  163. let text, successMessage, errorMessage;
  164. if (op === "confirmed") {
  165. text = ` می خواهید این نظر را قبول کنید؟`;
  166. successMessage = "!نظر با موفقیت قبول شد";
  167. errorMessage = "!مشکلی در تغییر وضعیت نظر ایجاد شد";
  168. } else if (op === "rejected") {
  169. text = `می خواهید این نظر را رد کنید؟`;
  170. successMessage = "!نظر با موفقیت رد شد";
  171. errorMessage = "!مشکلی در تغییر وضعیت نظر ایجاد شد";
  172. }
  173. Swal.fire({
  174. text: text,
  175. icon: "warning",
  176. showCancelButton: true,
  177. confirmButtonColor: "#3085d6",
  178. cancelButtonColor: "#d33",
  179. confirmButtonText: "بله!",
  180. cancelButtonText: "خیر",
  181. }).then((result) => {
  182. if (result.isConfirmed) {
  183. const formData = new FormData();
  184. formData.append("status", op);
  185. ApiServiece.put(`admin/comments/${id}`, formData)
  186. .then(() => {
  187. toast.success(successMessage, {
  188. position: "top-right",
  189. autoClose: 3000,
  190. });
  191. getComments();
  192. })
  193. .catch(() => {
  194. toast.error(errorMessage, {
  195. position: "top-right",
  196. autoClose: 3000,
  197. });
  198. });
  199. }
  200. });
  201. };
  202. watch(searchQuery, () => {
  203. getComments();
  204. });
  205. watch(page, () => {
  206. getComments();
  207. });
  208. watch(selectedStatus, () => {
  209. getComments();
  210. });
  211. watch(selectedCommentType, () => {
  212. selectedProduct.value = "";
  213. selectedBlog.value = "";
  214. getComments();
  215. });
  216. watch([selectedBlog, selectedProduct], () => {
  217. getComments();
  218. });
  219. const nextPage = () => {
  220. if (currentPage.value < totalPages.value) {
  221. page.value++;
  222. getComments();
  223. }
  224. };
  225. const prevPage = () => {
  226. if (currentPage.value > 1) {
  227. page.value--;
  228. getComments();
  229. }
  230. };
  231. onMounted(() => {
  232. getComments();
  233. });
  234. return {
  235. comments,
  236. convertToJalali,
  237. changeCommentStatus,
  238. deleteDiscount,
  239. searchQuery,
  240. filterLoading,
  241. currentPage,
  242. totalPages,
  243. nextPage,
  244. prevPage,
  245. page,
  246. handlePageInput,
  247. searchPage,
  248. visiblePages,
  249. modalData,
  250. comment,
  251. selectedCommentType,
  252. selectedStatus,
  253. handleBlogSearch,
  254. formattedBlog,
  255. selectedBlog,
  256. formattedProducts,
  257. handleProductsSearch,
  258. selectedProduct,
  259. };
  260. },
  261. };
  262. </script>
  263. <template>
  264. <Layout>
  265. <BRow>
  266. <div class="col-md-12">
  267. <div class="card shadow-sm border-0 rounded">
  268. <div
  269. class="card-header d-flex justify-content-between align-items-center p-3"
  270. dir="rtl"
  271. >
  272. <div class="d-flex align-items-center">
  273. <select
  274. class="form-select form-select-sm"
  275. v-model="selectedCommentType"
  276. style="width: 120px; border-radius: 15px"
  277. >
  278. <option value="" disabled selected>نوع نظر</option>
  279. <option value="">همه</option>
  280. <option value="product">محصول</option>
  281. <option value="blog">بلاگ</option>
  282. </select>
  283. <select
  284. class="form-select form-select-sm"
  285. v-model="selectedStatus"
  286. style="width: 120px; border-radius: 15px; margin-right: 7px"
  287. >
  288. <option value="" disabled selected>وضعیت</option>
  289. <option value="">همه</option>
  290. <option value="confirmed">تایید شده</option>
  291. <option value="rejected">رد شده</option>
  292. <option value="pending">معلق</option>
  293. </select>
  294. <VueSelect
  295. v-if="selectedCommentType === 'blog'"
  296. style="
  297. --vs-border-radius: 16px;
  298. margin-right: 7px;
  299. --vs-min-height: 18px;
  300. "
  301. v-model="selectedBlog"
  302. :options="formattedBlog"
  303. placeholder="بلاگی را انتخاب کنید"
  304. @search="handleBlogSearch"
  305. />
  306. <VueSelect
  307. v-if="selectedCommentType === 'product'"
  308. style="
  309. --vs-border-radius: 16px;
  310. margin-right: 7px;
  311. --vs-min-height: 18px;
  312. "
  313. v-model="selectedProduct"
  314. :options="formattedProducts"
  315. placeholder="محصولی را انتخاب کنید"
  316. @search="handleProductsSearch"
  317. />
  318. </div>
  319. </div>
  320. <div v-if="!filterLoading" class="card-body table-border-style p-0">
  321. <div class="table-responsive">
  322. <table class="table table-hover table-bordered m-0" dir="rtl">
  323. <thead class="table-light">
  324. <tr>
  325. <th>نویسنده</th>
  326. <th>عنوان</th>
  327. <th>نام محصول</th>
  328. <th>امتیاز</th>
  329. <th>نظر</th>
  330. <th>وضعیت</th>
  331. <th>تاریخ ایجاد</th>
  332. <th>عملیات</th>
  333. </tr>
  334. </thead>
  335. <tbody>
  336. <tr v-for="comment in comments" :key="comment.id">
  337. <td>{{ comment?.user?.name }}</td>
  338. <td v-if="comment?.commentable_type === 'product'">
  339. محصول
  340. </td>
  341. <td v-if="comment?.commentable_type === 'blog'">بلاگ</td>
  342. <td>{{ comment.commentable.title }}</td>
  343. <td>
  344. <span
  345. v-for="n in 5"
  346. :key="n"
  347. :class="
  348. n <= comment.rate ? 'fa fa-star' : 'fa fa-star-o'
  349. "
  350. ></span>
  351. </td>
  352. <td
  353. data-bs-toggle="modal"
  354. data-bs-target="#showDescription"
  355. @click="modalData(comment.text)"
  356. class="comment-td"
  357. >
  358. <span class="comment-text">
  359. {{
  360. comment.text.length > 20
  361. ? comment.text.substring(0, 20) + "..."
  362. : comment.text
  363. }}
  364. </span>
  365. </td>
  366. <td>
  367. <span
  368. :class="{
  369. badge: true,
  370. 'bg-success': comment.status === 'confirmed',
  371. 'bg-danger': comment.status === 'rejected',
  372. 'bg-warning': comment.status === 'pending',
  373. }"
  374. >
  375. {{
  376. comment.status === "confirmed"
  377. ? "تایید شده"
  378. : comment.status === "rejected"
  379. ? "رد شده"
  380. : comment.status === "pending"
  381. ? "معلق"
  382. : comment.status
  383. }}
  384. </span>
  385. </td>
  386. <td>{{ convertToJalali(comment?.created_at) }}</td>
  387. <td>
  388. <div class="dropdown">
  389. <button
  390. class="btn btn-sm btn-outline-primary dropdown-toggle"
  391. type="button"
  392. id="dropdownMenuButton"
  393. data-bs-toggle="dropdown"
  394. aria-expanded="false"
  395. >
  396. تغییر وضعیت
  397. </button>
  398. <ul
  399. class="dropdown-menu"
  400. aria-labelledby="dropdownMenuButton"
  401. >
  402. <li
  403. v-if="
  404. comment.status === 'rejected' ||
  405. comment.status === 'pending'
  406. "
  407. >
  408. <a
  409. class="dropdown-item"
  410. href="#"
  411. @click="
  412. changeCommentStatus(comment.id, 'confirmed')
  413. "
  414. >
  415. <i class="ph-duotone ph-check-circle"></i> قبول
  416. نظر
  417. </a>
  418. </li>
  419. <li
  420. v-if="
  421. comment.status === 'confirmed' ||
  422. comment.status === 'pending'
  423. "
  424. >
  425. <a
  426. class="dropdown-item"
  427. href="#"
  428. @click="
  429. changeCommentStatus(comment.id, 'rejected')
  430. "
  431. >
  432. <i class="ph-duotone ph-x-circle"></i> رد نظر
  433. </a>
  434. </li>
  435. </ul>
  436. </div>
  437. </td>
  438. </tr>
  439. </tbody>
  440. </table>
  441. </div>
  442. </div>
  443. <div
  444. v-else
  445. class="filter-loader card table-card user-profile-list"
  446. ></div>
  447. </div>
  448. </div>
  449. <showDescription :desc="comment" />
  450. </BRow>
  451. <BRow>
  452. <BCol sm="12">
  453. <div class="d-flex justify-content-center">
  454. <nav aria-label="Page navigation">
  455. <ul class="pagination">
  456. <li class="page-item" :class="{ disabled: currentPage === 1 }">
  457. <span class="page-link" @click="prevPage">قبلی</span>
  458. </li>
  459. <li v-if="currentPage > 2" class="page-item" @click="page = 1">
  460. <a class="page-link" href="javascript:void(0)">1</a>
  461. </li>
  462. <li v-if="currentPage > 3" class="page-item" disabled>
  463. <span class="page-link">...</span>
  464. </li>
  465. <li
  466. v-for="n in visiblePages"
  467. :key="n"
  468. class="page-item"
  469. :class="{ active: currentPage === n }"
  470. >
  471. <a
  472. class="page-link"
  473. href="javascript:void(0)"
  474. @click="page = n"
  475. >
  476. {{ n }}
  477. </a>
  478. </li>
  479. <li
  480. v-if="currentPage < totalPages - 2"
  481. class="page-item"
  482. disabled
  483. >
  484. <span class="page-link">...</span>
  485. </li>
  486. <li
  487. v-if="currentPage < totalPages - 1"
  488. class="page-item"
  489. @click="page = totalPages"
  490. >
  491. <a class="page-link" href="javascript:void(0)">{{
  492. totalPages
  493. }}</a>
  494. </li>
  495. <li
  496. class="page-item"
  497. :class="{ disabled: currentPage === totalPages }"
  498. >
  499. <span class="page-link" @click="nextPage">بعدی</span>
  500. </li>
  501. </ul>
  502. </nav>
  503. </div>
  504. </BCol>
  505. <BCol sm="4">
  506. <div class="ms-0 search-number">
  507. <input
  508. v-model="searchPage"
  509. type="text"
  510. class="form-control"
  511. placeholder="برو به صفحه"
  512. :max="totalPages"
  513. min="1"
  514. @input="handlePageInput"
  515. />
  516. </div>
  517. </BCol>
  518. </BRow>
  519. </Layout>
  520. </template>
  521. <style scoped>
  522. .card {
  523. transition: transform 0.3s ease;
  524. }
  525. .card:hover {
  526. transform: translateY(-3px);
  527. }
  528. .table th,
  529. .table td {
  530. vertical-align: middle;
  531. text-align: center;
  532. }
  533. .filter-loader {
  534. border: 4px solid rgba(0, 123, 255, 0.3);
  535. border-top: 4px solid #007bff;
  536. border-radius: 50%;
  537. width: 40px;
  538. height: 40px;
  539. animation: spin 1s linear infinite;
  540. margin: 20px auto;
  541. }
  542. .subject-box {
  543. padding: 8px 14px;
  544. background: linear-gradient(135deg, #fff3e0, #ffe0b2);
  545. color: #ef6c00;
  546. font-weight: 600;
  547. border-radius: 10px;
  548. box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
  549. display: inline-flex;
  550. align-items: center;
  551. gap: 8px;
  552. transition: transform 0.2s ease, box-shadow 0.2s ease;
  553. }
  554. .subject-box i {
  555. color: #ef6c00;
  556. font-size: 1rem;
  557. }
  558. .subject-box:hover {
  559. transform: translateY(-2px);
  560. box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
  561. }
  562. .search-number {
  563. display: flex;
  564. align-items: center;
  565. }
  566. .search-number input {
  567. width: 150px;
  568. padding: 0.5rem;
  569. font-size: 1rem;
  570. border-radius: 0.375rem;
  571. margin-bottom: 7px;
  572. border: 1px solid #ced4da;
  573. transition: border-color 0.3s ease, box-shadow 0.3s ease;
  574. }
  575. .search-number input:focus {
  576. border-color: #007bff;
  577. box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
  578. }
  579. .search-number input::placeholder {
  580. color: #6c757d;
  581. }
  582. .search-number input:disabled {
  583. background-color: #f8f9fa;
  584. cursor: not-allowed;
  585. }
  586. .pagination {
  587. display: flex;
  588. flex-wrap: wrap;
  589. gap: 5px;
  590. }
  591. .page-item {
  592. flex: 0 0 auto;
  593. }
  594. .page-link {
  595. cursor: pointer;
  596. user-select: none;
  597. }
  598. .comment-text {
  599. display: inline-block;
  600. max-width: 100%;
  601. font-size: 14px;
  602. white-space: nowrap;
  603. overflow: hidden;
  604. text-overflow: ellipsis;
  605. }
  606. .comment-td {
  607. cursor: pointer;
  608. }
  609. .btn {
  610. transition: all 0.3s ease;
  611. }
  612. .btn:hover {
  613. transform: translateY(-2px);
  614. box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
  615. }
  616. </style>