- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints. - Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete. - Created VendorContext for managing vendor state across components. - Developed VendorGeneralInfo component to display detailed vendor information. - Introduced AedSymbol and Money components for consistent currency representation. - Added PromptDialog for user input prompts throughout the application. - Implemented RelationLink component for unified related-data display in CRUD tables. - Created InspectionTemplatesClient for API interactions related to inspection templates.
140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { toast } from "sonner"
|
|
import { Check, Copy, ExternalLink, Link2, XCircle } from "lucide-react"
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/components/ui/dialog"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { confirm } from "@/shared/components/confirm-dialog"
|
|
|
|
export function InspectionShareDialog({
|
|
open,
|
|
onOpenChange,
|
|
inspectionId,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (o: boolean) => void
|
|
inspectionId: number | string
|
|
}) {
|
|
const api = useAuthApi()
|
|
const [loading, setLoading] = useState(false)
|
|
const [shareUrl, setShareUrl] = useState<string | null>(null)
|
|
const [expiresAt, setExpiresAt] = useState<string | null>(null)
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
const generate = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await api.inspections.share(inspectionId)
|
|
setShareUrl(res.data.share_url)
|
|
setExpiresAt(res.data.share_expires_at)
|
|
} catch (e: any) {
|
|
toast.error(e?.payload?.message ?? e?.message ?? "Failed to generate share link")
|
|
onOpenChange(false)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (open && !shareUrl) generate()
|
|
if (!open) {
|
|
setShareUrl(null)
|
|
setExpiresAt(null)
|
|
setCopied(false)
|
|
}
|
|
}, [open])
|
|
|
|
const copy = async () => {
|
|
if (!shareUrl) return
|
|
try {
|
|
await navigator.clipboard.writeText(shareUrl)
|
|
setCopied(true)
|
|
toast.success("Link copied")
|
|
setTimeout(() => setCopied(false), 2000)
|
|
} catch {
|
|
toast.error("Copy failed — select the field and press Cmd/Ctrl+C")
|
|
}
|
|
}
|
|
|
|
const revoke = async () => {
|
|
const ok = await confirm({
|
|
title: "Revoke this share link?",
|
|
description: "The customer will no longer be able to view the report.",
|
|
confirmLabel: "Revoke",
|
|
variant: "destructive",
|
|
})
|
|
if (!ok) return
|
|
try {
|
|
await api.inspections.revokeShare(inspectionId)
|
|
toast.success("Share link revoked")
|
|
onOpenChange(false)
|
|
} catch (e: any) {
|
|
toast.error(e?.payload?.message ?? e?.message ?? "Failed to revoke")
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Link2 className="size-4" /> Share inspection with customer
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Anyone with this link can view the inspection report. No login required.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{loading && (
|
|
<div className="py-6 text-center text-sm text-muted-foreground">Generating link…</div>
|
|
)}
|
|
|
|
{shareUrl && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Input value={shareUrl} readOnly className="font-mono text-xs" />
|
|
<Button type="button" size="sm" onClick={copy}>
|
|
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
{copied ? "Copied" : "Copy"}
|
|
</Button>
|
|
</div>
|
|
{expiresAt && (
|
|
<p className="text-xs text-muted-foreground">
|
|
Expires on {new Date(expiresAt).toLocaleDateString()}.
|
|
</p>
|
|
)}
|
|
<div className="flex items-center justify-between text-xs">
|
|
<a
|
|
href={shareUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary inline-flex items-center gap-1 hover:underline"
|
|
>
|
|
<ExternalLink className="size-3.5" /> Open preview in new tab
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter className="flex justify-between gap-2 sm:justify-between">
|
|
<Button variant="ghost" size="sm" onClick={revoke} disabled={!shareUrl}>
|
|
<XCircle className="size-4 text-rose-600" /> Revoke link
|
|
</Button>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>Done</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|