OrderCard.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <template>
  2. <div
  3. @mouseenter="$emit('focus', order.id)"
  4. @mouseleave="$emit('focus', null)"
  5. :class="[
  6. 'group relative bg-card/40 backdrop-blur-md border rounded-3xl overflow-hidden transition-all duration-300',
  7. isFocused ? 'border-primary ring-1 ring-primary/20 shadow-glow' : 'border-border/50'
  8. ]"
  9. >
  10. <!-- Paste Indicator -->
  11. <div v-if="isFocused" class="absolute top-2 right-2 z-50 pointer-events-none animate-pulse">
  12. <div class="px-2 py-1 bg-primary/20 backdrop-blur-md border border-primary/50 rounded-lg flex items-center gap-1.5 shadow-lg">
  13. <span class="text-[9px] font-black text-primary uppercase tracking-widest">Ctrl+V — Photo Report</span>
  14. </div>
  15. </div>
  16. <div class="flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-border/50">
  17. <!-- Info Column -->
  18. <div class="p-6 lg:w-1/2">
  19. <div class="flex items-center justify-between mb-4">
  20. <div class="flex items-center gap-2">
  21. <span class="text-xl font-black text-foreground bg-primary/10 px-3 py-1 rounded-xl tracking-tight">#{{ order.id }}</span>
  22. <span class="text-[8px] font-mono text-muted-foreground">v3.0-FIX</span>
  23. </div>
  24. <span :class="['flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider', statusColor]">
  25. <component :is="statusIcon" class="w-3.5 h-3.5" />
  26. {{ t("statuses." + order.status) }}
  27. </span>
  28. </div>
  29. <div class="flex items-center gap-2">
  30. <div class="flex flex-col">
  31. <div class="flex items-center gap-2">
  32. <h3 class="font-bold leading-none">{{ order.first_name }} {{ order.last_name }}</h3>
  33. <button @click.stop="$emit('edit-order', order)" class="p-1.5 bg-primary/5 hover:bg-primary/20 rounded-md text-primary transition-colors" style="position: relative; z-index: 100;">
  34. <Edit2 class="w-3 h-3" />
  35. </button>
  36. </div>
  37. <div v-if="order.is_company" class="mt-1 flex flex-col gap-0.5">
  38. <span class="text-[9px] font-bold uppercase py-0.5 px-1.5 bg-primary text-primary-foreground rounded-md w-fit">{{ t("auth.fields.company") }}</span>
  39. <p class="text-[10px] font-bold text-primary truncate max-w-[150px]">{{ order.company_name }}</p>
  40. <p class="text-[8px] text-muted-foreground font-mono">PIB: {{ order.company_pib }}</p>
  41. </div>
  42. <p v-else class="text-[10px] text-muted-foreground mt-1">{{ order.email }}</p>
  43. </div>
  44. </div>
  45. <div class="mt-8 grid grid-cols-2 gap-x-8 gap-y-4 border-t border-border/10 pt-6 px-1">
  46. <div v-if="order.phone" class="space-y-1">
  47. <div class="flex items-center justify-between">
  48. <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.phone") }}</span>
  49. <button @click.stop="$emit('edit-order', order)" class="p-1 text-primary hover:bg-primary/5 rounded"><Edit2 class="w-2.5 h-2.5" /></button>
  50. </div>
  51. <p class="text-xs font-medium">{{ order.phone }}</p>
  52. </div>
  53. <div class="space-y-1">
  54. <div class="flex items-center justify-between">
  55. <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.labels.registered") }}</span>
  56. <button @click.stop="$emit('edit-order', order)" class="p-1 text-primary hover:bg-primary/5 rounded"><Edit2 class="w-2.5 h-2.5" /></button>
  57. </div>
  58. <p class="text-xs font-medium">{{ formatDate(order.created_at) }}</p>
  59. </div>
  60. <div class="space-y-1">
  61. <div class="flex items-center justify-between">
  62. <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.address") }}</span>
  63. <button @click.stop="$emit('edit-order', order)" class="p-1 text-primary hover:bg-primary/5 rounded"><Edit2 class="w-2.5 h-2.5" /></button>
  64. </div>
  65. <p class="text-xs font-medium leading-relaxed">{{ order.shipping_address || '—' }}</p>
  66. </div>
  67. <div class="space-y-1">
  68. <div class="flex items-center justify-between">
  69. <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.deliveryType") }}</span>
  70. <button @click.stop="$emit('edit-order', order)" class="p-1 text-primary hover:bg-primary/5 rounded"><Edit2 class="w-2.5 h-2.5" /></button>
  71. </div>
  72. <p class="text-xs font-medium">{{ order.delivery_type === 'cargo' ? t("admin.fields.cargo") : t("admin.fields.pickup") }}</p>
  73. </div>
  74. </div>
  75. <div class="mt-6 p-4 bg-primary/5 rounded-2xl border border-primary/20 space-y-4">
  76. <div class="flex items-center justify-between">
  77. <div class="flex flex-col">
  78. <span class="text-[9px] font-bold uppercase text-primary/60 tracking-widest">{{ t("admin.fields.material") }} & {{ t("admin.fields.colors") }}</span>
  79. <p class="text-sm font-bold">{{ order.material_name || '???' }} — {{ order.color_name || '???' }}</p>
  80. </div>
  81. <button @click.stop="$emit('edit-order', order)" class="bg-primary text-primary-foreground p-3 rounded-xl shadow-lg hover:scale-105 transition-transform" style="position: relative; z-index: 100;">
  82. <Edit2 class="w-5 h-5" />
  83. </button>
  84. </div>
  85. <div class="flex items-center justify-between pt-4 border-t border-primary/10">
  86. <div class="flex flex-col">
  87. <span class="text-[9px] font-bold uppercase text-primary/60 tracking-widest">{{ t("admin.fields.quantity") }}</span>
  88. <p class="text-2xl font-black text-primary">x{{ order.quantity || 1 }}</p>
  89. </div>
  90. </div>
  91. </div>
  92. <div v-if="order.notes" class="mt-8 p-4 bg-primary/5 border border-primary/10 rounded-2xl italic">
  93. <div class="flex items-center justify-between mb-2">
  94. <span class="text-[9px] font-bold uppercase text-primary/60 tracking-widest">{{ t("admin.fields.projectNotes") }}</span>
  95. <button @click.stop="$emit('edit-order', order)" class="p-1 text-primary hover:bg-primary/5 rounded"><Edit2 class="w-2.5 h-2.5" /></button>
  96. </div>
  97. <p class="text-[11px] leading-relaxed">"{{ order.notes }}"</p>
  98. </div>
  99. </div>
  100. <!-- Resources Column -->
  101. <div class="p-6 lg:w-1/4 border-x border-border/50">
  102. <div class="flex items-center justify-between mb-4">
  103. <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.sourceFiles") }} ({{ order.files?.length || 0 }})</span>
  104. <label class="p-1.5 bg-blue-500/10 text-blue-500 rounded-lg cursor-pointer hover:bg-blue-500 hover:text-white transition-all">
  105. <Plus class="w-3.5 h-3.5" />
  106. <input type="file" class="hidden" accept=".stl,.obj" @change="e => $emit('attach-file', order.id, (e.target as HTMLInputElement).files?.[0])" />
  107. </label>
  108. </div>
  109. <div v-if="order.model_link" class="mb-6 p-3 bg-blue-500/5 border border-blue-500/20 rounded-xl">
  110. <span class="text-[9px] font-bold uppercase text-blue-500/60 mb-1 block">{{ t("admin.fields.externalLink") }}</span>
  111. <div class="flex items-center justify-between gap-2 overflow-hidden">
  112. <p class="text-[10px] text-muted-foreground truncate">{{ order.model_link }}</p>
  113. <a :href="order.model_link" target="_blank" class="text-blue-500 hover:underline"><ExternalLink class="w-3.5 h-3.5" /></a>
  114. </div>
  115. </div>
  116. <div class="space-y-3">
  117. <template v-for="(f, i) in order.files" :key="f.id || i">
  118. <div v-if="f.id" class="relative group/file bg-background/30 border border-border/50 rounded-2xl overflow-hidden hover:border-primary/30 transition-all flex h-20">
  119. <div class="w-20 bg-muted/20 flex items-center justify-center border-r border-border/50 overflow-hidden">
  120. <img v-if="f.preview_path" :src="`${resourcesBaseUrl}/${f.preview_path}`" class="w-full h-full object-contain p-1" />
  121. <FileBox v-else class="w-6 h-6 text-muted-foreground/30" />
  122. <div class="absolute inset-0 bg-primary/20 opacity-0 group-hover/file:opacity-100 transition-opacity flex items-center justify-center">
  123. <a :href="`${resourcesBaseUrl}/${f.file_path}`" target="_blank" class="bg-card w-8 h-8 rounded-full flex items-center justify-center shadow-lg"><Download class="w-4 h-4 text-primary" /></a>
  124. </div>
  125. </div>
  126. <div class="flex-1 p-3 flex flex-col justify-center min-w-0">
  127. <p class="text-[11px] font-bold truncate mb-1 pr-8">{{ f.filename }}</p>
  128. <div class="flex flex-wrap gap-2 items-center">
  129. <span v-if="f.file_size" class="text-[9px] text-muted-foreground uppercase opacity-60">{{ (f.file_size / 1024 / 1024).toFixed(1) }} MB</span>
  130. <div v-if="f.print_time || f.filament_g" class="flex gap-2 border-l border-border/50 pl-2">
  131. <span v-if="f.print_time" class="text-[9px] font-bold text-primary/80">⏱️ {{ f.print_time }}</span>
  132. <span v-if="f.filament_g" class="text-[9px] font-bold text-primary/80">⚖️ {{ f.filament_g.toFixed(1) }}g</span>
  133. </div>
  134. </div>
  135. </div>
  136. <div class="absolute top-2 right-2 flex flex-col items-end gap-1">
  137. <div class="px-1.5 py-0.5 bg-background/80 backdrop-blur-md rounded-md border border-border/50 text-[10px] font-bold">x{{ f.quantity || 1 }}</div>
  138. <button @click.prevent="$emit('delete-file', order.id, f.id, f.filename)" class="p-1 bg-background/80 backdrop-blur-md border border-border/50 text-rose-500 hover:bg-rose-500 hover:text-white rounded-md transition-colors shadow-sm">
  139. <Trash2 class="w-2.5 h-2.5" />
  140. </button>
  141. </div>
  142. </div>
  143. </template>
  144. </div>
  145. <div class="mt-8 pt-6 border-t border-border/50">
  146. <div class="flex items-center justify-between mb-4">
  147. <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.photoReport") }} ({{ order.photos?.length || 0 }})</span>
  148. <label class="p-1.5 bg-primary/10 text-primary rounded-lg cursor-pointer hover:bg-primary hover:text-primary-foreground transition-all">
  149. <Plus class="w-3.5 h-3.5" />
  150. <input type="file" class="hidden" accept="image/*" @change="e => $emit('upload-photo', order.id, (e.target as HTMLInputElement).files?.[0])" />
  151. </label>
  152. </div>
  153. <div class="flex flex-wrap gap-2">
  154. <div v-for="(p, i) in order.photos" :key="i" class="relative group/img overflow-hidden rounded-lg border border-border/50 w-12 h-12 bg-background/50">
  155. <img :src="`${resourcesBaseUrl}/${p.file_path}`" class="w-full h-full object-cover" />
  156. <button @click="$emit('toggle-photo-public', p.id, p.is_public)" :class="`absolute top-0 right-0 p-0.5 rounded-bl-md bg-black/60 z-10 transition-colors ${p.is_public ? 'text-blue-400 hover:text-blue-300' : 'text-gray-400 hover:text-white'}`">
  157. <Eye v-if="p.is_public" class="w-2.5 h-2.5" /><EyeOff v-else class="w-2.5 h-2.5" />
  158. </button>
  159. <div class="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover/img:opacity-100 transition-opacity gap-2">
  160. <a :href="`${resourcesBaseUrl}/${p.file_path}`" target="_blank" class="w-7 h-7 bg-white/10 hover:bg-white/20 rounded-full flex items-center justify-center transition-colors">
  161. <ExternalLink class="w-3.5 h-3.5 text-white" />
  162. </a>
  163. <button @click="$emit('delete-photo', p.id)" class="w-7 h-7 bg-rose-500/20 hover:bg-rose-500/40 rounded-full flex items-center justify-center transition-colors">
  164. <Trash2 class="w-3.5 h-3.5 text-white" />
  165. </button>
  166. </div>
  167. </div>
  168. <div v-if="!order.photos?.length" class="w-full py-6 border border-dashed border-border/50 rounded-2xl flex flex-col items-center justify-center opacity-40">
  169. <ImageIcon class="w-6 h-6 mb-2" /><span class="text-[10px] font-bold uppercase tracking-tighter">{{ t("admin.fields.noPhotos") }}</span>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <!-- Pricing & Actions Column -->
  175. <div class="p-6 lg:w-1/4 bg-primary/5">
  176. <div class="flex items-center gap-2 mb-6">
  177. <input type="checkbox" :id="`notify-${order.id}`" v-model="internalNotify" @change="$emit('update-notify', order.id, internalNotify)" class="w-4 h-4 rounded border-border" />
  178. <label :for="`notify-${order.id}`" class="text-[10px] font-bold uppercase text-muted-foreground cursor-pointer">{{ t("admin.fields.notifyUser") }}</label>
  179. </div>
  180. <div class="grid grid-cols-2 gap-2 mb-8">
  181. <button v-for="s in statusOptions" :key="s"
  182. @click="$emit('update-status', order.id, s)"
  183. :class="`text-[9px] font-bold uppercase py-2 rounded-xl border transition-all ${order.status === s ? 'bg-primary text-primary-foreground border-primary shadow-glow' : 'bg-background hover:border-primary/30 border-border/50'}`">
  184. {{ t("statuses." + s) }}
  185. </button>
  186. </div>
  187. <div class="space-y-4 mb-8">
  188. <div class="flex justify-between items-baseline group/edit relative">
  189. <span class="text-[10px] font-bold uppercase text-muted-foreground">{{ t("admin.fields.totalPrice") }}</span>
  190. <div class="flex items-center gap-2">
  191. <p class="text-2xl font-black font-display text-primary">{{ order.invoice_amount || 0 }} EUR</p>
  192. <button @click.stop="$emit('edit-order', order)" class="opacity-0 group-hover/edit:opacity-100 p-1 text-primary"><Edit2 class="w-3.5 h-3.5" /></button>
  193. </div>
  194. </div>
  195. <div class="pt-4 border-t border-primary/10 space-y-3">
  196. <div class="flex items-center justify-between text-[10px] font-bold uppercase text-muted-foreground tracking-tighter">
  197. <span>{{ t("admin.labels.fiscalization") }}</span>
  198. <span :class="order.fiscal_jikr ? 'text-emerald-500' : 'text-rose-500' ">{{ order.fiscal_jikr ? t("admin.fields.active") : t("admin.fields.notActive") }}</span>
  199. </div>
  200. <div class="grid grid-cols-2 gap-3">
  201. <div class="space-y-1">
  202. <label class="text-[9px] font-bold uppercase text-muted-foreground ml-1">IKOF</label>
  203. <input v-model="fiscalData.ikof" @change="$emit('update-fiscal', order.id, fiscalData)" class="w-full bg-background border border-border/50 rounded-lg px-2 py-1.5 text-[10px] font-mono outline-none" />
  204. </div>
  205. <div class="space-y-1">
  206. <label class="text-[9px] font-bold uppercase text-muted-foreground ml-1">JIKR</label>
  207. <input v-model="fiscalData.jikr" @change="$emit('update-fiscal', order.id, fiscalData)" class="w-full bg-background border border-border/50 rounded-lg px-2 py-1.5 text-[10px] font-mono outline-none" />
  208. </div>
  209. </div>
  210. </div>
  211. </div>
  212. <div class="mt-auto space-y-3">
  213. <Button variant="hero" class="w-full gap-2 rounded-2xl h-12" @click="$emit('open-chat', order.id)">
  214. <MessageCircle class="w-4 h-4" />{{ t("admin.actions.chatWithClient") }}
  215. </Button>
  216. <div class="pt-4 border-t border-rose-500/10">
  217. <button @click="$emit('delete-order', order.id)" class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white border border-transparent font-bold transition-all text-xs group">
  218. <Trash2 class="w-4 h-4 transition-transform group-hover:scale-110" /> {{ t("admin.actions.deleteOrder") }}
  219. </button>
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. <!-- Feedback Row -->
  225. <div v-if="order.review_text" class="p-6 bg-amber-500/5 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-6 relative z-10">
  226. <div class="flex items-start gap-4 flex-1">
  227. <div class="pt-1">
  228. <div class="flex gap-0.5">
  229. <Star v-for="i in 5" :key="i" class="w-4 h-4" :class="order.rating >= i ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground/30'" />
  230. </div>
  231. </div>
  232. <div>
  233. <p class="text-xs font-bold text-foreground italic leading-relaxed">"{{ order.review_text }}"</p>
  234. <div class="flex items-center gap-3 mt-2">
  235. <span class="text-[9px] font-black uppercase tracking-widest text-muted-foreground shadow-sm">Customer Feedback</span>
  236. <span v-if="order.review_approved" class="flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-emerald-500">
  237. <CheckCircle2 class="w-3.5 h-3.5" /> Approved
  238. </span>
  239. <span v-else class="flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-amber-500">
  240. <Clock class="w-3.5 h-3.5" /> Pending
  241. </span>
  242. </div>
  243. </div>
  244. </div>
  245. <Button v-if="!order.review_approved" variant="hero" class="whitespace-nowrap rounded-xl" @click="$emit('approve-review', order.id)">
  246. Approve Entry
  247. </Button>
  248. </div>
  249. <!-- Chat Panel -->
  250. <div v-if="isAdminChatOpen" :id="'admin-chat-' + order.id" class="border-t border-border/50">
  251. <OrderChat :orderId="order.id" @close="$emit('close-chat')" closable />
  252. </div>
  253. </div>
  254. </template>
  255. <script setup lang="ts">
  256. import { ref, watch, reactive } from "vue";
  257. import { useI18n } from "vue-i18n";
  258. import {
  259. Clock, CheckCircle2, Truck, XCircle, AlertCircle, RefreshCw,
  260. FileText, ExternalLink, ShieldCheck, Eye, Trash2, ImageIcon,
  261. EyeOff, MessageCircle, FileBox, Download, Star, Plus, Edit2
  262. } from "lucide-vue-next";
  263. import Button from "@/components/ui/button.vue";
  264. import OrderChat from "@/components/OrderChat.vue";
  265. const { t } = useI18n();
  266. const props = defineProps<{
  267. order: any;
  268. statusConfig: Record<string, any>;
  269. resourcesBaseUrl: string;
  270. isFocused: boolean;
  271. isAdminChatOpen: boolean;
  272. notifyStatus: boolean;
  273. fiscalData: { fiscal_qr_url: string; ikof: string; jikr: string };
  274. adminChatId?: any;
  275. }>();
  276. const emit = defineEmits([
  277. 'focus', 'update-status', 'delete-order', 'attach-file', 'upload-photo',
  278. 'delete-file', 'delete-photo', 'toggle-photo-public', 'approve-review',
  279. 'open-chat', 'close-chat', 'update-notify', 'update-fiscal', 'edit-order'
  280. ]);
  281. const internalNotify = ref(props.notifyStatus);
  282. const fiscalData = reactive({ ...props.fiscalData });
  283. watch(() => props.notifyStatus, (val) => { internalNotify.value = val; });
  284. watch(() => props.fiscalData, (val) => { Object.assign(fiscalData, val); }, { deep: true });
  285. const statusOptions = Object.keys(props.statusConfig);
  286. const statusColor = props.statusConfig[props.order.status]?.color || "bg-muted text-muted-foreground";
  287. const statusIcon = props.statusConfig[props.order.status]?.icon || Clock;
  288. const formatDate = (date: string) => {
  289. return new Date(date).toLocaleDateString();
  290. };
  291. </script>