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:
2026-05-14 12:21:01 +04:00
parent 05b55b5721
commit fcbba6247d
22 changed files with 571 additions and 60 deletions
@@ -1,7 +1,7 @@
"use client"
import { useState } from "react"
import { ClipboardList } from "lucide-react"
import { ClipboardList, Loader2 } from "lucide-react"
import { useRouter } from "next/navigation"
import { ApiError } from "@garage/api"
import { Button } from "@/shared/components/ui/button"
@@ -30,34 +30,37 @@ export function CreateJobCardFromEstimateButton() {
if (!confirmed) return
setIsConverting(true)
const promise = api.estimates.convertToJobCard(estimateId, {})
toast.promise(promise, {
loading: "Generating job card...",
success: "Estimate converted to job card successfully",
error: (error: unknown) =>
error instanceof ApiError && error.status === 409
? "A job card already exists for this estimate."
: "Failed to convert estimate to job card",
})
try {
const response = await api.estimates.convertToJobCard(estimateId, {})
const response = await promise
const jobCardId = response?.data?.id
toast.success("Estimate converted to job card successfully")
if (jobCardId) {
router.push(`/sales/job-cards/${jobCardId}`)
return
}
} catch (error) {
if (error instanceof ApiError && error.status === 409) {
const jobCardId = (error.payload?.data as { job_card_id?: number } | undefined)?.job_card_id
toast.info("A job card already exists for this estimate.")
if (jobCardId) {
router.push(`/sales/job-cards/${jobCardId}`)
return
}
}
toast.error("Failed to convert estimate to job card")
} finally {
setIsConverting(false)
}
setIsConverting(false)
}
return (
<Button variant="outline" size="sm" onClick={handleConvert} disabled={isConverting}>
<ClipboardList className="me-2 size-4" />
{isConverting ? <Loader2 className="me-2 size-4 animate-spin" /> : <ClipboardList className="me-2 size-4" />}
{isConverting ? "Generating..." : "Generate Job Card"}
</Button>
)
@@ -24,7 +24,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X, Printer } from "lucide-react"
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X, Printer, Share2 } from "lucide-react"
import { useState } from "react"
import { useMutation, useQuery } from "@tanstack/react-query"
import { toast } from "sonner"
@@ -34,6 +34,7 @@ import { DatePickerField, TimePickerField } from "@/shared/components/form"
import { cn } from "@/shared/lib/utils"
import { EmployeeCombobox, type EmployeeOption } from "../employees/employee-combobox"
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
type EstimateActionsProps = {
estimateId: string
@@ -135,6 +136,7 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
const { print, isPrinting } = useDocumentPrint()
const [editOpen, setEditOpen] = useState(false)
const [authOpen, setAuthOpen] = useState(false)
const [shareOpen, setShareOpen] = useState(false)
const [itemStatuses, setItemStatuses] = useState<Record<string, string>>({})
const [authMethod, setAuthMethod] = useState("in_person")
const [employee, setEmployee] = useState<EmployeeOption | null>(null)
@@ -224,6 +226,10 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
<Printer className="size-4" />
{isPrinting ? "Printing..." : "Print"}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShareOpen(true)}>
<Share2 className="size-4" />
Share
</DropdownMenuItem>
<DropdownMenuItem onClick={openAuthDialog}>
<ShieldCheck className="size-4" />
Store Authorisation
@@ -235,6 +241,8 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
</DropdownMenuContent>
</DropdownMenu>
<ShareDocumentDialog type="estimate" id={estimateId} open={shareOpen} onOpenChange={setShareOpen} />
{/* Edit Dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="min-w-xl">