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:
@@ -10,7 +10,7 @@ import { useAuthApi } from "@/shared/useApi"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||
import { JOB_CARD_ROUTES } from "@garage/api"
|
||||
import { ApiError, JOB_CARD_ROUTES } from "@garage/api"
|
||||
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { CONSTANTS } from "@/config/constants"
|
||||
@@ -22,6 +22,62 @@ function getFileIcon(mimeType?: string) {
|
||||
return FileIcon
|
||||
}
|
||||
|
||||
const ALLOWED_HINT = "Allowed: images, PDF, Office docs, audio, video."
|
||||
|
||||
function describeAttachmentError(rawMessage: string, filename: string): string {
|
||||
const msg = rawMessage.toLowerCase()
|
||||
if (msg.includes("must be a file of type") || msg.includes("mimes")) {
|
||||
return `${filename}: unsupported file type. ${ALLOWED_HINT}`
|
||||
}
|
||||
if (msg.includes("may not be greater") || msg.includes("max")) {
|
||||
return `${filename}: exceeds 5 MB limit.`
|
||||
}
|
||||
if (msg.includes("must be a file")) {
|
||||
return `${filename}: invalid file.`
|
||||
}
|
||||
return `${filename}: ${rawMessage}`
|
||||
}
|
||||
|
||||
function showUploadError(err: unknown, files: File[]) {
|
||||
if (!(err instanceof ApiError)) {
|
||||
toast.error("Failed to upload attachment(s)")
|
||||
return
|
||||
}
|
||||
|
||||
const validation = err.validationErrors
|
||||
if (!validation || Object.keys(validation).length === 0) {
|
||||
toast.error(err.payload?.message ?? "Failed to upload attachment(s)")
|
||||
return
|
||||
}
|
||||
|
||||
const messages: string[] = []
|
||||
for (const [key, msgs] of Object.entries(validation)) {
|
||||
const firstMsg = Array.isArray(msgs) ? msgs[0] : String(msgs)
|
||||
if (!firstMsg) continue
|
||||
|
||||
const match = key.match(/^attachments\.(\d+)$/)
|
||||
if (match) {
|
||||
const idx = Number(match[1])
|
||||
const filename = files[idx]?.name ?? `File #${idx + 1}`
|
||||
messages.push(describeAttachmentError(firstMsg, filename))
|
||||
} else if (key === "attachments") {
|
||||
messages.push(firstMsg)
|
||||
} else {
|
||||
messages.push(firstMsg)
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
toast.error(err.payload?.message ?? "Failed to upload attachment(s)")
|
||||
return
|
||||
}
|
||||
|
||||
const shown = messages.slice(0, 3)
|
||||
const extra = messages.length - shown.length
|
||||
shown.forEach((m) => toast.error(m))
|
||||
if (extra > 0) toast.error(`...and ${extra} more`)
|
||||
}
|
||||
|
||||
export default function JobCardAttachmentsPage() {
|
||||
const { id: jobCardId } = useParams<{ id: string }>()
|
||||
const api = useAuthApi()
|
||||
@@ -66,18 +122,19 @@ export default function JobCardAttachmentsPage() {
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const fileList = Array.from(files)
|
||||
setIsUploading(true)
|
||||
const promise = api.jobCards.addAttachment(jobCardId, Array.from(files))
|
||||
toast.promise(promise, {
|
||||
loading: "Uploading attachment(s)...",
|
||||
success: "Attachment(s) uploaded successfully",
|
||||
error: "Failed to upload attachment(s)",
|
||||
})
|
||||
const loadingToast = toast.loading("Uploading attachment(s)...")
|
||||
|
||||
try {
|
||||
await promise
|
||||
await api.jobCards.addAttachment(jobCardId, fileList)
|
||||
toast.dismiss(loadingToast)
|
||||
toast.success("Attachment(s) uploaded successfully")
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
startRefreshTransition(() => router.refresh())
|
||||
} catch (err) {
|
||||
toast.dismiss(loadingToast)
|
||||
showUploadError(err, fileList)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
if (fileInputRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user