OrdersSection.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <template>
  2. <div class="space-y-8">
  3. <!-- Filter bar -->
  4. <div class="flex flex-wrap items-center gap-4 bg-card/30 p-4 rounded-2xl border border-border/50">
  5. <div class="flex items-center gap-2">
  6. <Filter class="w-4 h-4 text-muted-foreground" />
  7. <span class="text-xs font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.filters") }}</span>
  8. </div>
  9. <select v-model="statusFilter" class="bg-background border border-border/50 rounded-xl px-4 py-2 text-xs min-w-[140px] focus:ring-2 ring-primary/20 outline-none">
  10. <option value="all">{{ t("admin.allStatuses") }}</option>
  11. <option v-for="s in statusOptions" :key="s" :value="s">{{ t("statuses." + s) }}</option>
  12. </select>
  13. <div class="flex items-center gap-2 bg-background border border-border/50 rounded-xl px-3 py-1.5">
  14. <span class="text-[10px] font-bold text-muted-foreground uppercase mr-1">{{ t("admin.from") }}</span>
  15. <input type="date" v-model="dateFrom" class="bg-transparent border-none text-xs focus:ring-0 p-0" />
  16. </div>
  17. <div class="flex items-center gap-2 bg-background border border-border/50 rounded-xl px-3 py-1.5">
  18. <span class="text-[10px] font-bold text-muted-foreground uppercase mr-1">{{ t("admin.to") }}</span>
  19. <input type="date" v-model="dateTo" class="bg-transparent border-none text-xs focus:ring-0 p-0" />
  20. </div>
  21. <button @click="resetFilters" class="text-xs text-muted-foreground hover:text-primary transition-colors underline ml-auto">{{ t("admin.reset") }}</button>
  22. </div>
  23. <!-- Orders Grid -->
  24. <div v-if="filtered.length > 0" class="grid gap-6">
  25. <OrderCard
  26. v-for="order in filtered"
  27. :key="order.id"
  28. :order="order"
  29. :statusConfig="statusConfig"
  30. :resourcesBaseUrl="resourcesBaseUrl"
  31. :isFocused="focusedOrderId === order.id"
  32. :isAdminChatOpen="adminChatId === order.id"
  33. :notifyStatus="notifyStatusMap[order.id]"
  34. :fiscalData="fiscalFormMap[order.id]"
  35. @focus="id => focusedOrderId = id"
  36. @update-status="(id, s) => $emit('update-status', id, s)"
  37. @delete-order="id => $emit('delete-order', id)"
  38. @attach-file="(id, f) => $emit('attach-file', id, f)"
  39. @upload-photo="(id, f) => $emit('upload-photo', id, f)"
  40. @delete-file="(id, fid, fname) => $emit('delete-file', id, fid, fname)"
  41. @delete-photo="pid => $emit('delete-photo', pid)"
  42. @toggle-photo-public="(pid, pub) => $emit('toggle-photo-public', pid, pub)"
  43. @approve-review="id => $emit('approve-review', id)"
  44. @open-chat="id => $emit('open-chat', id)"
  45. @close-chat="$emit('close-chat')"
  46. @update-notify="(id, val) => $emit('update-notify', id, val)"
  47. @update-fiscal="(id, data) => $emit('update-fiscal', id, data)"
  48. @edit-order="o => $emit('edit-order', o)"
  49. />
  50. </div>
  51. <!-- No results -->
  52. <div v-else class="flex flex-col items-center justify-center py-20 bg-card/40 border border-border/50 rounded-3xl opacity-50">
  53. <Package class="w-12 h-12 text-muted-foreground/20 mb-4" />
  54. <p class="text-sm text-muted-foreground">{{ t("admin.noOrdersFound") }}</p>
  55. </div>
  56. </div>
  57. </template>
  58. <script setup lang="ts">
  59. import { ref, computed } from "vue";
  60. import { useI18n } from "vue-i18n";
  61. import { Search, Filter, RefreshCw, Plus, Package } from "lucide-vue-next";
  62. import Button from "@/components/ui/button.vue";
  63. import OrderCard from "./OrderCard.vue";
  64. const { t } = useI18n();
  65. const props = defineProps<{
  66. orders: any[];
  67. statusConfig: Record<string, any>;
  68. resourcesBaseUrl: string;
  69. adminChatId: any;
  70. notifyStatusMap: Record<number, boolean>;
  71. fiscalFormMap: Record<number, any>;
  72. searchQuery: string;
  73. }>();
  74. const emit = defineEmits([
  75. 'update-status', 'delete-order', 'attach-file', 'upload-photo',
  76. 'delete-file', 'delete-photo', 'toggle-photo-public',
  77. 'approve-review', 'open-chat', 'close-chat',
  78. 'update-notify', 'update-fiscal', 'edit-order'
  79. ]);
  80. const statusFilter = ref("all");
  81. const dateFrom = ref("");
  82. const dateTo = ref("");
  83. const focusedOrderId = ref<number | null>(null);
  84. const statusOptions = Object.keys(props.statusConfig);
  85. const filtered = computed(() => {
  86. let list = [...props.orders];
  87. if (statusFilter.value !== "all") {
  88. list = list.filter(o => o.status === statusFilter.value);
  89. }
  90. if (dateFrom.value) {
  91. const from = new Date(dateFrom.value);
  92. list = list.filter(o => new Date(o.created_at) >= from);
  93. }
  94. if (dateTo.value) {
  95. const to = new Date(dateTo.value);
  96. to.setHours(23, 59, 59);
  97. list = list.filter(o => new Date(o.created_at) <= to);
  98. }
  99. if (props.searchQuery) {
  100. const q = props.searchQuery.toLowerCase();
  101. list = list.filter(o =>
  102. o.id.toString().includes(q) ||
  103. o.first_name?.toLowerCase().includes(q) ||
  104. o.last_name?.toLowerCase().includes(q) ||
  105. o.email?.toLowerCase().includes(q)
  106. );
  107. }
  108. return list.sort((a, b) => b.id - a.id);
  109. });
  110. const resetFilters = () => {
  111. statusFilter.value = 'all';
  112. dateFrom.value = '';
  113. dateTo.value = '';
  114. };
  115. </script>