feat: add document sharing functionality across various modules
- Introduced ShareDocumentButton component for sharing documents. - Added ShareDocumentDialog for email and WhatsApp sharing options. - Integrated document sharing in estimates, invoices, inspections, job cards, bills, and purchase orders. - Implemented useDocumentShare hook for handling share logic. - Created DocumentShareClient for API interactions related to document sharing. - Updated layouts and actions to include sharing options for relevant entities.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Share2 } from "lucide-react"
|
||||
import type { DocumentShareType } from "@garage/api"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
||||
|
||||
interface ShareDocumentButtonProps {
|
||||
type: DocumentShareType
|
||||
id: string | number
|
||||
label?: string
|
||||
variant?: "default" | "outline" | "ghost" | "secondary"
|
||||
}
|
||||
|
||||
export function ShareDocumentButton({ type, id, label = "Share", variant = "outline" }: ShareDocumentButtonProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={variant} onClick={() => setOpen(true)}>
|
||||
<Share2 className="size-4" />
|
||||
{label}
|
||||
</Button>
|
||||
<ShareDocumentDialog type={type} id={id} open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import type { DocumentShareType } from "@garage/api"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { Label } from "@/shared/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/components/ui/tabs"
|
||||
import { Textarea } from "@/shared/components/ui/textarea"
|
||||
import { useDocumentShare } from "@/shared/hooks/use-document-share"
|
||||
|
||||
interface ShareDocumentDialogProps {
|
||||
type: DocumentShareType
|
||||
id: string | number
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const DEFAULT_MESSAGES: Record<string, { email: string; whatsapp: string }> = {
|
||||
estimate: {
|
||||
email: "Hello, please find your estimate at the link below. Kindly review the details and let us know if you'd like to proceed.",
|
||||
whatsapp: "Hello, here is your estimate. Please review and let us know if you'd like to proceed.",
|
||||
},
|
||||
invoice: {
|
||||
email: "Hello, please find your invoice at the link below. Let us know once payment has been arranged.",
|
||||
whatsapp: "Hello, here is your invoice. Let us know once payment is arranged.",
|
||||
},
|
||||
job_card: {
|
||||
email: "Hello, please find the job card for your vehicle at the link below.",
|
||||
whatsapp: "Hello, here is the job card for your vehicle.",
|
||||
},
|
||||
inspection: {
|
||||
email: "Hello, please find your vehicle inspection report at the link below.",
|
||||
whatsapp: "Hello, here is your vehicle inspection report.",
|
||||
},
|
||||
payment_received: {
|
||||
email: "Hello, please find your payment receipt at the link below. Thank you.",
|
||||
whatsapp: "Hello, here is your payment receipt. Thank you.",
|
||||
},
|
||||
purchase_order: {
|
||||
email: "Hello, please find our purchase order at the link below. Kindly confirm receipt.",
|
||||
whatsapp: "Hello, here is our purchase order. Kindly confirm receipt.",
|
||||
},
|
||||
bill: {
|
||||
email: "Hello, please find the bill at the link below for your reference.",
|
||||
whatsapp: "Hello, here is the bill for your reference.",
|
||||
},
|
||||
expense: {
|
||||
email: "Hello, please find the expense document at the link below.",
|
||||
whatsapp: "Hello, here is the expense document.",
|
||||
},
|
||||
payment_made: {
|
||||
email: "Hello, please find the payment confirmation at the link below.",
|
||||
whatsapp: "Hello, here is the payment confirmation.",
|
||||
},
|
||||
}
|
||||
|
||||
function getDefaultMessage(type: DocumentShareType, channel: "email" | "whatsapp"): string {
|
||||
return DEFAULT_MESSAGES[type as string]?.[channel] ?? ""
|
||||
}
|
||||
|
||||
export function ShareDocumentDialog({ type, id, open, onOpenChange }: ShareDocumentDialogProps) {
|
||||
const { shareEmail, shareWhatsapp, isSharing } = useDocumentShare(type, id)
|
||||
const [email, setEmail] = useState("")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [emailMessage, setEmailMessage] = useState("")
|
||||
const [whatsappMessage, setWhatsappMessage] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setEmailMessage(getDefaultMessage(type, "email"))
|
||||
setWhatsappMessage(getDefaultMessage(type, "whatsapp"))
|
||||
}
|
||||
}, [open, type])
|
||||
|
||||
const close = () => {
|
||||
onOpenChange(false)
|
||||
setEmail("")
|
||||
setPhone("")
|
||||
setEmailMessage("")
|
||||
setWhatsappMessage("")
|
||||
}
|
||||
|
||||
const handleEmail = async () => {
|
||||
if (!email) return
|
||||
await shareEmail({ email, message: emailMessage || undefined })
|
||||
close()
|
||||
}
|
||||
|
||||
const handleWhatsapp = async () => {
|
||||
await shareWhatsapp({ phone: phone || undefined, message: whatsappMessage || undefined })
|
||||
close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share document</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="email" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="email">Email</TabsTrigger>
|
||||
<TabsTrigger value="whatsapp">WhatsApp</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="email" className="space-y-3 py-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="share-email">Recipient email</Label>
|
||||
<Input
|
||||
id="share-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="customer@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="share-email-message">Message</Label>
|
||||
<Textarea
|
||||
id="share-email-message"
|
||||
value={emailMessage}
|
||||
onChange={(e) => setEmailMessage(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button variant="outline" type="button" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={handleEmail} disabled={!email || isSharing}>
|
||||
{isSharing ? "Sending..." : "Send email"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
<TabsContent value="whatsapp" className="space-y-3 py-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="share-phone">Phone with country code (optional)</Label>
|
||||
<Input
|
||||
id="share-phone"
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+971501234567"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave empty to pick recipient inside WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="share-wa-message">Message</Label>
|
||||
<Textarea
|
||||
id="share-wa-message"
|
||||
value={whatsappMessage}
|
||||
onChange={(e) => setWhatsappMessage(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button variant="outline" type="button" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={handleWhatsapp} disabled={isSharing}>
|
||||
{isSharing ? "Opening..." : "Open WhatsApp"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Share link expires 24 hours after creation.
|
||||
</p>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import type { DocumentShareResponse, DocumentShareType } from "@garage/api"
|
||||
import { useAuthApi } from "../useApi"
|
||||
|
||||
interface UseDocumentShareOptions {
|
||||
onSuccess?: (result: DocumentShareResponse) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export function useDocumentShare(type: DocumentShareType, id: string | number, options?: UseDocumentShareOptions) {
|
||||
const api = useAuthApi()
|
||||
const [isSharing, setIsSharing] = useState(false)
|
||||
|
||||
const shareEmail = async (input: { email: string; message?: string }) => {
|
||||
setIsSharing(true)
|
||||
try {
|
||||
const result = await api.documentShare.share({
|
||||
document_type: type,
|
||||
document_id: id,
|
||||
channel: "email",
|
||||
email: input.email,
|
||||
message: input.message,
|
||||
})
|
||||
toast.success("Email sent. Link expires in 24 hours.")
|
||||
options?.onSuccess?.(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
toast.error("Failed to send email")
|
||||
options?.onError?.(error as Error)
|
||||
throw error
|
||||
} finally {
|
||||
setIsSharing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const shareWhatsapp = async (input: { phone?: string; message?: string }) => {
|
||||
setIsSharing(true)
|
||||
try {
|
||||
const result = await api.documentShare.share({
|
||||
document_type: type,
|
||||
document_id: id,
|
||||
channel: "link",
|
||||
phone: input.phone,
|
||||
message: input.message,
|
||||
})
|
||||
window.open(result.whatsapp_url, "_blank", "noopener,noreferrer")
|
||||
toast.success("Share link ready. Expires in 24 hours.")
|
||||
options?.onSuccess?.(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
toast.error("Failed to create share link")
|
||||
options?.onError?.(error as Error)
|
||||
throw error
|
||||
} finally {
|
||||
setIsSharing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return { shareEmail, shareWhatsapp, isSharing }
|
||||
}
|
||||
Reference in New Issue
Block a user