feat: breadcrumb nav, grouped properties sidebar, larger status selector, transitions
This commit is contained in:
@@ -19,6 +19,7 @@ import type {
|
|||||||
PreviewResult,
|
PreviewResult,
|
||||||
UpdateResult,
|
UpdateResult,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
@@ -85,7 +86,7 @@ function TransactionBubble({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-2">
|
<div className="flex justify-center py-2">
|
||||||
<span className="text-xs text-[#8a8f98]">
|
<span className="text-xs text-muted-foreground">
|
||||||
{message} · {timeAgo}
|
{message} · {timeAgo}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,28 +110,28 @@ function TransactionBubble({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: isAgent
|
backgroundColor: isAgent
|
||||||
? getInitialColor(tx.creator_id)
|
? getInitialColor(tx.creator_id)
|
||||||
: "#191a1b",
|
: "var(--muted)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="text-[11px] font-semibold"
|
className="text-[11px] font-semibold"
|
||||||
style={{ color: isAgent ? "#f7f8f8" : "#8a8f98" }}
|
style={{ color: isAgent ? "#f7f8f8" : "var(--muted-foreground)" }}
|
||||||
>
|
>
|
||||||
{getInitial(tx.creator_id)}
|
{getInitial(tx.creator_id)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-w-[75%] rounded-lg px-3 py-2",
|
"max-w-[75%] rounded-lg px-3 py-2 transition-all duration-150",
|
||||||
isAgent
|
isAgent
|
||||||
? "bg-[#5e6ad2]/15 text-[#f7f8f8]"
|
? "bg-primary/15 text-foreground"
|
||||||
: isInternal
|
: isInternal
|
||||||
? "bg-[#191a1b] border border-[rgba(255,255,255,0.05)] text-[#f7f8f8]"
|
? "bg-muted border border-border text-foreground"
|
||||||
: "bg-[#191a1b] text-[#f7f8f8]"
|
: "bg-muted text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isInternal && (
|
{isInternal && (
|
||||||
<div className="text-[10px] font-semibold text-[#f59e0b] mb-0.5 uppercase tracking-wider">
|
<div className="text-[10px] font-semibold text-chart-3 mb-0.5 uppercase tracking-wider">
|
||||||
Internal note
|
Internal note
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -139,7 +140,7 @@ function TransactionBubble({
|
|||||||
? String((tx.data as Record<string, unknown>).body)
|
? String((tx.data as Record<string, unknown>).body)
|
||||||
: tx.transaction_type}
|
: tx.transaction_type}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-[#8a8f98] mt-1">{timeAgo}</p>
|
<p className="text-[10px] text-muted-foreground mt-1">{timeAgo}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -262,17 +263,17 @@ export default function TicketDetailPage({
|
|||||||
<div className="flex-1 p-4 space-y-4">
|
<div className="flex-1 p-4 space-y-4">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<div key={i} className="flex gap-3">
|
<div key={i} className="flex gap-3">
|
||||||
<div className="w-6 h-6 rounded-full bg-[#191a1b] animate-pulse" />
|
<div className="w-6 h-6 rounded-full bg-muted animate-pulse" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<div className="h-3 bg-[#191a1b] rounded animate-pulse w-3/4" />
|
<div className="h-3 bg-muted rounded animate-pulse w-3/4" />
|
||||||
<div className="h-3 bg-[#191a1b] rounded animate-pulse w-1/2" />
|
<div className="h-3 bg-muted rounded animate-pulse w-1/2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-80 bg-[#0f1011] border-l border-[rgba(255,255,255,0.05)] p-4 space-y-3">
|
<div className="w-80 bg-sidebar border-l border-border p-4 space-y-3">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div key={i} className="h-6 bg-[#191a1b] rounded animate-pulse" />
|
<div key={i} className="h-6 bg-muted rounded animate-pulse" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,10 +283,10 @@ export default function TicketDetailPage({
|
|||||||
if (error && !ticket) {
|
if (error && !ticket) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
<p className="text-red-400 text-sm">{error}</p>
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={fetchData}
|
onClick={fetchData}
|
||||||
className="mt-2 text-sm text-[#7170ff] hover:text-[#828fff]"
|
className="mt-2 text-sm text-primary hover:text-primary/80 transition-all duration-150"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
@@ -296,7 +297,7 @@ export default function TicketDetailPage({
|
|||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
<p className="text-[#8a8f98] text-sm">Ticket not found</p>
|
<p className="text-muted-foreground text-sm">Ticket not found</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -311,28 +312,31 @@ export default function TicketDetailPage({
|
|||||||
{/* Left panel — conversation */}
|
{/* Left panel — conversation */}
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-[rgba(255,255,255,0.05)]">
|
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-border">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="w-7 h-7 flex items-center justify-center rounded-md hover:bg-[rgba(255,255,255,0.05)] text-[#8a8f98] hover:text-[#d0d6e0] transition-colors flex-shrink-0"
|
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-all duration-150"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
<ArrowLeftIcon className="w-3.5 h-3.5" />
|
||||||
|
All tickets
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex-1 min-w-0">
|
</div>
|
||||||
<h1 className="text-sm font-semibold text-[#f7f8f8] truncate">
|
|
||||||
{ticket.subject}
|
{/* Title */}
|
||||||
</h1>
|
<div className="px-4 py-3 border-b border-border">
|
||||||
<p className="text-xs text-[#8a8f98]">
|
<h1 className="text-sm font-semibold text-foreground truncate">
|
||||||
{ticket.id.slice(0, 8)} · {queue?.name || ticket.queue_id}
|
{ticket.subject}
|
||||||
</p>
|
</h1>
|
||||||
</div>
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
<span className="font-mono">{ticket.id.slice(0, 8)}</span> · {queue?.name || ticket.queue_id}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conversation */}
|
{/* Conversation */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{transactions.length === 0 && (
|
{transactions.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center py-20">
|
<div className="flex flex-col items-center justify-center py-20">
|
||||||
<p className="text-sm text-[#8a8f98]">
|
<p className="text-sm text-muted-foreground">
|
||||||
No activity yet
|
No activity yet
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,16 +347,16 @@ export default function TicketDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reply box */}
|
{/* Reply box */}
|
||||||
<div className="border-t border-[rgba(255,255,255,0.05)] bg-[#0f1011] p-3">
|
<div className="border-t border-border bg-sidebar p-3">
|
||||||
{/* Toggle tabs */}
|
{/* Toggle tabs */}
|
||||||
<div className="flex gap-0.5 mb-2 p-0.5 rounded-lg bg-[#08090a] w-fit">
|
<div className="flex gap-0.5 mb-2 p-0.5 rounded-lg bg-background w-fit">
|
||||||
<button
|
<button
|
||||||
onClick={() => setReplyMode("public")}
|
onClick={() => setReplyMode("public")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2.5 py-1 rounded-md text-xs font-medium transition-colors",
|
"px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-150",
|
||||||
replyMode === "public"
|
replyMode === "public"
|
||||||
? "bg-[#5e6ad2] text-[#f7f8f8]"
|
? "bg-primary text-primary-foreground"
|
||||||
: "text-[#8a8f98] hover:text-[#d0d6e0]"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Reply
|
Reply
|
||||||
@@ -360,10 +364,10 @@ export default function TicketDetailPage({
|
|||||||
<button
|
<button
|
||||||
onClick={() => setReplyMode("internal")}
|
onClick={() => setReplyMode("internal")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2.5 py-1 rounded-md text-xs font-medium transition-colors",
|
"px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-150",
|
||||||
replyMode === "internal"
|
replyMode === "internal"
|
||||||
? "bg-[#5e6ad2] text-[#f7f8f8]"
|
? "bg-primary text-primary-foreground"
|
||||||
: "text-[#8a8f98] hover:text-[#d0d6e0]"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Internal note
|
Internal note
|
||||||
@@ -376,19 +380,19 @@ export default function TicketDetailPage({
|
|||||||
onChange={(e) => setReplyText(e.target.value)}
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
placeholder="Reply to this ticket..."
|
placeholder="Reply to this ticket..."
|
||||||
rows={2}
|
rows={2}
|
||||||
className="flex-1 px-3 py-2 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)] text-sm text-[#f7f8f8] placeholder:text-[#8a8f98] outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2] resize-none"
|
className="flex-1 px-3 py-2 rounded-lg bg-background border border-border text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary resize-none transition-all duration-150"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button className="w-8 h-8 flex items-center justify-center rounded-lg text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[rgba(255,255,255,0.05)] transition-colors" title="Attach file (coming soon)">
|
<button className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-all duration-150" title="Attach file (coming soon)">
|
||||||
<PaperclipIcon className="w-4 h-4" />
|
<PaperclipIcon className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={!replyText.trim()}
|
disabled={!replyText.trim()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-8 h-8 flex items-center justify-center rounded-lg transition-colors",
|
"w-8 h-8 flex items-center justify-center rounded-lg transition-all duration-150",
|
||||||
replyText.trim()
|
replyText.trim()
|
||||||
? "bg-[#5e6ad2] text-[#f7f8f8] hover:bg-[#7170ff]"
|
? "bg-primary text-primary-foreground hover:bg-primary/80"
|
||||||
: "bg-[#191a1b] text-[#8a8f98] cursor-not-allowed"
|
: "bg-muted text-muted-foreground cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SendIcon className="w-4 h-4" />
|
<SendIcon className="w-4 h-4" />
|
||||||
@@ -399,27 +403,27 @@ export default function TicketDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel — properties */}
|
{/* Right panel — properties */}
|
||||||
<div className="w-80 flex-shrink-0 bg-[#0f1011] border-l border-[rgba(255,255,255,0.05)] flex flex-col overflow-y-auto">
|
<div className="w-80 flex-shrink-0 bg-sidebar border-l border-border flex flex-col overflow-y-auto">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-5">
|
||||||
{/* Status */}
|
{/* Section: Status */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
Status
|
Status
|
||||||
</label>
|
</h3>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setStatusSelectOpen(!statusSelectOpen)}
|
onClick={() => setStatusSelectOpen(!statusSelectOpen)}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a] text-sm hover:border-[rgba(255,255,255,0.15)] transition-colors"
|
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg border border-border bg-background text-sm hover:border-foreground/20 transition-all duration-150"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||||
style={{ backgroundColor: currentStatusColor }}
|
style={{ backgroundColor: currentStatusColor }}
|
||||||
/>
|
/>
|
||||||
<span className="text-[#f7f8f8] font-medium flex-1 text-left">
|
<span className="text-foreground font-medium flex-1 text-left">
|
||||||
{currentStatusLabel}
|
{currentStatusLabel}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-[#8a8f98]"
|
className="w-4 h-4 text-muted-foreground"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -434,7 +438,7 @@ export default function TicketDetailPage({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{statusSelectOpen && (
|
{statusSelectOpen && (
|
||||||
<div className="absolute top-full left-0 right-0 mt-1 z-10 bg-[#191a1b] border border-[rgba(255,255,255,0.08)] rounded-lg shadow-xl overflow-hidden">
|
<div className="absolute top-full left-0 right-0 mt-1 z-10 bg-popover border border-border rounded-lg shadow-xl overflow-hidden">
|
||||||
{ALL_STATUSES.map((status) => {
|
{ALL_STATUSES.map((status) => {
|
||||||
const color = STATUS_COLORS[status];
|
const color = STATUS_COLORS[status];
|
||||||
const label = STATUS_LABELS[status];
|
const label = STATUS_LABELS[status];
|
||||||
@@ -445,19 +449,19 @@ export default function TicketDetailPage({
|
|||||||
onClick={() => handleStatusSelect(status)}
|
onClick={() => handleStatusSelect(status)}
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors",
|
"w-full flex items-center gap-3 px-4 py-2.5 text-sm text-left transition-all duration-150",
|
||||||
isCurrent
|
isCurrent
|
||||||
? "bg-[rgba(255,255,255,0.03)] text-[#8a8f98] cursor-default"
|
? "bg-accent text-muted-foreground cursor-default"
|
||||||
: "text-[#d0d6e0] hover:bg-[rgba(255,255,255,0.05)]"
|
: "text-foreground hover:bg-accent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
{isCurrent && (
|
{isCurrent && (
|
||||||
<span className="text-xs text-[#8a8f98] ml-auto">
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
current
|
current
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -471,10 +475,10 @@ export default function TicketDetailPage({
|
|||||||
|
|
||||||
{/* Status change preview */}
|
{/* Status change preview */}
|
||||||
{preview && (
|
{preview && (
|
||||||
<div className="p-3 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)]">
|
<div className="p-3 rounded-lg bg-background border border-border">
|
||||||
<p className="text-xs text-[#8a8f98] mb-2">
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
Preview: changing to{" "}
|
Preview: changing to{" "}
|
||||||
<span className="text-[#f7f8f8] font-medium">
|
<span className="text-foreground font-medium">
|
||||||
{STATUS_LABELS[pendingStatus || ""]}
|
{STATUS_LABELS[pendingStatus || ""]}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -483,15 +487,15 @@ export default function TicketDetailPage({
|
|||||||
{preview.prepared_scrips.map((scrip) => (
|
{preview.prepared_scrips.map((scrip) => (
|
||||||
<div
|
<div
|
||||||
key={scrip.scripId}
|
key={scrip.scripId}
|
||||||
className="text-xs text-[#d0d6e0] flex items-center gap-1.5"
|
className="text-xs text-foreground flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<span className="w-2 h-2 rounded-full bg-[#f59e0b] flex-shrink-0" />
|
<span className="w-2 h-2 rounded-full bg-chart-3 flex-shrink-0" />
|
||||||
{scrip.scripName}
|
{scrip.scripName}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-[#8a8f98] mb-3">
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
No scrips will fire
|
No scrips will fire
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -499,7 +503,7 @@ export default function TicketDetailPage({
|
|||||||
<button
|
<button
|
||||||
onClick={handleApplyStatus}
|
onClick={handleApplyStatus}
|
||||||
disabled={applyLoading}
|
disabled={applyLoading}
|
||||||
className="px-2.5 py-1 rounded-md text-xs font-medium bg-[#5e6ad2] hover:bg-[#7170ff] text-[#f7f8f8] disabled:opacity-50 transition-colors"
|
className="px-2.5 py-1 rounded-md text-xs font-medium bg-primary hover:bg-primary/80 text-primary-foreground disabled:opacity-50 transition-all duration-150"
|
||||||
>
|
>
|
||||||
{applyLoading
|
{applyLoading
|
||||||
? "Applying..."
|
? "Applying..."
|
||||||
@@ -508,7 +512,7 @@ export default function TicketDetailPage({
|
|||||||
<button
|
<button
|
||||||
onClick={handleCancelStatus}
|
onClick={handleCancelStatus}
|
||||||
disabled={applyLoading}
|
disabled={applyLoading}
|
||||||
className="px-2.5 py-1 rounded-md text-xs font-medium text-[#8a8f98] hover:text-[#d0d6e0] transition-colors"
|
className="px-2.5 py-1 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground transition-all duration-150"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -517,27 +521,27 @@ export default function TicketDetailPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{previewError && (
|
{previewError && (
|
||||||
<div className="p-2 rounded-lg bg-red-400/5 border border-red-400/10">
|
<div className="p-2 rounded-lg bg-destructive/5 border border-destructive/10">
|
||||||
<p className="text-xs text-red-400">{previewError}</p>
|
<p className="text-xs text-destructive">{previewError}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{scripResults && (
|
{scripResults && (
|
||||||
<div className="p-3 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)]">
|
<div className="p-3 rounded-lg bg-background border border-border">
|
||||||
<p className="text-xs text-[#8a8f98] mb-2">Scrip results:</p>
|
<p className="text-xs text-muted-foreground mb-2">Scrip results:</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{scripResults.map((result) => (
|
{scripResults.map((result) => (
|
||||||
<div
|
<div
|
||||||
key={result.scripId}
|
key={result.scripId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs flex items-center gap-1.5",
|
"text-xs flex items-center gap-1.5",
|
||||||
result.success ? "text-[#22c55e]" : "text-red-400"
|
result.success ? "text-[#22c55e]" : "text-destructive"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-2 h-2 rounded-full flex-shrink-0",
|
"w-2 h-2 rounded-full flex-shrink-0",
|
||||||
result.success ? "bg-[#22c55e]" : "bg-red-400"
|
result.success ? "bg-[#22c55e]" : "bg-destructive"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{result.message}
|
{result.message}
|
||||||
@@ -546,119 +550,134 @@ export default function TicketDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setScripResults(null)}
|
onClick={() => setScripResults(null)}
|
||||||
className="mt-2 text-xs text-[#8a8f98] hover:text-[#d0d6e0]"
|
className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-all duration-150"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Priority (placeholder) */}
|
<Separator />
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
|
||||||
Priority
|
|
||||||
</label>
|
|
||||||
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a] text-sm text-[#8a8f98]">
|
|
||||||
Not set
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assignee */}
|
{/* Section: Assignment */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
Assignee
|
Assignment
|
||||||
</label>
|
</h3>
|
||||||
{ticket.owner_id ? (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a]">
|
|
||||||
<div
|
|
||||||
className="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0"
|
|
||||||
style={{
|
|
||||||
backgroundColor: getInitialColor(ticket.owner_id),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] font-semibold text-[#f7f8f8]">
|
|
||||||
{getInitial(ticket.owner_id)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-[#f7f8f8]">
|
|
||||||
{ticket.owner_id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a] text-sm text-[#8a8f98]">
|
|
||||||
Unassigned
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Queue */}
|
{/* Assignee */}
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
<label className="block text-[11px] font-medium text-muted-foreground mb-1">
|
||||||
Queue
|
Assignee
|
||||||
</label>
|
|
||||||
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a]">
|
|
||||||
<span className="text-sm text-[#f7f8f8]">
|
|
||||||
{queue?.name || ticket.queue_id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom fields */}
|
|
||||||
{ticket.custom_fields && ticket.custom_fields.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
|
||||||
Custom fields
|
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-1.5">
|
{ticket.owner_id ? (
|
||||||
{ticket.custom_fields.map((cf) => (
|
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-background transition-all duration-150 hover:border-foreground/20">
|
||||||
<div
|
<div
|
||||||
key={cf.id}
|
className="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0"
|
||||||
className="flex justify-between items-center px-3 py-1.5 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a]"
|
style={{
|
||||||
|
backgroundColor: getInitialColor(ticket.owner_id),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-xs text-[#8a8f98]">
|
<span className="text-[10px] font-semibold text-primary-foreground">
|
||||||
{cf.custom_field?.name || cf.custom_field_id}
|
{getInitial(ticket.owner_id)}
|
||||||
</span>
|
|
||||||
<span className="text-xs text-[#f7f8f8] font-medium">
|
|
||||||
{cf.value}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<span className="text-sm text-foreground">
|
||||||
|
{ticket.owner_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-muted-foreground">
|
||||||
|
Unassigned
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority (placeholder) */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-muted-foreground mb-1">
|
||||||
|
Priority
|
||||||
|
</label>
|
||||||
|
<div className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-muted-foreground">
|
||||||
|
Not set
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Dates */}
|
<Separator />
|
||||||
|
|
||||||
|
{/* Section: Details */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
Dates
|
Details
|
||||||
</label>
|
</h3>
|
||||||
<div className="space-y-1 text-xs">
|
|
||||||
<div className="flex justify-between px-1">
|
{/* Queue */}
|
||||||
<span className="text-[#8a8f98]">Created</span>
|
<div className="mb-3">
|
||||||
<span className="text-[#d0d6e0] tabular-nums">
|
<label className="block text-[11px] font-medium text-muted-foreground mb-1">
|
||||||
{formatDistanceToNow(new Date(ticket.created_at), {
|
Queue
|
||||||
addSuffix: true,
|
</label>
|
||||||
})}
|
<div className="px-3 py-2 rounded-lg border border-border bg-background">
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{queue?.name || ticket.queue_id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between px-1">
|
</div>
|
||||||
<span className="text-[#8a8f98]">Updated</span>
|
|
||||||
<span className="text-[#d0d6e0] tabular-nums">
|
{/* Custom fields */}
|
||||||
{formatDistanceToNow(new Date(ticket.updated_at), {
|
{ticket.custom_fields && ticket.custom_fields.length > 0 && (
|
||||||
addSuffix: true,
|
<div className="mb-3">
|
||||||
})}
|
<label className="block text-[11px] font-medium text-muted-foreground mb-1">
|
||||||
</span>
|
Custom fields
|
||||||
|
</label>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{ticket.custom_fields.map((cf) => (
|
||||||
|
<div
|
||||||
|
key={cf.id}
|
||||||
|
className="flex justify-between items-center px-3 py-1.5 rounded-lg border border-border bg-background"
|
||||||
|
>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{cf.custom_field?.name || cf.custom_field_id}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-foreground font-medium">
|
||||||
|
{cf.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ticket.resolved_at && (
|
)}
|
||||||
<div className="flex justify-between px-1">
|
|
||||||
<span className="text-[#8a8f98]">Resolved</span>
|
{/* Dates */}
|
||||||
<span className="text-[#d0d6e0] tabular-nums">
|
<div>
|
||||||
{formatDistanceToNow(new Date(ticket.resolved_at), {
|
<div className="space-y-1.5 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Created</span>
|
||||||
|
<span className="text-foreground tabular-nums">
|
||||||
|
{formatDistanceToNow(new Date(ticket.created_at), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Updated</span>
|
||||||
|
<span className="text-foreground tabular-nums">
|
||||||
|
{formatDistanceToNow(new Date(ticket.updated_at), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{ticket.resolved_at && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Resolved</span>
|
||||||
|
<span className="text-foreground tabular-nums">
|
||||||
|
{formatDistanceToNow(new Date(ticket.resolved_at), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user