| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- <template>
- <div
- @mouseenter="$emit('focus', order.id)"
- @mouseleave="$emit('focus', null)"
- :class="[
- 'group relative bg-card/40 backdrop-blur-md border rounded-3xl overflow-hidden transition-all duration-300',
- isFocused ? 'border-primary ring-1 ring-primary/20 shadow-glow' : 'border-border/50'
- ]"
- >
- <!-- Paste Indicator -->
- <div v-if="isFocused" class="absolute top-2 right-2 z-50 pointer-events-none animate-pulse">
- <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">
- <span class="text-[9px] font-black text-primary uppercase tracking-widest">Ctrl+V — Photo Report</span>
- </div>
- </div>
- <div class="flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-border/50">
- <!-- Info Column -->
- <div class="p-6 lg:w-1/2">
- <div class="flex items-center justify-between mb-4">
- <span class="text-xl font-black text-foreground bg-primary/10 px-3 py-1 rounded-xl tracking-tight">#{{ order.id }}</span>
- <span :class="['flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider', statusColor]">
- <component :is="statusIcon" class="w-3.5 h-3.5" />
- {{ t("statuses." + order.status) }}
- </span>
- </div>
- <div class="flex items-center gap-2">
- <div class="flex flex-col">
- <h3 class="font-bold leading-none">{{ order.first_name }} {{ order.last_name }}</h3>
- <div v-if="order.is_company" class="mt-1 flex flex-col gap-0.5">
- <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>
- <p class="text-[10px] font-bold text-primary truncate max-w-[150px]">{{ order.company_name }}</p>
- <p class="text-[8px] text-muted-foreground font-mono">PIB: {{ order.company_pib }}</p>
- </div>
- <p v-else class="text-[10px] text-muted-foreground mt-1">{{ order.email }}</p>
- </div>
- </div>
- <div class="mt-8 grid grid-cols-2 gap-x-8 gap-y-4 border-t border-border/10 pt-6">
- <div v-if="order.phone" class="space-y-1">
- <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.phone") }}</span>
- <p class="text-xs font-medium">{{ order.phone }}</p>
- </div>
- <div class="space-y-1">
- <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.labels.registered") }}</span>
- <p class="text-xs font-medium">{{ formatDate(order.created_at) }}</p>
- </div>
- <div class="space-y-1">
- <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.address") }}</span>
- <p class="text-xs font-medium leading-relaxed">{{ order.shipping_address || '—' }}</p>
- </div>
- <div class="space-y-1">
- <span class="text-[9px] font-bold uppercase text-muted-foreground tracking-widest">{{ t("admin.fields.deliveryType") }}</span>
- <p class="text-xs font-medium">{{ order.delivery_type === 'cargo' ? t("admin.fields.cargo") : t("admin.fields.pickup") }}</p>
- </div>
- </div>
- <div v-if="order.notes" class="mt-8 p-4 bg-primary/5 border border-primary/10 rounded-2xl italic">
- <span class="text-[9px] font-bold uppercase text-primary/60 mb-2 block tracking-widest">{{ t("admin.fields.projectNotes") }}</span>
- <p class="text-[11px] leading-relaxed">"{{ order.notes }}"</p>
- </div>
- </div>
- <!-- Resources Column -->
- <div class="p-6 lg:w-1/4 border-x border-border/50">
- <div class="flex items-center justify-between mb-4">
- <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.sourceFiles") }} ({{ order.files?.length || 0 }})</span>
- <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">
- <Plus class="w-3.5 h-3.5" />
- <input type="file" class="hidden" accept=".stl,.obj" @change="e => $emit('attach-file', order.id, (e.target as HTMLInputElement).files?.[0])" />
- </label>
- </div>
- <div v-if="order.model_link" class="mb-6 p-3 bg-blue-500/5 border border-blue-500/20 rounded-xl">
- <span class="text-[9px] font-bold uppercase text-blue-500/60 mb-1 block">{{ t("admin.fields.externalLink") }}</span>
- <div class="flex items-center justify-between gap-2 overflow-hidden">
- <p class="text-[10px] text-muted-foreground truncate">{{ order.model_link }}</p>
- <a :href="order.model_link" target="_blank" class="text-blue-500 hover:underline"><ExternalLink class="w-3.5 h-3.5" /></a>
- </div>
- </div>
- <div class="space-y-3">
- <template v-for="(f, i) in order.files" :key="f.id || i">
- <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">
- <div class="w-20 bg-muted/20 flex items-center justify-center border-r border-border/50 overflow-hidden">
- <img v-if="f.preview_path" :src="`${resourcesBaseUrl}/${f.preview_path}`" class="w-full h-full object-contain p-1" />
- <FileBox v-else class="w-6 h-6 text-muted-foreground/30" />
- <div class="absolute inset-0 bg-primary/20 opacity-0 group-hover/file:opacity-100 transition-opacity flex items-center justify-center">
- <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>
- </div>
- </div>
- <div class="flex-1 p-3 flex flex-col justify-center min-w-0">
- <p class="text-[11px] font-bold truncate mb-1 pr-8">{{ f.filename }}</p>
- <div class="flex flex-wrap gap-2 items-center">
- <span v-if="f.file_size" class="text-[9px] text-muted-foreground uppercase opacity-60">{{ (f.file_size / 1024 / 1024).toFixed(1) }} MB</span>
- <div v-if="f.print_time || f.filament_g" class="flex gap-2 border-l border-border/50 pl-2">
- <span v-if="f.print_time" class="text-[9px] font-bold text-primary/80">⏱️ {{ f.print_time }}</span>
- <span v-if="f.filament_g" class="text-[9px] font-bold text-primary/80">⚖️ {{ f.filament_g.toFixed(1) }}g</span>
- </div>
- </div>
- </div>
- <div class="absolute top-2 right-2 flex flex-col items-end gap-1">
- <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>
- <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">
- <Trash2 class="w-2.5 h-2.5" />
- </button>
- </div>
- </div>
- </template>
- </div>
- <div class="mt-8 pt-6 border-t border-border/50">
- <div class="flex items-center justify-between mb-4">
- <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.photoReport") }} ({{ order.photos?.length || 0 }})</span>
- <label class="p-1.5 bg-primary/10 text-primary rounded-lg cursor-pointer hover:bg-primary hover:text-primary-foreground transition-all">
- <Plus class="w-3.5 h-3.5" />
- <input type="file" class="hidden" accept="image/*" @change="e => $emit('upload-photo', order.id, (e.target as HTMLInputElement).files?.[0])" />
- </label>
- </div>
- <div class="flex flex-wrap gap-2">
- <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">
- <img :src="`${resourcesBaseUrl}/${p.file_path}`" class="w-full h-full object-cover" />
- <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'}`">
- <Eye v-if="p.is_public" class="w-2.5 h-2.5" /><EyeOff v-else class="w-2.5 h-2.5" />
- </button>
- <div class="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover/img:opacity-100 transition-opacity gap-2">
- <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">
- <ExternalLink class="w-3.5 h-3.5 text-white" />
- </a>
- <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">
- <Trash2 class="w-3.5 h-3.5 text-white" />
- </button>
- </div>
- </div>
- <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">
- <ImageIcon class="w-6 h-6 mb-2" /><span class="text-[10px] font-bold uppercase tracking-tighter">{{ t("admin.fields.noPhotos") }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- Pricing & Actions Column -->
- <div class="p-6 lg:w-1/4 bg-primary/5">
- <div class="flex items-center gap-2 mb-6">
- <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" />
- <label :for="`notify-${order.id}`" class="text-[10px] font-bold uppercase text-muted-foreground cursor-pointer">{{ t("admin.fields.notifyUser") }}</label>
- </div>
- <div class="grid grid-cols-2 gap-2 mb-8">
- <button v-for="s in statusOptions" :key="s"
- @click="$emit('update-status', order.id, s)"
- :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'}`">
- {{ t("statuses." + s) }}
- </button>
- </div>
- <div class="space-y-4 mb-8">
- <div class="flex justify-between items-baseline">
- <span class="text-[10px] font-bold uppercase text-muted-foreground">{{ t("admin.fields.totalPrice") }}</span>
- <p class="text-2xl font-black font-display text-primary">{{ order.invoice_amount || 0 }} EUR</p>
- </div>
-
- <div class="pt-4 border-t border-primary/10 space-y-3">
- <div class="flex items-center justify-between text-[10px] font-bold uppercase text-muted-foreground tracking-tighter">
- <span>{{ t("admin.labels.fiscalization") }}</span>
- <span :class="order.fiscal_jikr ? 'text-emerald-500' : 'text-rose-500' ">{{ order.fiscal_jikr ? t("admin.fields.active") : t("admin.fields.notActive") }}</span>
- </div>
- <div class="grid grid-cols-2 gap-3">
- <div class="space-y-1">
- <label class="text-[9px] font-bold uppercase text-muted-foreground ml-1">IKOF</label>
- <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" />
- </div>
- <div class="space-y-1">
- <label class="text-[9px] font-bold uppercase text-muted-foreground ml-1">JIKR</label>
- <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" />
- </div>
- </div>
- </div>
- </div>
- <div class="mt-auto space-y-3">
- <button @click="$emit('edit-order', order)" class="w-full py-2 bg-red-600 text-white font-bold rounded-xl mb-2">!!! DEBUG EDIT ORDER !!!</button>
- <Button variant="outline" class="w-full gap-2 rounded-2xl h-12 border-primary/20 text-primary hover:bg-primary/10" @click="$emit('edit-order', order)">
- <Edit2 class="w-4 h-4" />{{ t("admin.actions.edit") }}
- </Button>
- <Button variant="hero" class="w-full gap-2 rounded-2xl h-12" @click="$emit('open-chat', order.id)">
- <MessageCircle class="w-4 h-4" />{{ t("admin.actions.chatWithClient") }}
- </Button>
- <div class="pt-4 border-t border-rose-500/10">
- <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">
- <Trash2 class="w-4 h-4 transition-transform group-hover:scale-110" /> {{ t("admin.actions.deleteOrder") }}
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- Feedback Row -->
- <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">
- <div class="flex items-start gap-4 flex-1">
- <div class="pt-1">
- <div class="flex gap-0.5">
- <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'" />
- </div>
- </div>
- <div>
- <p class="text-xs font-bold text-foreground italic leading-relaxed">"{{ order.review_text }}"</p>
- <div class="flex items-center gap-3 mt-2">
- <span class="text-[9px] font-black uppercase tracking-widest text-muted-foreground shadow-sm">Customer Feedback</span>
- <span v-if="order.review_approved" class="flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-emerald-500">
- <CheckCircle2 class="w-3.5 h-3.5" /> Approved
- </span>
- <span v-else class="flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-amber-500">
- <Clock class="w-3.5 h-3.5" /> Pending
- </span>
- </div>
- </div>
- </div>
- <Button v-if="!order.review_approved" variant="hero" class="whitespace-nowrap rounded-xl" @click="$emit('approve-review', order.id)">
- Approve Entry
- </Button>
- </div>
- <!-- Chat Panel -->
- <div v-if="isAdminChatOpen" :id="'admin-chat-' + order.id" class="border-t border-border/50">
- <OrderChat :orderId="order.id" @close="$emit('close-chat')" closable />
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, watch, reactive } from "vue";
- import { useI18n } from "vue-i18n";
- import {
- Clock, CheckCircle2, Truck, XCircle, AlertCircle, RefreshCw,
- FileText, ExternalLink, ShieldCheck, Eye, Trash2, ImageIcon,
- EyeOff, MessageCircle, FileBox, Download, Star, Plus, Edit2
- } from "lucide-vue-next";
- import Button from "@/components/ui/button.vue";
- import OrderChat from "@/components/OrderChat.vue";
- const { t } = useI18n();
- const props = defineProps<{
- order: any;
- statusConfig: Record<string, any>;
- resourcesBaseUrl: string;
- isFocused: boolean;
- isAdminChatOpen: boolean;
- notifyStatus: boolean;
- fiscalData: { fiscal_qr_url: string; ikof: string; jikr: string };
- adminChatId?: any;
- }>();
- const emit = defineEmits([
- 'focus', 'update-status', 'delete-order', 'attach-file', 'upload-photo',
- 'delete-file', 'delete-photo', 'toggle-photo-public', 'approve-review',
- 'open-chat', 'close-chat', 'update-notify', 'update-fiscal', 'edit-order'
- ]);
- const internalNotify = ref(props.notifyStatus);
- const fiscalData = reactive({ ...props.fiscalData });
- watch(() => props.notifyStatus, (val) => { internalNotify.value = val; });
- watch(() => props.fiscalData, (val) => { Object.assign(fiscalData, val); }, { deep: true });
- const statusOptions = Object.keys(props.statusConfig);
- const statusColor = props.statusConfig[props.order.status]?.color || "bg-muted text-muted-foreground";
- const statusIcon = props.statusConfig[props.order.status]?.icon || Clock;
- const formatDate = (date: string) => {
- return new Date(date).toLocaleDateString();
- };
- </script>
|