|
@@ -1,6 +1,7 @@
|
|
|
-"use client";
|
|
|
|
|
|
|
+'use client'
|
|
|
|
|
|
|
|
-import { Button } from "@/components/ui/button";
|
|
|
|
|
|
|
+import { useState } from 'react'
|
|
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
import {
|
|
import {
|
|
|
Camera,
|
|
Camera,
|
|
|
Download,
|
|
Download,
|
|
@@ -8,15 +9,18 @@ import {
|
|
|
Image as ImageIcon,
|
|
Image as ImageIcon,
|
|
|
Loader2,
|
|
Loader2,
|
|
|
Paperclip,
|
|
Paperclip,
|
|
|
|
|
+ Play,
|
|
|
Trash2,
|
|
Trash2,
|
|
|
-} from "lucide-react";
|
|
|
|
|
-import { formatFileSize } from "./types";
|
|
|
|
|
-import type { Attachment } from "./types";
|
|
|
|
|
|
|
+ Video,
|
|
|
|
|
+ X,
|
|
|
|
|
+} from 'lucide-react'
|
|
|
|
|
+import { formatFileSize } from './types'
|
|
|
|
|
+import type { Attachment } from './types'
|
|
|
|
|
|
|
|
function getFileIcon(type: string) {
|
|
function getFileIcon(type: string) {
|
|
|
- if (type === "application/pdf") return <FileText className="h-5 w-5 text-red-500" />;
|
|
|
|
|
- if (type.startsWith("image/")) return <ImageIcon className="h-5 w-5 text-blue-500" />;
|
|
|
|
|
- return <Paperclip className="h-5 w-5 text-muted-foreground" />;
|
|
|
|
|
|
|
+ if (type === 'application/pdf') return <FileText className="h-5 w-5 text-red-500" />
|
|
|
|
|
+ if (type.startsWith('image/')) return <ImageIcon className="h-5 w-5 text-blue-500" />
|
|
|
|
|
+ return <Paperclip className="h-5 w-5 text-muted-foreground" />
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function AttachmentRow({
|
|
function AttachmentRow({
|
|
@@ -24,9 +28,9 @@ function AttachmentRow({
|
|
|
onDelete,
|
|
onDelete,
|
|
|
deleting,
|
|
deleting,
|
|
|
}: {
|
|
}: {
|
|
|
- attachment: Attachment;
|
|
|
|
|
- onDelete: (id: string) => void;
|
|
|
|
|
- deleting: boolean;
|
|
|
|
|
|
|
+ attachment: Attachment
|
|
|
|
|
+ onDelete: (id: string) => void
|
|
|
|
|
+ deleting: boolean
|
|
|
}) {
|
|
}) {
|
|
|
return (
|
|
return (
|
|
|
<div className="grid grid-cols-[2rem_1fr_auto] items-center gap-2 rounded border p-2">
|
|
<div className="grid grid-cols-[2rem_1fr_auto] items-center gap-2 rounded border p-2">
|
|
@@ -34,7 +38,9 @@ function AttachmentRow({
|
|
|
{getFileIcon(attachment.fileType)}
|
|
{getFileIcon(attachment.fileType)}
|
|
|
</div>
|
|
</div>
|
|
|
<div className="overflow-hidden">
|
|
<div className="overflow-hidden">
|
|
|
- <p className="truncate text-xs font-medium" title={attachment.fileName}>{attachment.fileName}</p>
|
|
|
|
|
|
|
+ <p className="truncate text-xs font-medium" title={attachment.fileName}>
|
|
|
|
|
+ {attachment.fileName}
|
|
|
|
|
+ </p>
|
|
|
<p className="text-xs text-muted-foreground">{formatFileSize(attachment.fileSize)}</p>
|
|
<p className="text-xs text-muted-foreground">{formatFileSize(attachment.fileSize)}</p>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex items-center gap-0.5">
|
|
<div className="flex items-center gap-0.5">
|
|
@@ -54,15 +60,15 @@ function AttachmentRow({
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- );
|
|
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ServiceAttachmentsProps {
|
|
interface ServiceAttachmentsProps {
|
|
|
- attachments: Attachment[];
|
|
|
|
|
- imageAttachments: Attachment[];
|
|
|
|
|
- onImageClick: (index: number) => void;
|
|
|
|
|
- onDeleteAttachment: (id: string) => void;
|
|
|
|
|
- deletingAttachment: string | null;
|
|
|
|
|
|
|
+ attachments: Attachment[]
|
|
|
|
|
+ imageAttachments: Attachment[]
|
|
|
|
|
+ onImageClick: (index: number) => void
|
|
|
|
|
+ onDeleteAttachment: (id: string) => void
|
|
|
|
|
+ deletingAttachment: string | null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function ServiceAttachments({
|
|
export function ServiceAttachments({
|
|
@@ -72,8 +78,10 @@ export function ServiceAttachments({
|
|
|
onDeleteAttachment,
|
|
onDeleteAttachment,
|
|
|
deletingAttachment,
|
|
deletingAttachment,
|
|
|
}: ServiceAttachmentsProps) {
|
|
}: ServiceAttachmentsProps) {
|
|
|
- const diagnostics = attachments.filter((a) => a.category === "diagnostic");
|
|
|
|
|
- const documents = attachments.filter((a) => a.category === "document");
|
|
|
|
|
|
|
+ const diagnostics = attachments.filter((a) => a.category === 'diagnostic')
|
|
|
|
|
+ const documents = attachments.filter((a) => a.category === 'document')
|
|
|
|
|
+ const videos = attachments.filter((a) => a.category === 'video')
|
|
|
|
|
+ const [activeVideo, setActiveVideo] = useState<Attachment | null>(null)
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
@@ -111,9 +119,11 @@ export function ServiceAttachments({
|
|
|
disabled={deletingAttachment === attachment.id}
|
|
disabled={deletingAttachment === attachment.id}
|
|
|
onClick={() => onDeleteAttachment(attachment.id)}
|
|
onClick={() => onDeleteAttachment(attachment.id)}
|
|
|
>
|
|
>
|
|
|
- {deletingAttachment === attachment.id
|
|
|
|
|
- ? <Loader2 className="h-2.5 w-2.5 animate-spin" />
|
|
|
|
|
- : <Trash2 className="h-2.5 w-2.5" />}
|
|
|
|
|
|
|
+ {deletingAttachment === attachment.id ? (
|
|
|
|
|
+ <Loader2 className="h-2.5 w-2.5 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Trash2 className="h-2.5 w-2.5" />
|
|
|
|
|
+ )}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -123,6 +133,61 @@ export function ServiceAttachments({
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {videos.length > 0 && (
|
|
|
|
|
+ <div className="min-w-0 overflow-hidden rounded-lg border p-3">
|
|
|
|
|
+ <h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
|
|
|
|
|
+ <Video className="h-3.5 w-3.5" />
|
|
|
|
|
+ Videos ({videos.length})
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <div className="space-y-1.5">
|
|
|
|
|
+ {videos.map((a) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={a.id}
|
|
|
|
|
+ className="grid grid-cols-[2rem_1fr_auto] items-center gap-2 rounded border p-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveVideo(a)}
|
|
|
|
|
+ className="flex h-8 w-8 items-center justify-center rounded bg-muted/50 transition-colors hover:bg-muted"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Play className="h-4 w-4 text-primary" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveVideo(a)}
|
|
|
|
|
+ className="overflow-hidden text-left"
|
|
|
|
|
+ >
|
|
|
|
|
+ <p className="truncate text-xs font-medium" title={a.fileName}>
|
|
|
|
|
+ {a.fileName}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">{formatFileSize(a.fileSize)}</p>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div className="flex items-center gap-0.5">
|
|
|
|
|
+ <Button variant="ghost" size="icon" className="h-6 w-6" asChild>
|
|
|
|
|
+ <a href={a.fileUrl} download={a.fileName}>
|
|
|
|
|
+ <Download className="h-3 w-3" />
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
|
|
|
|
+ disabled={deletingAttachment === a.id}
|
|
|
|
|
+ onClick={() => onDeleteAttachment(a.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {deletingAttachment === a.id ? (
|
|
|
|
|
+ <Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Trash2 className="h-3 w-3" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{diagnostics.length > 0 && (
|
|
{diagnostics.length > 0 && (
|
|
|
<div className="min-w-0 overflow-hidden rounded-lg border p-3">
|
|
<div className="min-w-0 overflow-hidden rounded-lg border p-3">
|
|
|
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
|
|
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
|
|
@@ -160,6 +225,30 @@ export function ServiceAttachments({
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Fullscreen video player */}
|
|
|
|
|
+ {activeVideo && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
|
|
|
|
|
+ onClick={() => setActiveVideo(null)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ className="absolute right-4 top-4 h-10 w-10 text-white hover:bg-white/10"
|
|
|
|
|
+ onClick={() => setActiveVideo(null)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <X className="h-6 w-6" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <video
|
|
|
|
|
+ src={activeVideo.fileUrl}
|
|
|
|
|
+ controls
|
|
|
|
|
+ autoPlay
|
|
|
|
|
+ className="max-h-[90vh] max-w-[90vw] rounded-lg"
|
|
|
|
|
+ onClick={(e) => e.stopPropagation()}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</>
|
|
</>
|
|
|
- );
|
|
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|