Compare commits
No commits in common. "fcbba6247d06ae446c37c29c573be81dda80d93b" and "cc7dc1bd1724b301c516eeb1acb90a212c5230f4" have entirely different histories.
fcbba6247d
...
cc7dc1bd17
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(pnpm --version)",
|
|
||||||
"Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")",
|
|
||||||
"Bash(grep -E \"\\\\.\\(tsx?\\)$\")"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
NIXPACKS_NODE_VERSION=22
|
|
||||||
NEXT_PUBLIC_API_URL=http://reparee.test
|
|
||||||
@ -16,14 +16,11 @@ export default function ExpenseItemPage() {
|
|||||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||||
actions: (
|
actions: (
|
||||||
<FormDialog title="Expense Item">
|
<FormDialog title="Expense Item">
|
||||||
{(resourceId, { close }) => (
|
{(resourceId) => (
|
||||||
<ExpenseItemForm
|
<ExpenseItemForm
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
initialData={selectedItem}
|
initialData={selectedItem}
|
||||||
onSuccess={() => {
|
onSuccess={invalidateQuery}
|
||||||
invalidateQuery()
|
|
||||||
close()
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { getServerApi } from '@garage/api/server'
|
|||||||
import { BillActions } from '@/modules/bills/bill-actions'
|
import { BillActions } from '@/modules/bills/bill-actions'
|
||||||
import { BillProvider, type BillResponse } from '@/modules/bills/bill-context'
|
import { BillProvider, type BillResponse } from '@/modules/bills/bill-context'
|
||||||
import BillStatusBadge from '@/modules/bills/bill-status-badge'
|
import BillStatusBadge from '@/modules/bills/bill-status-badge'
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button'
|
|
||||||
import { ReceiptIcon } from 'lucide-react'
|
import { ReceiptIcon } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ export default async function BillDetailLayout(props: {
|
|||||||
actions={
|
actions={
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<BillStatusBadge bill={{id, status:data?.status}} />
|
<BillStatusBadge bill={{id, status:data?.status}} />
|
||||||
<ShareDocumentButton type="bill" id={id} />
|
|
||||||
<BillActions billId={id} />
|
<BillActions billId={id} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { getServerApi } from '@garage/api/server'
|
|||||||
import { PurchaseOrderActions } from '@/modules/purchase-orders/purchase-order-actions'
|
import { PurchaseOrderActions } from '@/modules/purchase-orders/purchase-order-actions'
|
||||||
import { PurchaseOrderProvider } from '@/modules/purchase-orders/purchase-order-context'
|
import { PurchaseOrderProvider } from '@/modules/purchase-orders/purchase-order-context'
|
||||||
import { CreateBillFromPOButton } from '@/modules/purchase-orders/create-bill-from-po-button'
|
import { CreateBillFromPOButton } from '@/modules/purchase-orders/create-bill-from-po-button'
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button'
|
|
||||||
import { ClipboardList } from 'lucide-react'
|
import { ClipboardList } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@ -31,7 +30,6 @@ export default async function layout(props: {
|
|||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CreateBillFromPOButton />
|
<CreateBillFromPOButton />
|
||||||
<ShareDocumentButton type="purchase_order" id={id} />
|
|
||||||
<PurchaseOrderActions purchaseOrderId={id} />
|
<PurchaseOrderActions purchaseOrderId={id} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { EstimateActions } from '@/modules/estimates/estimate-actions'
|
|||||||
import { EstimateProvider } from '@/modules/estimates/estimate-context'
|
import { EstimateProvider } from '@/modules/estimates/estimate-context'
|
||||||
import { CreateInvoiceFromEstimateButton } from '@/modules/estimates/create-invoice-from-estimate-button'
|
import { CreateInvoiceFromEstimateButton } from '@/modules/estimates/create-invoice-from-estimate-button'
|
||||||
import { CreateJobCardFromEstimateButton } from '@/modules/estimates/create-job-card-from-estimate-button'
|
import { CreateJobCardFromEstimateButton } from '@/modules/estimates/create-job-card-from-estimate-button'
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button'
|
|
||||||
import { FileTextIcon } from 'lucide-react'
|
import { FileTextIcon } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { formatDate } from '@/shared/utils/formatters'
|
import { formatDate } from '@/shared/utils/formatters'
|
||||||
@ -41,7 +40,6 @@ export default async function layout(props: {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CreateInvoiceFromEstimateButton />
|
<CreateInvoiceFromEstimateButton />
|
||||||
<CreateJobCardFromEstimateButton />
|
<CreateJobCardFromEstimateButton />
|
||||||
<ShareDocumentButton type="estimate" id={id} />
|
|
||||||
<EstimateActions estimateId={id} />
|
<EstimateActions estimateId={id} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import { ResourcePage } from '@/shared/data-view/resource-page'
|
import { ResourcePage } from '@/shared/data-view/resource-page'
|
||||||
import { ColumnHeader } from '@/shared/data-view/table-view'
|
import { ColumnHeader } from '@/shared/data-view/table-view'
|
||||||
import FormDialog from '@/shared/components/form-dialog'
|
import FormDialog from '@/shared/components/form-dialog'
|
||||||
@ -15,13 +14,11 @@ import { getVehicleLabel } from '@/modules/vehicles/utils/getVehicleLabel'
|
|||||||
import { getFullName } from '@/shared/utils/getFullName'
|
import { getFullName } from '@/shared/utils/getFullName'
|
||||||
|
|
||||||
export default function EstimatesPage() {
|
export default function EstimatesPage() {
|
||||||
const router = useRouter()
|
|
||||||
return (
|
return (
|
||||||
<ResourcePage<EstimatesClient>
|
<ResourcePage<EstimatesClient>
|
||||||
pageTitle="Estimates"
|
pageTitle="Estimates"
|
||||||
routeKey={ESTIMATE_ROUTES.INDEX}
|
routeKey={ESTIMATE_ROUTES.INDEX}
|
||||||
getClient={(api) => api.estimates}
|
getClient={(api) => api.estimates}
|
||||||
onRowClick={(row) => router.push(`/sales/estimates/${(row as any).id}`)}
|
|
||||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||||
actions: (
|
actions: (
|
||||||
<FormDialog title="Estimate">
|
<FormDialog title="Estimate">
|
||||||
@ -42,7 +39,7 @@ export default function EstimatesPage() {
|
|||||||
const item = row.original
|
const item = row.original
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/sales/estimates/${item.id}`} className="flex items-center gap-2 hover:underline" onClick={(e) => e.stopPropagation()}>
|
<Link href={`/sales/estimates/${item.id}`} className="flex items-center gap-2 hover:underline">
|
||||||
<FileTextIcon className="text-muted-foreground h-4 w-4" />
|
<FileTextIcon className="text-muted-foreground h-4 w-4" />
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -59,13 +56,11 @@ export default function EstimatesPage() {
|
|||||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item:any = row.original
|
const item:any = row.original
|
||||||
if (!item.customer?.id) return "—"
|
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" asChild size="sm" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/sales/customers/${item.customer.id}`}>
|
<UserIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<UserIcon /> {getFullName(item.customer) || "—"}
|
<span>{getFullName(item.customer) || "—"}</span>
|
||||||
</Link>
|
</div>
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -74,7 +69,7 @@ export default function EstimatesPage() {
|
|||||||
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item :any= row.original
|
const item :any= row.original
|
||||||
return <Button variant="outline" asChild size="sm" onClick={(e) => e.stopPropagation()}>
|
return <Button variant="outline" asChild size="sm">
|
||||||
<Link href={`/sales/vehicles/${item.vehicle?.id}`}>
|
<Link href={`/sales/vehicles/${item.vehicle?.id}`}>
|
||||||
<Car/> {getVehicleLabel(item.vehicle as any) || "—"}
|
<Car/> {getVehicleLabel(item.vehicle as any) || "—"}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { DashboardDetailsPage } from '@/base/components/layout/dashboard'
|
|||||||
import { getServerApi } from '@garage/api/server'
|
import { getServerApi } from '@garage/api/server'
|
||||||
import { InspectionActions } from '@/modules/inspections/inspection-actions'
|
import { InspectionActions } from '@/modules/inspections/inspection-actions'
|
||||||
import { InspectionProvider } from '@/modules/inspections/inspection-context'
|
import { InspectionProvider } from '@/modules/inspections/inspection-context'
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default async function layout(props: {
|
export default async function layout(props: {
|
||||||
@ -24,12 +23,7 @@ export default async function layout(props: {
|
|||||||
title={title}
|
title={title}
|
||||||
description={orderNumber ? `Order: ${orderNumber}` : undefined}
|
description={orderNumber ? `Order: ${orderNumber}` : undefined}
|
||||||
backHref="/sales/inspections"
|
backHref="/sales/inspections"
|
||||||
actions={
|
actions={<InspectionActions inspectionId={id} status={status} />}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ShareDocumentButton type="inspection" id={id} />
|
|
||||||
<InspectionActions inspectionId={id} status={status} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
href: `/sales/inspections/${id}`,
|
href: `/sales/inspections/${id}`,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { InvoiceProvider } from '@/modules/invoices/invoice-context'
|
|||||||
import { ReceiptIcon } from 'lucide-react'
|
import { ReceiptIcon } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import InvoiceStatusBadge from '@/modules/invoices/invoice-status-badge'
|
import InvoiceStatusBadge from '@/modules/invoices/invoice-status-badge'
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button'
|
|
||||||
|
|
||||||
export default async function InvoiceDetailLayout(props: { params: Promise<{ id: string }>, children: React.ReactNode }) {
|
export default async function InvoiceDetailLayout(props: { params: Promise<{ id: string }>, children: React.ReactNode }) {
|
||||||
const { id } = await props.params
|
const { id } = await props.params
|
||||||
@ -25,7 +24,6 @@ export default async function InvoiceDetailLayout(props: { params: Promise<{ id:
|
|||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
|
|
||||||
<InvoiceStatusBadge invoice={{id, status:data?.status}} />
|
<InvoiceStatusBadge invoice={{id, status:data?.status}} />
|
||||||
<ShareDocumentButton type="invoice" id={id} />
|
|
||||||
<InvoiceActions invoiceId={id} />
|
<InvoiceActions invoiceId={id} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
|
||||||
import { UserIcon } from "lucide-react"
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ResourcePage } from "@/shared/data-view/resource-page"
|
import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||||
import FormDialog from "@/shared/components/form-dialog"
|
import FormDialog from "@/shared/components/form-dialog"
|
||||||
import { InvoiceForm } from "@/modules/invoices/invoice-form"
|
import { InvoiceForm } from "@/modules/invoices/invoice-form"
|
||||||
import { Badge } from "@/shared/components/ui/badge"
|
import { Badge } from "@/shared/components/ui/badge"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
|
||||||
import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters"
|
import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters"
|
||||||
import { INVOICE_ROUTES } from "@garage/api"
|
import { INVOICE_ROUTES } from "@garage/api"
|
||||||
import type { InvoicesClient } from "@garage/api"
|
import type { InvoicesClient } from "@garage/api"
|
||||||
@ -26,7 +23,6 @@ type InvoiceItem = {
|
|||||||
total?: number | string
|
total?: number | string
|
||||||
balance_due?: number | string
|
balance_due?: number | string
|
||||||
customer?: {
|
customer?: {
|
||||||
id?: number | string
|
|
||||||
first_name?: string
|
first_name?: string
|
||||||
last_name?: string
|
last_name?: string
|
||||||
company_name?: string
|
company_name?: string
|
||||||
@ -138,27 +134,13 @@ export default function InvoicesPage() {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as unknown as InvoiceItem
|
const item = row.original as unknown as InvoiceItem
|
||||||
const customerLabel = getCustomerLabel(item)
|
const customerLabel = getCustomerLabel(item)
|
||||||
const phone = item.customer?.phone
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-[190px]">
|
<div className="min-w-[190px]">
|
||||||
{item.customer?.id ? (
|
<p className="font-medium leading-none">{customerLabel}</p>
|
||||||
<Button variant="outline" asChild size="sm" onClick={(e) => e.stopPropagation()}>
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
<Link href={`/sales/customers/${item.customer.id}`}>
|
{item.customer?.phone || item.customer?.company_name || "—"}
|
||||||
<UserIcon /> {customerLabel}
|
|
||||||
{phone && (
|
|
||||||
<span className="ms-1 text-muted-foreground">· {phone}</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<p className="font-medium leading-none">
|
|
||||||
{customerLabel}
|
|
||||||
{phone && (
|
|
||||||
<span className="ms-1 text-muted-foreground">· {phone}</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,8 +12,6 @@ import type { AppointmentsClient } from "@garage/api"
|
|||||||
import { CalendarCheck2Icon, ClockIcon } from "lucide-react"
|
import { CalendarCheck2Icon, ClockIcon } from "lucide-react"
|
||||||
import { Badge } from "@/shared/components/ui/badge"
|
import { Badge } from "@/shared/components/ui/badge"
|
||||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||||
import { getFullName } from "@/shared/utils/getFullName"
|
|
||||||
import { getVehicleLabel } from "@/modules/vehicles/utils/getVehicleLabel"
|
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
requested: "bg-yellow-100 text-yellow-800",
|
requested: "bg-yellow-100 text-yellow-800",
|
||||||
@ -44,28 +42,10 @@ export default function JobCardAppointmentsPage({
|
|||||||
router.replace(`${pathname}?${params.toString()}`)
|
router.replace(`${pathname}?${params.toString()}`)
|
||||||
}, [pathname, router, searchParams])
|
}, [pathname, router, searchParams])
|
||||||
|
|
||||||
const jc = jobCard as any
|
const defaultJobCard = jobCard
|
||||||
const defaultJobCard = jc
|
? { value: String((jobCard as any).id), label: (jobCard as any).label || (jobCard as any).title || `Job Card` }
|
||||||
? { value: String(jc.id), label: jc.label || jc.title || `Job Card` }
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const appointmentDefaults = jc
|
|
||||||
? {
|
|
||||||
job_card_id: jc.id,
|
|
||||||
job_card_title: jc.title || `Job Card #${jc.id}`,
|
|
||||||
customer_id: jc.customer?.id,
|
|
||||||
customer_name: getFullName(jc.customer),
|
|
||||||
vehicle_id: jc.vehicle?.id,
|
|
||||||
vehicle_name: getVehicleLabel(jc.vehicle),
|
|
||||||
service_writer_id: jc.service_writer?.id,
|
|
||||||
service_writer_name: getFullName(jc.service_writer),
|
|
||||||
technician_id: jc.primary_technician?.id,
|
|
||||||
technician_name: getFullName(jc.primary_technician),
|
|
||||||
department_id: jc.department?.id,
|
|
||||||
department_name: jc.department?.name,
|
|
||||||
}
|
|
||||||
: { job_card: defaultJobCard }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourcePage<AppointmentsClient>
|
<ResourcePage<AppointmentsClient>
|
||||||
routeKey={APPOINTMENT_ROUTES.INDEX}
|
routeKey={APPOINTMENT_ROUTES.INDEX}
|
||||||
@ -79,8 +59,8 @@ export default function JobCardAppointmentsPage({
|
|||||||
{(resourceId) => (
|
{(resourceId) => (
|
||||||
<AppointmentForm
|
<AppointmentForm
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
initialData={selectedItem ?? appointmentDefaults}
|
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||||
onSuccess={() => { closeDialog(); invalidateQuery(); router.refresh() }}
|
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useAuthApi } from "@/shared/useApi"
|
|||||||
import { confirm } from "@/shared/components/confirm-dialog"
|
import { confirm } from "@/shared/components/confirm-dialog"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||||
import { ApiError, JOB_CARD_ROUTES } from "@garage/api"
|
import { JOB_CARD_ROUTES } from "@garage/api"
|
||||||
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
||||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||||
import { CONSTANTS } from "@/config/constants"
|
import { CONSTANTS } from "@/config/constants"
|
||||||
@ -22,62 +22,6 @@ function getFileIcon(mimeType?: string) {
|
|||||||
return FileIcon
|
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() {
|
export default function JobCardAttachmentsPage() {
|
||||||
const { id: jobCardId } = useParams<{ id: string }>()
|
const { id: jobCardId } = useParams<{ id: string }>()
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
@ -122,19 +66,18 @@ export default function JobCardAttachmentsPage() {
|
|||||||
const files = e.target.files
|
const files = e.target.files
|
||||||
if (!files || files.length === 0) return
|
if (!files || files.length === 0) return
|
||||||
|
|
||||||
const fileList = Array.from(files)
|
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const loadingToast = toast.loading("Uploading attachment(s)...")
|
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)",
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.jobCards.addAttachment(jobCardId, fileList)
|
await promise
|
||||||
toast.dismiss(loadingToast)
|
|
||||||
toast.success("Attachment(s) uploaded successfully")
|
|
||||||
queryClient.invalidateQueries({ queryKey })
|
queryClient.invalidateQueries({ queryKey })
|
||||||
startRefreshTransition(() => router.refresh())
|
startRefreshTransition(() => router.refresh())
|
||||||
} catch (err) {
|
|
||||||
toast.dismiss(loadingToast)
|
|
||||||
showUploadError(err, fileList)
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
|
|||||||
@ -44,7 +44,6 @@ export default function JobCardExpenseItemsPage({
|
|||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: () => api.jobCards.getExpenseItems(jobCardId),
|
queryFn: () => api.jobCards.getExpenseItems(jobCardId),
|
||||||
staleTime: 30_000,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = (data as any)?.data ?? []
|
const rows = (data as any)?.data ?? []
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
|||||||
|
|
||||||
const title = jobCard?.title || 'Job Card Details'
|
const title = jobCard?.title || 'Job Card Details'
|
||||||
const status = jobCard?.status || 'draft'
|
const status = jobCard?.status || 'draft'
|
||||||
const attachmentsCount = jobCard?.attachment_files?.length ?? jobCard?.documents?.length ?? 0
|
const docs = jobCard?.documents
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JobCardProvider jobCard={{ ...jobCard }}>
|
<JobCardProvider jobCard={{ ...jobCard }}>
|
||||||
@ -55,7 +55,7 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
|||||||
// TODO: Needs refactor from API side then refactor in frontend
|
// TODO: Needs refactor from API side then refactor in frontend
|
||||||
{
|
{
|
||||||
href: `/sales/job-cards/${id}/attachments`,
|
href: `/sales/job-cards/${id}/attachments`,
|
||||||
label: `Attachments (${attachmentsCount})`
|
label: `Attachments (${docs?.length || 0})`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
|
import { getServerApi } from '@garage/api/server'
|
||||||
import { JobCardGeneralInfo } from '@/modules/job-cards/job-card-general-info'
|
import { JobCardGeneralInfo } from '@/modules/job-cards/job-card-general-info'
|
||||||
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
||||||
|
import type { JobCardShowData } from '@garage/api'
|
||||||
|
|
||||||
|
export default async function JobCardDetailPage(props: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await props.params
|
||||||
|
const api = await getServerApi()
|
||||||
|
const response = await api.jobCards.show(id)
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <div className="text-muted-foreground">Job card not found.</div>
|
||||||
|
}
|
||||||
|
|
||||||
export default async function JobCardDetailPage() {
|
|
||||||
return (
|
return (
|
||||||
<DashboardPage header={null}>
|
<DashboardPage header={null}>
|
||||||
<JobCardGeneralInfo />
|
<JobCardGeneralInfo jobCard={data} />
|
||||||
</DashboardPage>
|
</DashboardPage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,6 @@ export default function JobCardPartsPage({
|
|||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: () => api.jobCards.getParts(jobCardId),
|
queryFn: () => api.jobCards.getParts(jobCardId),
|
||||||
staleTime: 30_000,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = (data as any)?.data ?? []
|
const rows = (data as any)?.data ?? []
|
||||||
|
|||||||
@ -46,7 +46,6 @@ export default function JobCardServicesPage({
|
|||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: () => api.jobCards.getServices(jobCardId),
|
queryFn: () => api.jobCards.getServices(jobCardId),
|
||||||
staleTime: 30_000,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = (data as any)?.data ?? []
|
const rows = (data as any)?.data ?? []
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Geist_Mono, Inter } from "next/font/google"
|
import { Geist_Mono, Inter } from "next/font/google"
|
||||||
import NextTopLoader from "nextjs-toploader"
|
|
||||||
|
|
||||||
|
|
||||||
import { QueryProvider } from "@/shared/components/query-provider"
|
import { QueryProvider } from "@/shared/components/query-provider"
|
||||||
@ -34,7 +33,6 @@ export default function RootLayout({
|
|||||||
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable)}
|
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable)}
|
||||||
>
|
>
|
||||||
<body>
|
<body>
|
||||||
<NextTopLoader color="var(--primary)" height={3} showSpinner={false} />
|
|
||||||
<NuqsAdapter>
|
<NuqsAdapter>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<QueryProvider>{children}</QueryProvider>
|
<QueryProvider>{children}</QueryProvider>
|
||||||
|
|||||||
@ -109,7 +109,6 @@ export default function DashboardDetailsPageLayout({
|
|||||||
<Link
|
<Link
|
||||||
key={tab.href}
|
key={tab.href}
|
||||||
href={tab.href}
|
href={tab.href}
|
||||||
prefetch
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors",
|
"relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors",
|
||||||
"text-muted-foreground hover:text-foreground",
|
"text-muted-foreground hover:text-foreground",
|
||||||
|
|||||||
@ -11,11 +11,9 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { Ellipsis, Pencil, Trash2, Share2 } from "lucide-react"
|
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
||||||
import { useState } from "react"
|
|
||||||
import { useFormDialog } from "@/shared/components/form-dialog"
|
import { useFormDialog } from "@/shared/components/form-dialog"
|
||||||
import { BillForm } from "./bill-form"
|
import { BillForm } from "./bill-form"
|
||||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
|
||||||
|
|
||||||
type BillActionsProps = {
|
type BillActionsProps = {
|
||||||
billId: string
|
billId: string
|
||||||
@ -25,7 +23,6 @@ export function BillActions({ billId }: BillActionsProps) {
|
|||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const editDialog = useFormDialog("bill-details-edit")
|
const editDialog = useFormDialog("bill-details-edit")
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await api.bills.destroy(billId)
|
await api.bills.destroy(billId)
|
||||||
@ -45,10 +42,6 @@ export function BillActions({ billId }: BillActionsProps) {
|
|||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
@ -56,8 +49,6 @@ export function BillActions({ billId }: BillActionsProps) {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="bill" id={billId} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
|
|
||||||
<Dialog open={editDialog.isOpen} onOpenChange={(v) => { if (!v) editDialog.close() }}>
|
<Dialog open={editDialog.isOpen} onOpenChange={(v) => { if (!v) editDialog.close() }}>
|
||||||
<DialogContent className="min-w-xl lg:min-w-4xl">
|
<DialogContent className="min-w-xl lg:min-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { ClipboardList, Loader2 } from "lucide-react"
|
import { ClipboardList } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ApiError } from "@garage/api"
|
import { ApiError } from "@garage/api"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
@ -30,37 +30,34 @@ export function CreateJobCardFromEstimateButton() {
|
|||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
setIsConverting(true)
|
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 {
|
try {
|
||||||
const response = await promise
|
const response = await api.estimates.convertToJobCard(estimateId, {})
|
||||||
const jobCardId = response?.data?.id
|
const jobCardId = response?.data?.id
|
||||||
|
|
||||||
|
toast.success("Estimate converted to job card successfully")
|
||||||
|
|
||||||
if (jobCardId) {
|
if (jobCardId) {
|
||||||
router.push(`/sales/job-cards/${jobCardId}`)
|
router.push(`/sales/job-cards/${jobCardId}`)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError && error.status === 409) {
|
if (error instanceof ApiError && error.status === 409) {
|
||||||
const jobCardId = (error.payload?.data as { job_card_id?: number } | undefined)?.job_card_id
|
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) {
|
if (jobCardId) {
|
||||||
router.push(`/sales/job-cards/${jobCardId}`)
|
router.push(`/sales/job-cards/${jobCardId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
toast.error("Failed to convert estimate to job card")
|
||||||
|
} finally {
|
||||||
setIsConverting(false)
|
setIsConverting(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" size="sm" onClick={handleConvert} disabled={isConverting}>
|
<Button variant="outline" size="sm" onClick={handleConvert} disabled={isConverting}>
|
||||||
{isConverting ? <Loader2 className="me-2 size-4 animate-spin" /> : <ClipboardList className="me-2 size-4" />}
|
<ClipboardList className="me-2 size-4" />
|
||||||
{isConverting ? "Generating..." : "Generate Job Card"}
|
{isConverting ? "Generating..." : "Generate Job Card"}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shared/components/ui/select"
|
} from "@/shared/components/ui/select"
|
||||||
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X, Printer, Share2 } from "lucide-react"
|
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X, Printer } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
@ -34,7 +34,6 @@ import { DatePickerField, TimePickerField } from "@/shared/components/form"
|
|||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from "@/shared/lib/utils"
|
||||||
import { EmployeeCombobox, type EmployeeOption } from "../employees/employee-combobox"
|
import { EmployeeCombobox, type EmployeeOption } from "../employees/employee-combobox"
|
||||||
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
|
||||||
|
|
||||||
type EstimateActionsProps = {
|
type EstimateActionsProps = {
|
||||||
estimateId: string
|
estimateId: string
|
||||||
@ -136,7 +135,6 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
|||||||
const { print, isPrinting } = useDocumentPrint()
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const [editOpen, setEditOpen] = useState(false)
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
const [authOpen, setAuthOpen] = useState(false)
|
const [authOpen, setAuthOpen] = useState(false)
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
const [itemStatuses, setItemStatuses] = useState<Record<string, string>>({})
|
const [itemStatuses, setItemStatuses] = useState<Record<string, string>>({})
|
||||||
const [authMethod, setAuthMethod] = useState("in_person")
|
const [authMethod, setAuthMethod] = useState("in_person")
|
||||||
const [employee, setEmployee] = useState<EmployeeOption | null>(null)
|
const [employee, setEmployee] = useState<EmployeeOption | null>(null)
|
||||||
@ -226,10 +224,6 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
|||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
{isPrinting ? "Printing..." : "Print"}
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={openAuthDialog}>
|
<DropdownMenuItem onClick={openAuthDialog}>
|
||||||
<ShieldCheck className="size-4" />
|
<ShieldCheck className="size-4" />
|
||||||
Store Authorisation
|
Store Authorisation
|
||||||
@ -241,8 +235,6 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="estimate" id={estimateId} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
{/* Edit Dialog */}
|
||||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||||
<DialogContent className="min-w-xl">
|
<DialogContent className="min-w-xl">
|
||||||
|
|||||||
@ -63,48 +63,24 @@ const DEFAULT_VALUES: EstimateFormValues = {
|
|||||||
|
|
||||||
// ── Mapping helpers ──
|
// ── Mapping helpers ──
|
||||||
|
|
||||||
function customerLabel(c: any): string | undefined {
|
|
||||||
if (!c) return undefined
|
|
||||||
const name = [c.first_name, c.last_name].filter(Boolean).join(" ").trim()
|
|
||||||
return c.company_name || name || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function vehicleLabel(v: any): string | undefined {
|
|
||||||
if (!v) return undefined
|
|
||||||
const core = [v.make, v.model, v.sub_model].filter(Boolean).join(" ")
|
|
||||||
const suffix = [v.year, v.license_plate].filter(Boolean).join(" - ")
|
|
||||||
if (core && suffix) return `${core} (${suffix})`
|
|
||||||
return core || suffix || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToFormValues(data: unknown): EstimateFormValues {
|
function mapToFormValues(data: unknown): EstimateFormValues {
|
||||||
const d = (data as any)?.data ?? data ?? {}
|
const d = (data as any)?.data ?? data ?? {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: d.title || "",
|
title: d.title || "",
|
||||||
customer: toRelation(
|
customer: toRelation(d.customer_id, d.customer_name),
|
||||||
d.customer_id ?? d.customer?.id,
|
vehicle: toRelation(d.vehicle_id, d.vehicle_name),
|
||||||
d.customer_name ?? customerLabel(d.customer),
|
department: toRelation(d.department_id, d.department_name),
|
||||||
),
|
insurance_type: toRelation(d.insurance_type_id, d.insurance_type_title ?? d.insurance_type_name ?? d.insurance_type?.title),
|
||||||
vehicle: toRelation(
|
|
||||||
d.vehicle_id ?? d.vehicle?.id,
|
|
||||||
d.vehicle_name ?? vehicleLabel(d.vehicle),
|
|
||||||
),
|
|
||||||
department: toRelation(
|
|
||||||
d.department_id ?? d.department?.id,
|
|
||||||
d.department_name ?? d.department?.name ?? d.department?.title,
|
|
||||||
),
|
|
||||||
insurance_type: toRelation(
|
|
||||||
d.insurance_type_id ?? d.insurance_type?.id,
|
|
||||||
d.insurance_type_title ?? d.insurance_type_name ?? d.insurance_type?.title ?? d.insurance_type?.name,
|
|
||||||
),
|
|
||||||
insurer: toRelation(
|
insurer: toRelation(
|
||||||
d.insurer_id ?? d.insurer?.id,
|
d.insurer_id,
|
||||||
d.insurer_name ?? customerLabel(d.insurer),
|
d.insurer_name
|
||||||
|
?? [d.insurer?.first_name, d.insurer?.last_name].filter(Boolean).join(" ")
|
||||||
|
?? d.insurer?.company_name,
|
||||||
),
|
),
|
||||||
service_writer: toRelation(
|
service_writer: toRelation(
|
||||||
d.service_writer_id ?? d.service_writer?.id,
|
d.service_writer_id,
|
||||||
d.service_writer_name ?? customerLabel(d.service_writer),
|
d.service_writer_name ?? [d.service_writer?.first_name, d.service_writer?.last_name].filter(Boolean).join(" "),
|
||||||
),
|
),
|
||||||
estimate_number: d.estimate_number || "",
|
estimate_number: d.estimate_number || "",
|
||||||
date: d.date ? d.date.split("T")[0] : "",
|
date: d.date ? d.date.split("T")[0] : "",
|
||||||
@ -164,13 +140,12 @@ const DISCOUNT_OPTIONS = DiscountType.map((value) => ({
|
|||||||
export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFormProps) {
|
export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFormProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
|
|
||||||
const { form, isEditing, invalidate } = useResourceForm<EstimateFormValues, any>({
|
const { form, isEditing } = useResourceForm<EstimateFormValues, any>({
|
||||||
schema: estimateFormSchema,
|
schema: estimateFormSchema,
|
||||||
defaultValues: DEFAULT_VALUES,
|
defaultValues: DEFAULT_VALUES,
|
||||||
resourceId,
|
resourceId,
|
||||||
initialData,
|
initialData,
|
||||||
queryKey: [ESTIMATE_ROUTES.BY_ID, resourceId],
|
queryKey: [ESTIMATE_ROUTES.BY_ID, resourceId],
|
||||||
initialize: (id) => api.estimates.show(id),
|
|
||||||
mapToFormValues,
|
mapToFormValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -188,7 +163,7 @@ export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFor
|
|||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (!isEditing) form.reset()
|
form.reset()
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -27,9 +27,9 @@ import {
|
|||||||
INVENTORY_CATEGORY_ROUTES,
|
INVENTORY_CATEGORY_ROUTES,
|
||||||
INVENTORY_ROUTES,
|
INVENTORY_ROUTES,
|
||||||
DEPARTMENT_ROUTES,
|
DEPARTMENT_ROUTES,
|
||||||
|
VENDOR_ROUTES,
|
||||||
} from "@garage/api"
|
} from "@garage/api"
|
||||||
import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog"
|
import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog"
|
||||||
import { RhfVendorSelectField } from "@/modules/vendors/rhf-vendor-select-field"
|
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
|
|
||||||
@ -84,26 +84,15 @@ function mapToFormValues(data: unknown): ExpenseItemFormValues {
|
|||||||
sku: d.sku || "",
|
sku: d.sku || "",
|
||||||
item_code: d.item_code || "",
|
item_code: d.item_code || "",
|
||||||
description: d.description || "",
|
description: d.description || "",
|
||||||
category: toRelation(
|
category: toRelation(d.category_id, d.category_title ?? d.category_name),
|
||||||
d.category_id ?? d.category?.id,
|
unit_type: toRelation(d.unit_type_id, d.unit_type_title ?? d.unit_type_name),
|
||||||
d.category_title ?? d.category_name ?? d.category?.title ?? d.category?.name,
|
department: toRelation(d.department_id, d.department_name ?? d.department_title),
|
||||||
),
|
|
||||||
unit_type: toRelation(
|
|
||||||
d.unit_type_id ?? d.unit_type?.id,
|
|
||||||
d.unit_type_title ?? d.unit_type_name ?? d.unit_type?.title ?? d.unit_type?.name,
|
|
||||||
),
|
|
||||||
department: toRelation(
|
|
||||||
d.department_id ?? d.department?.id,
|
|
||||||
d.department_name ?? d.department_title ?? d.department?.name ?? d.department?.title,
|
|
||||||
),
|
|
||||||
purchase_information: d.purchase_information ?? true,
|
purchase_information: d.purchase_information ?? true,
|
||||||
purchase_price: d.purchase_price ?? undefined,
|
purchase_price: d.purchase_price ?? undefined,
|
||||||
purchase_chart_of_account: d.purchase_chart_of_account || "",
|
purchase_chart_of_account: d.purchase_chart_of_account || "",
|
||||||
purchase_preferred_vendor: toRelation(
|
purchase_preferred_vendor: toRelation(
|
||||||
d.purchase_preferred_vendor_id ?? d.purchase_preferred_vendor?.id,
|
d.purchase_preferred_vendor_id,
|
||||||
d.purchase_preferred_vendor_name
|
d.purchase_preferred_vendor_name,
|
||||||
?? d.purchase_preferred_vendor?.company_name
|
|
||||||
?? [d.purchase_preferred_vendor?.first_name, d.purchase_preferred_vendor?.last_name].filter(Boolean).join(" "),
|
|
||||||
),
|
),
|
||||||
sales_information: d.sales_information ?? false,
|
sales_information: d.sales_information ?? false,
|
||||||
selling_price: d.selling_price ?? undefined,
|
selling_price: d.selling_price ?? undefined,
|
||||||
@ -138,7 +127,7 @@ function mapFormToPayload(values: ExpenseItemFormValues) {
|
|||||||
export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseItemFormProps) {
|
export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseItemFormProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
|
|
||||||
const { form, isEditing, invalidate } = useResourceForm<ExpenseItemFormValues, any>({
|
const { form, isEditing } = useResourceForm<ExpenseItemFormValues, any>({
|
||||||
schema: expenseItemFormSchema,
|
schema: expenseItemFormSchema,
|
||||||
defaultValues: DEFAULT_VALUES,
|
defaultValues: DEFAULT_VALUES,
|
||||||
resourceId,
|
resourceId,
|
||||||
@ -161,7 +150,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
|
|||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (!isEditing) form.reset()
|
form.reset()
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -258,7 +247,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Purchase Information */}
|
{/* Purchase Information */}
|
||||||
<RhfCheckboxField
|
{/* <RhfCheckboxField
|
||||||
name="purchase_information"
|
name="purchase_information"
|
||||||
label="Purchase Information"
|
label="Purchase Information"
|
||||||
/>
|
/>
|
||||||
@ -270,21 +259,22 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
|
|||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
{/* TODO(phase-2): wire Purchase Chart of Account to the chart-of-accounts module (currently disabled, marked "Coming soon"). */}
|
|
||||||
<RhfTextField
|
<RhfTextField
|
||||||
name="purchase_chart_of_account"
|
name="purchase_chart_of_account"
|
||||||
label="Purchase Chart of Account"
|
label="Purchase Chart of Account"
|
||||||
placeholder="e.g. Expenses"
|
placeholder="e.g. Expenses"
|
||||||
description="Coming soon"
|
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RhfVendorSelectField
|
<RhfAsyncSelectField
|
||||||
name="purchase_preferred_vendor"
|
name="purchase_preferred_vendor"
|
||||||
label="Preferred Vendor"
|
label="Preferred Vendor"
|
||||||
placeholder="Select vendor"
|
placeholder="Select vendor"
|
||||||
/>
|
queryKey={[VENDOR_ROUTES.INDEX]}
|
||||||
|
listFn={() => api.vendors.list()}
|
||||||
|
mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })}
|
||||||
|
{...STORE_OBJECT}
|
||||||
|
/> */}
|
||||||
|
|
||||||
{/* Sales Information */}
|
{/* Sales Information */}
|
||||||
{/* <RhfCheckboxField
|
{/* <RhfCheckboxField
|
||||||
|
|||||||
@ -12,13 +12,11 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { Ellipsis, Pencil, Trash2, Play, CheckCircle2, Printer, Share2 } from "lucide-react"
|
import { Ellipsis, Pencil, Trash2, Play, CheckCircle2, Printer } from "lucide-react"
|
||||||
import { useState } from "react"
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useFormDialog } from "@/shared/components/form-dialog"
|
import { useFormDialog } from "@/shared/components/form-dialog"
|
||||||
import { InspectionForm } from "./inspection-form"
|
import { InspectionForm } from "./inspection-form"
|
||||||
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
|
||||||
|
|
||||||
type InspectionActionsProps = {
|
type InspectionActionsProps = {
|
||||||
inspectionId: string
|
inspectionId: string
|
||||||
@ -36,7 +34,6 @@ export function InspectionActions({ inspectionId, status, onStatusChange }: Insp
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const editDialog = useFormDialog("inspection-details-edit")
|
const editDialog = useFormDialog("inspection-details-edit")
|
||||||
const { print, isPrinting } = useDocumentPrint()
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const promise = api.inspections.destroy(inspectionId)
|
const promise = api.inspections.destroy(inspectionId)
|
||||||
@ -82,10 +79,6 @@ export function InspectionActions({ inspectionId, status, onStatusChange }: Insp
|
|||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
{isPrinting ? "Printing..." : "Print"}
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{transition && (
|
{transition && (
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange(transition.next)}>
|
<DropdownMenuItem onClick={() => handleStatusChange(transition.next)}>
|
||||||
<transition.icon className="size-4" />
|
<transition.icon className="size-4" />
|
||||||
@ -100,8 +93,6 @@ export function InspectionActions({ inspectionId, status, onStatusChange }: Insp
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="inspection" id={inspectionId} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
|
|
||||||
<Dialog open={editDialog.isOpen} onOpenChange={(v) => { if (!v) editDialog.close() }}>
|
<Dialog open={editDialog.isOpen} onOpenChange={(v) => { if (!v) editDialog.close() }}>
|
||||||
<DialogContent className="min-w-xl lg:min-w-4xl">
|
<DialogContent className="min-w-xl lg:min-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
@ -10,9 +9,8 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { Ellipsis, Printer, Share2, Trash2 } from "lucide-react"
|
import { Ellipsis, Printer, Trash2 } from "lucide-react"
|
||||||
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
|
||||||
|
|
||||||
type InvoiceActionsProps = {
|
type InvoiceActionsProps = {
|
||||||
invoiceId: string
|
invoiceId: string
|
||||||
@ -22,7 +20,6 @@ export function InvoiceActions({ invoiceId }: InvoiceActionsProps) {
|
|||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { print, isPrinting } = useDocumentPrint()
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await api.invoices.destroy(invoiceId)
|
await api.invoices.destroy(invoiceId)
|
||||||
@ -42,18 +39,12 @@ export function InvoiceActions({ invoiceId }: InvoiceActionsProps) {
|
|||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
{isPrinting ? "Printing..." : "Print"}
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="invoice" id={invoiceId} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -251,13 +251,12 @@ function TransactionDiscountField() {
|
|||||||
export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormProps) {
|
export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
|
|
||||||
const { form, isEditing, invalidate } = useResourceForm<InvoiceFormValues, any>({
|
const { form, isEditing } = useResourceForm<InvoiceFormValues, any>({
|
||||||
schema: invoiceFormSchema,
|
schema: invoiceFormSchema,
|
||||||
defaultValues: DEFAULT_VALUES,
|
defaultValues: DEFAULT_VALUES,
|
||||||
resourceId,
|
resourceId,
|
||||||
initialData,
|
initialData,
|
||||||
queryKey: [INVOICE_ROUTES.BY_ID, resourceId],
|
queryKey: [INVOICE_ROUTES.BY_ID, resourceId],
|
||||||
initialize: (id) => api.invoices.show(id),
|
|
||||||
mapToFormValues,
|
mapToFormValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -278,7 +277,7 @@ export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormP
|
|||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (!isEditing) form.reset()
|
form.reset()
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,13 +5,11 @@ import { confirm } from '@/shared/components/confirm-dialog';
|
|||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/shared/components/ui/dropdown-menu'
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/shared/components/ui/dropdown-menu'
|
||||||
import { Button } from '@/shared/components/ui/button'
|
import { Button } from '@/shared/components/ui/button'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { CalendarPlus, Ellipsis, FileText, Loader2, Pencil, Printer, Share2, Trash2 } from 'lucide-react';
|
import { CalendarPlus, Ellipsis, FileText, Pencil, Printer, Trash2 } from 'lucide-react';
|
||||||
import { useDocumentPrint } from '@/shared/hooks/use-document-print';
|
import { useDocumentPrint } from '@/shared/hooks/use-document-print';
|
||||||
import { useJobCard } from './job-card-context';
|
import { useJobCard } from './job-card-context';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuthApi } from '@/shared/useApi';
|
import { useAuthApi } from '@/shared/useApi';
|
||||||
import { ShareDocumentDialog } from '@/shared/components/share-document-dialog';
|
|
||||||
import { ShareDocumentButton } from '@/shared/components/share-document-button';
|
|
||||||
|
|
||||||
// TODO: setting a sales person not working
|
// TODO: setting a sales person not working
|
||||||
// TODO: unable to set a Primary technician for the job card. Need to investigate and fix it.
|
// TODO: unable to set a Primary technician for the job card. Need to investigate and fix it.
|
||||||
@ -23,7 +21,6 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
const { print, isPrinting } = useDocumentPrint()
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const jobCard = useJobCard()
|
const jobCard = useJobCard()
|
||||||
const [isConverting, setIsConverting] = useState(false)
|
const [isConverting, setIsConverting] = useState(false)
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
router.push(`/sales/job-cards/${id}/edit`)
|
router.push(`/sales/job-cards/${id}/edit`)
|
||||||
@ -46,33 +43,25 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
setIsConverting(true)
|
setIsConverting(true)
|
||||||
const promise = api.jobCards.convertToInvoice(id, {}) as Promise<any>
|
|
||||||
toast.promise(promise, {
|
|
||||||
loading: "Converting job card to invoice...",
|
|
||||||
success: "Job card converted to invoice successfully",
|
|
||||||
error: (err: any) => {
|
|
||||||
const conflictId = err?.response?.data?.data?.invoice_id ?? err?.data?.data?.invoice_id
|
|
||||||
return conflictId
|
|
||||||
? "An invoice already exists for this job card."
|
|
||||||
: (err?.message || "Failed to convert job card to invoice")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
try {
|
try {
|
||||||
const res = await promise
|
const res = await api.jobCards.convertToInvoice(id, {}) as any
|
||||||
const invoiceId = res?.data?.id
|
const invoiceId = res?.data?.id
|
||||||
|
toast.success("Job card converted to invoice successfully")
|
||||||
if (invoiceId) {
|
if (invoiceId) {
|
||||||
router.push(`/sales/invoice/${invoiceId}`)
|
router.push(`/sales/invoice/${invoiceId}`)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const conflictId = err?.response?.data?.data?.invoice_id ?? err?.data?.data?.invoice_id
|
const conflictId = err?.response?.data?.data?.invoice_id ?? err?.data?.data?.invoice_id
|
||||||
if (conflictId) {
|
if (conflictId) {
|
||||||
|
toast.info("An invoice already exists for this job card.")
|
||||||
router.push(`/sales/invoice/${conflictId}`)
|
router.push(`/sales/invoice/${conflictId}`)
|
||||||
return
|
} else {
|
||||||
}
|
toast.error(err?.message || "Failed to convert job card to invoice")
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
setIsConverting(false)
|
setIsConverting(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const confirmed = await confirm({
|
const confirmed = await confirm({
|
||||||
@ -96,7 +85,7 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{jobCard?.status !== "draft" && (
|
{jobCard?.status !== "draft" && (
|
||||||
<Button variant="outline" onClick={handleConvertToInvoice} disabled={isConverting}>
|
<Button variant="outline" onClick={handleConvertToInvoice} disabled={isConverting}>
|
||||||
{isConverting ? <Loader2 className="size-4 animate-spin" /> : <FileText className="size-4" />}
|
<FileText className="size-4" />
|
||||||
{isConverting ? "Converting..." : "Convert to Invoice"}
|
{isConverting ? "Converting..." : "Convert to Invoice"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -106,8 +95,6 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
Create Appointment
|
Create Appointment
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ShareDocumentButton type="job_card" id={id} />
|
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon" className="self-stretch h-auto aspect-square">
|
<Button variant="outline" size="icon" className="self-stretch h-auto aspect-square">
|
||||||
@ -123,10 +110,6 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
{isPrinting ? "Printing..." : "Print"}
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleCreateAppointment}>
|
<DropdownMenuItem onClick={handleCreateAppointment}>
|
||||||
<CalendarPlus className="size-4" />
|
<CalendarPlus className="size-4" />
|
||||||
@ -145,8 +128,6 @@ export default function JobCardDropdown({ id }: { id: string }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="job_card" id={id} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -231,13 +231,12 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const [isCheckInDialogOpen, setIsCheckInDialogOpen] = useState(false)
|
const [isCheckInDialogOpen, setIsCheckInDialogOpen] = useState(false)
|
||||||
|
|
||||||
const { form, isEditing, invalidate } = useResourceForm<JobCardFormValues, any>({
|
const { form, isEditing } = useResourceForm<JobCardFormValues, any>({
|
||||||
schema: jobCardFormSchema,
|
schema: jobCardFormSchema,
|
||||||
defaultValues: DEFAULT_VALUES,
|
defaultValues: DEFAULT_VALUES,
|
||||||
resourceId,
|
resourceId,
|
||||||
initialData,
|
initialData,
|
||||||
queryKey: [JOB_CARD_ROUTES.BY_ID, resourceId],
|
queryKey: [JOB_CARD_ROUTES.BY_ID, resourceId],
|
||||||
initialize: (id) => api.jobCards.show(id),
|
|
||||||
mapToFormValues,
|
mapToFormValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -261,7 +260,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsCheckInDialogOpen(false)
|
setIsCheckInDialogOpen(false)
|
||||||
if (!isEditing) form.reset()
|
form.reset()
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Calendar,
|
Calendar,
|
||||||
@ -32,7 +30,6 @@ import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/pa
|
|||||||
import JobCardPaymentsReceived from "./job-card-payments-received"
|
import JobCardPaymentsReceived from "./job-card-payments-received"
|
||||||
import { formatDate } from "@/shared/utils/formatters"
|
import { formatDate } from "@/shared/utils/formatters"
|
||||||
import type { JobCardShowData } from "@garage/api"
|
import type { JobCardShowData } from "@garage/api"
|
||||||
import { useJobCard } from "./job-card-context"
|
|
||||||
|
|
||||||
type JobCard = JobCardShowData
|
type JobCard = JobCardShowData
|
||||||
|
|
||||||
@ -70,14 +67,7 @@ const statusColorMap: Record<string, string> = {
|
|||||||
cancelled: "destructive",
|
cancelled: "destructive",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JobCardGeneralInfo({ jobCard: jobCardProp }: { jobCard?: JobCard } = {}) {
|
export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) {
|
||||||
const jobCardFromContext = useJobCard()
|
|
||||||
const jobCard = (jobCardProp ?? jobCardFromContext) as JobCard
|
|
||||||
|
|
||||||
if (!jobCard) {
|
|
||||||
return <div className="text-muted-foreground">Job card not found.</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatStatus = (status?: string) => {
|
const formatStatus = (status?: string) => {
|
||||||
if (!status) return null
|
if (!status) return null
|
||||||
return status
|
return status
|
||||||
|
|||||||
@ -17,20 +17,12 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/shared/co
|
|||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import { useJobCard } from "./job-card-context"
|
import { useJobCard } from "./job-card-context"
|
||||||
import { formatDate, formatCurrency } from "@/shared/utils/formatters"
|
import { formatDate, formatCurrency } from "@/shared/utils/formatters"
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
export default function JobCardPaymentsReceived() {
|
export default function JobCardPaymentsReceived() {
|
||||||
const jobCard = useJobCard()
|
const jobCard = useJobCard()
|
||||||
const [hasOpened, setHasOpened] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible defaultOpen={false} className="group/collapsible">
|
||||||
defaultOpen={false}
|
|
||||||
className="group/collapsible"
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (open) setHasOpened(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -44,7 +36,6 @@ export default function JobCardPaymentsReceived() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
{hasOpened && (
|
|
||||||
<CrudResource<PaymentReceivedClient>
|
<CrudResource<PaymentReceivedClient>
|
||||||
extraParams={{ job_card_id: jobCard?.id }}
|
extraParams={{ job_card_id: jobCard?.id }}
|
||||||
routeKey={PAYMENT_RECEIVED_ROUTES.INDEX}
|
routeKey={PAYMENT_RECEIVED_ROUTES.INDEX}
|
||||||
@ -57,8 +48,6 @@ export default function JobCardPaymentsReceived() {
|
|||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
defaultJobCard={{ id: jobCard?.id, title: jobCard?.title }}
|
defaultJobCard={{ id: jobCard?.id, title: jobCard?.title }}
|
||||||
invoiceCustomer={jobCard?.customer as any}
|
invoiceCustomer={jobCard?.customer as any}
|
||||||
lockJobCard
|
|
||||||
lockCustomer
|
|
||||||
onSuccess={invalidateQuery}
|
onSuccess={invalidateQuery}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -138,7 +127,6 @@ export default function JobCardPaymentsReceived() {
|
|||||||
actionsColumn(),
|
actionsColumn(),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import { AlertTriangle, Plus, Save } from "lucide-react"
|
|||||||
|
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
|
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
|
||||||
import { Field, FieldGroup, FieldLabel } from "@/shared/components/ui/field"
|
import { FieldGroup } from "@/shared/components/ui/field"
|
||||||
import { Input } from "@/shared/components/ui/input"
|
|
||||||
import {
|
import {
|
||||||
Rhform,
|
Rhform,
|
||||||
RhfTextField,
|
RhfTextField,
|
||||||
@ -37,8 +36,6 @@ export type PaymentReceivedFormProps = {
|
|||||||
invoiceId?: string | null
|
invoiceId?: string | null
|
||||||
invoiceCustomer?: { id?: number | null; first_name?: string | null; last_name?: string | null } | null
|
invoiceCustomer?: { id?: number | null; first_name?: string | null; last_name?: string | null } | null
|
||||||
invoiceAmount?: number | string | null
|
invoiceAmount?: number | string | null
|
||||||
lockJobCard?: boolean
|
|
||||||
lockCustomer?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Default values ──
|
// ── Default values ──
|
||||||
@ -93,33 +90,27 @@ const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) =
|
|||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard, invoiceId, invoiceCustomer, invoiceAmount, lockJobCard, lockCustomer }: PaymentReceivedFormProps) {
|
export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard, invoiceId, invoiceCustomer, invoiceAmount }: PaymentReceivedFormProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
|
|
||||||
const isJobCardLocked = !resourceId && (lockJobCard ?? !!defaultJobCard?.id)
|
|
||||||
const isCustomerLocked = !resourceId && (lockCustomer ?? !!invoiceCustomer?.id)
|
|
||||||
|
|
||||||
const customerLabel = invoiceCustomer?.first_name
|
|
||||||
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
|
|
||||||
: (invoiceCustomer as any)?.company_name || (invoiceCustomer as any)?.name || ""
|
|
||||||
|
|
||||||
const resolvedInitialData = useMemo(() => {
|
const resolvedInitialData = useMemo(() => {
|
||||||
const base: any = { ...(initialData as any) }
|
const base: any = { ...(initialData as any) }
|
||||||
if (!resourceId) {
|
if (!resourceId) {
|
||||||
if (defaultJobCard?.id != null) {
|
if (defaultJobCard?.id != null) {
|
||||||
base.job_card_id = defaultJobCard.id
|
base.job_card = toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined)
|
||||||
base.job_card_name = defaultJobCard.title ?? undefined
|
|
||||||
}
|
}
|
||||||
if (invoiceCustomer?.id != null) {
|
if (invoiceCustomer?.id != null) {
|
||||||
base.customer_id = invoiceCustomer.id
|
const customerLabel = invoiceCustomer.first_name
|
||||||
base.customer_name = customerLabel
|
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
|
||||||
|
: (invoiceCustomer as any).company_name || (invoiceCustomer as any).name || undefined
|
||||||
|
base.customer = toRelation(invoiceCustomer.id, customerLabel)
|
||||||
}
|
}
|
||||||
if (invoiceAmount != null && invoiceAmount !== "") {
|
if (invoiceAmount != null && invoiceAmount !== "") {
|
||||||
base.amount_received = Number(invoiceAmount)
|
base.amount_received = Number(invoiceAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(base).length ? base : initialData
|
return Object.keys(base).length ? base : initialData
|
||||||
}, [resourceId, defaultJobCard, initialData, invoiceCustomer, invoiceAmount, customerLabel])
|
}, [resourceId, defaultJobCard, initialData, invoiceCustomer, invoiceAmount])
|
||||||
|
|
||||||
const { form, isEditing } = useResourceForm<PaymentReceivedFormValues, any>({
|
const { form, isEditing } = useResourceForm<PaymentReceivedFormValues, any>({
|
||||||
schema: paymentReceivedFormSchema,
|
schema: paymentReceivedFormSchema,
|
||||||
@ -162,14 +153,6 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
|
|||||||
|
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
{isCustomerLocked ? (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>
|
|
||||||
Customer<span className="text-destructive ms-0.5">*</span>
|
|
||||||
</FieldLabel>
|
|
||||||
<Input value={customerLabel} readOnly disabled />
|
|
||||||
</Field>
|
|
||||||
) : (
|
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="customer"
|
name="customer"
|
||||||
label="Customer"
|
label="Customer"
|
||||||
@ -185,13 +168,6 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
|
|||||||
})}
|
})}
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isJobCardLocked ? (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>Job Card</FieldLabel>
|
|
||||||
<Input value={defaultJobCard?.title ?? ""} readOnly disabled />
|
|
||||||
</Field>
|
|
||||||
) : (
|
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="job_card"
|
name="job_card"
|
||||||
label="Job Card"
|
label="Job Card"
|
||||||
@ -204,7 +180,6 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
|
|||||||
})}
|
})}
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
@ -11,10 +10,9 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { confirm } from "@/shared/components/confirm-dialog"
|
import { confirm } from "@/shared/components/confirm-dialog"
|
||||||
import { Ellipsis, Printer, Share2, Trash2 } from "lucide-react"
|
import { Ellipsis, Printer, Trash2 } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
import { ShareDocumentDialog } from "@/shared/components/share-document-dialog"
|
|
||||||
|
|
||||||
type PurchaseOrderActionsProps = {
|
type PurchaseOrderActionsProps = {
|
||||||
purchaseOrderId: string
|
purchaseOrderId: string
|
||||||
@ -24,7 +22,6 @@ export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsPr
|
|||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { print, isPrinting } = useDocumentPrint()
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const confirmed = await confirm({
|
const confirmed = await confirm({
|
||||||
@ -46,7 +43,6 @@ export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
@ -58,18 +54,11 @@ export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsPr
|
|||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
{isPrinting ? "Printing..." : "Print"}
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setShareOpen(true)}>
|
|
||||||
<Share2 className="size-4" />
|
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<ShareDocumentDialog type="purchase_order" id={purchaseOrderId} open={shareOpen} onOpenChange={setShareOpen} />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,6 @@
|
|||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
|
||||||
"nuqs": "^2.8.9",
|
"nuqs": "^2.8.9",
|
||||||
"object-to-formdata": "^4.5.1",
|
"object-to-formdata": "^4.5.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
"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} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
"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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { confirm } from "@/shared/components/confirm-dialog"
|
import { confirm } from "@/shared/components/confirm-dialog"
|
||||||
@ -42,10 +42,6 @@ export function useResourcePage<TClient extends ResourcePageClient>({
|
|||||||
const { open: openDialog, close: closeDialog, isOpen, resourceId } = useFormDialog(paramKey)
|
const { open: openDialog, close: closeDialog, isOpen, resourceId } = useFormDialog(paramKey)
|
||||||
const [selectedItem, setSelectedItem] = useState<TItem | null>(null)
|
const [selectedItem, setSelectedItem] = useState<TItem | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!resourceId) setSelectedItem(null)
|
|
||||||
}, [resourceId])
|
|
||||||
|
|
||||||
const tableQuery = useDataTableQuery({
|
const tableQuery = useDataTableQuery({
|
||||||
queryKey: [routeKey],
|
queryKey: [routeKey],
|
||||||
client,
|
client,
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
"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 }
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useForm, type DefaultValues, type FieldValues, type UseFormReturn } from "react-hook-form"
|
import { useForm, type DefaultValues, type FieldValues, type UseFormReturn } from "react-hook-form"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useQuery, useQueryClient, type QueryKey } from "@tanstack/react-query"
|
import { useQuery, type QueryKey } from "@tanstack/react-query"
|
||||||
import type { ZodType } from "zod"
|
import type { ZodType } from "zod"
|
||||||
|
|
||||||
type UseResourceFormOptions<TFormValues extends FieldValues, TApiData = unknown> = {
|
type UseResourceFormOptions<TFormValues extends FieldValues, TApiData = unknown> = {
|
||||||
@ -20,7 +20,6 @@ type UseResourceFormReturn<TFormValues extends FieldValues> = {
|
|||||||
form: UseFormReturn<TFormValues>
|
form: UseFormReturn<TFormValues>
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isInitializing: boolean
|
isInitializing: boolean
|
||||||
invalidate: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useResourceForm<TFormValues extends FieldValues, TApiData = unknown>({
|
export function useResourceForm<TFormValues extends FieldValues, TApiData = unknown>({
|
||||||
@ -33,15 +32,11 @@ export function useResourceForm<TFormValues extends FieldValues, TApiData = unkn
|
|||||||
queryKey,
|
queryKey,
|
||||||
}: UseResourceFormOptions<TFormValues, TApiData>): UseResourceFormReturn<TFormValues> {
|
}: UseResourceFormOptions<TFormValues, TApiData>): UseResourceFormReturn<TFormValues> {
|
||||||
const isEditing = !!resourceId
|
const isEditing = !!resourceId
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const resolvedQueryKey = queryKey ?? ["resource", resourceId]
|
|
||||||
|
|
||||||
const { data: queriedData, isLoading: isQueryLoading } = useQuery<TApiData>({
|
const { data: queriedData, isLoading: isQueryLoading } = useQuery<TApiData>({
|
||||||
queryKey: resolvedQueryKey,
|
queryKey: queryKey ?? ["resource", resourceId],
|
||||||
queryFn: () => initialize!(resourceId!),
|
queryFn: () => initialize!(resourceId!),
|
||||||
enabled: isEditing && !!initialize,
|
enabled: isEditing && !!initialize,
|
||||||
staleTime: 0,
|
|
||||||
refetchOnMount: "always",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolvedData = queriedData ?? (isEditing ? initialData : undefined)
|
const resolvedData = queriedData ?? (isEditing ? initialData : undefined)
|
||||||
@ -54,7 +49,7 @@ export function useResourceForm<TFormValues extends FieldValues, TApiData = unkn
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
form.reset({ ...defaultValues, ...mapToFormValues(initialData) } as any)
|
form.reset({ ...defaultValues, ...initialData } as any)
|
||||||
} else {
|
} else {
|
||||||
form.reset(defaultValues)
|
form.reset(defaultValues)
|
||||||
}
|
}
|
||||||
@ -66,9 +61,5 @@ export function useResourceForm<TFormValues extends FieldValues, TApiData = unkn
|
|||||||
}
|
}
|
||||||
}, [isEditing, resolvedData, initialData]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [isEditing, resolvedData, initialData]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const invalidate = () => {
|
return { form, isEditing, isInitializing: isEditing && !!initialize && isQueryLoading }
|
||||||
queryClient.invalidateQueries({ queryKey: resolvedQueryKey })
|
|
||||||
}
|
|
||||||
|
|
||||||
return { form, isEditing, isInitializing: isEditing && !!initialize && isQueryLoading, invalidate }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,6 @@ import { AutoGenerateClient } from "./clients/auto-generate"
|
|||||||
import { ExpenseItemsClient } from "./clients/expense-items"
|
import { ExpenseItemsClient } from "./clients/expense-items"
|
||||||
import { InventoryCategoriesClient } from "./clients/inventory-categories"
|
import { InventoryCategoriesClient } from "./clients/inventory-categories"
|
||||||
import { DocumentPrintClient } from "./clients/document-print"
|
import { DocumentPrintClient } from "./clients/document-print"
|
||||||
import { DocumentShareClient } from "./clients/document-share"
|
|
||||||
|
|
||||||
export function createApi(options?: ApiClientOptions) {
|
export function createApi(options?: ApiClientOptions) {
|
||||||
return {
|
return {
|
||||||
@ -119,7 +118,6 @@ export function createApi(options?: ApiClientOptions) {
|
|||||||
expenseItems: new ExpenseItemsClient(undefined, options),
|
expenseItems: new ExpenseItemsClient(undefined, options),
|
||||||
inventoryCategories: new InventoryCategoriesClient(undefined, options),
|
inventoryCategories: new InventoryCategoriesClient(undefined, options),
|
||||||
documentPrint: new DocumentPrintClient(undefined, options),
|
documentPrint: new DocumentPrintClient(undefined, options),
|
||||||
documentShare: new DocumentShareClient(undefined, options),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { ApiClient, ApiError, type ApiClientOptions } from "../infra/client"
|
|
||||||
import type { ApiPath } from "../infra/types"
|
|
||||||
|
|
||||||
export const DOCUMENT_SHARE_ROUTES = {
|
|
||||||
STORE: "/api/documents/share",
|
|
||||||
} as const satisfies Record<string, ApiPath>
|
|
||||||
|
|
||||||
export type DocumentShareType =
|
|
||||||
| "inspection"
|
|
||||||
| "estimate"
|
|
||||||
| "job_card"
|
|
||||||
| "invoice"
|
|
||||||
| "payment_received"
|
|
||||||
| "purchase_order"
|
|
||||||
| "bill"
|
|
||||||
| string
|
|
||||||
|
|
||||||
export type DocumentShareChannel = "email" | "link"
|
|
||||||
|
|
||||||
export interface DocumentSharePayload {
|
|
||||||
document_type: DocumentShareType
|
|
||||||
document_id: number | string
|
|
||||||
channel: DocumentShareChannel
|
|
||||||
email?: string
|
|
||||||
phone?: string
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentShareResponse {
|
|
||||||
share_url: string
|
|
||||||
whatsapp_url: string
|
|
||||||
expires_at: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DocumentShareClient extends ApiClient {
|
|
||||||
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
|
|
||||||
super(baseUrl, defaultOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
async share(payload: DocumentSharePayload): Promise<DocumentShareResponse> {
|
|
||||||
const endpoint = DOCUMENT_SHARE_ROUTES.STORE
|
|
||||||
const url = `${this.baseUrl.replace(/\/+$/, "")}${endpoint}`
|
|
||||||
const headers = new Headers(this.defaultOptions.headers as Record<string, string>)
|
|
||||||
headers.set("Accept", "application/json")
|
|
||||||
headers.set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
const body = { ...payload, document_id: Number(payload.document_id) }
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
const text = await response.text()
|
|
||||||
const data = text ? JSON.parse(text) : null
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new ApiError(response.status, response.statusText, endpoint, "post", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as DocumentShareResponse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -46,14 +46,6 @@ export {
|
|||||||
export { BillsClient, BILL_ROUTES } from "./bills"
|
export { BillsClient, BILL_ROUTES } from "./bills"
|
||||||
export { ReasonsClient, REASON_ROUTES } from "./reasons"
|
export { ReasonsClient, REASON_ROUTES } from "./reasons"
|
||||||
export { DocumentPrintClient, DOCUMENT_PRINT_ROUTES, type DocumentPrintType, type DocumentPrintMode } from "./document-print"
|
export { DocumentPrintClient, DOCUMENT_PRINT_ROUTES, type DocumentPrintType, type DocumentPrintMode } from "./document-print"
|
||||||
export {
|
|
||||||
DocumentShareClient,
|
|
||||||
DOCUMENT_SHARE_ROUTES,
|
|
||||||
type DocumentShareType,
|
|
||||||
type DocumentShareChannel,
|
|
||||||
type DocumentSharePayload,
|
|
||||||
type DocumentShareResponse,
|
|
||||||
} from "./document-share"
|
|
||||||
export { HolidaysClient, HOLIDAY_ROUTES } from "./holidays"
|
export { HolidaysClient, HOLIDAY_ROUTES } from "./holidays"
|
||||||
export { MakeAndModelsClient, MAKE_AND_MODEL_ROUTES } from "./make-and-models"
|
export { MakeAndModelsClient, MAKE_AND_MODEL_ROUTES } from "./make-and-models"
|
||||||
export { TimeSheetsClient, TIME_SHEET_ROUTES } from "./time-sheets"
|
export { TimeSheetsClient, TIME_SHEET_ROUTES } from "./time-sheets"
|
||||||
|
|||||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@ -62,9 +62,6 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
nextjs-toploader:
|
|
||||||
specifier: ^3.9.17
|
|
||||||
version: 3.9.17(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
nuqs:
|
nuqs:
|
||||||
specifier: ^2.8.9
|
specifier: ^2.8.9
|
||||||
version: 2.8.9(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
version: 2.8.9(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||||
@ -3710,13 +3707,6 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
nextjs-toploader@3.9.17:
|
|
||||||
resolution: {integrity: sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==}
|
|
||||||
peerDependencies:
|
|
||||||
next: '>= 6.0.0'
|
|
||||||
react: '>= 16.0.0'
|
|
||||||
react-dom: '>= 16.0.0'
|
|
||||||
|
|
||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@ -3737,9 +3727,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
nprogress@0.2.0:
|
|
||||||
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
|
|
||||||
|
|
||||||
nuqs@2.8.9:
|
nuqs@2.8.9:
|
||||||
resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==}
|
resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4934,7 +4921,7 @@ snapshots:
|
|||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@ -5092,7 +5079,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.2
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@ -5202,7 +5189,7 @@ snapshots:
|
|||||||
'@eslint/config-array@0.21.1':
|
'@eslint/config-array@0.21.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.7
|
'@eslint/object-schema': 2.1.7
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -5210,7 +5197,7 @@ snapshots:
|
|||||||
'@eslint/config-array@0.21.2':
|
'@eslint/config-array@0.21.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.7
|
'@eslint/object-schema': 2.1.7
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -5226,7 +5213,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@ -5240,7 +5227,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@3.3.5':
|
'@eslint/eslintrc@3.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.14.0
|
ajv: 6.14.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@ -6570,7 +6557,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
||||||
'@typescript-eslint/visitor-keys': 8.50.0
|
'@typescript-eslint/visitor-keys': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
eslint: 9.39.1(jiti@2.6.1)
|
eslint: 9.39.1(jiti@2.6.1)
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -6582,7 +6569,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.50.0
|
'@typescript-eslint/visitor-keys': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -6592,7 +6579,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -6601,7 +6588,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -6624,7 +6611,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
||||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.2)
|
'@typescript-eslint/utils': 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.2)
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
eslint: 9.39.1(jiti@2.6.1)
|
eslint: 9.39.1(jiti@2.6.1)
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.2)
|
ts-api-utils: 2.1.0(typescript@5.9.2)
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
@ -6636,7 +6623,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@ -6651,7 +6638,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/visitor-keys': 8.50.0
|
'@typescript-eslint/visitor-keys': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
@ -6666,7 +6653,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.50.0
|
'@typescript-eslint/types': 8.50.0
|
||||||
'@typescript-eslint/visitor-keys': 8.50.0
|
'@typescript-eslint/visitor-keys': 8.50.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
@ -6949,7 +6936,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
http-errors: 2.0.1
|
http-errors: 2.0.1
|
||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@ -7542,7 +7529,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
get-tsconfig: 4.13.7
|
get-tsconfig: 4.13.7
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@ -7706,7 +7693,7 @@ snapshots:
|
|||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.4.0
|
eslint-scope: 8.4.0
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
@ -7747,7 +7734,7 @@ snapshots:
|
|||||||
ajv: 6.14.0
|
ajv: 6.14.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.4.0
|
eslint-scope: 8.4.0
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
@ -7859,7 +7846,7 @@ snapshots:
|
|||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cookie-signature: 1.2.2
|
cookie-signature: 1.2.2
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
@ -7957,7 +7944,7 @@ snapshots:
|
|||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@ -8161,6 +8148,13 @@ snapshots:
|
|||||||
jsprim: 2.0.2
|
jsprim: 2.0.2
|
||||||
sshpk: 1.18.0
|
sshpk: 1.18.0
|
||||||
|
|
||||||
|
https-proxy-agent@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
agent-base: 7.1.4
|
||||||
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
@ -8702,14 +8696,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
nextjs-toploader@3.9.17(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
|
||||||
dependencies:
|
|
||||||
next: 16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
nprogress: 0.2.0
|
|
||||||
prop-types: 15.8.1
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@3.3.2:
|
node-fetch@3.3.2:
|
||||||
@ -8729,8 +8715,6 @@ snapshots:
|
|||||||
path-key: 4.0.0
|
path-key: 4.0.0
|
||||||
unicorn-magic: 0.3.0
|
unicorn-magic: 0.3.0
|
||||||
|
|
||||||
nprogress@0.2.0: {}
|
|
||||||
|
|
||||||
nuqs@2.8.9(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4):
|
nuqs@2.8.9(next@16.1.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
@ -9260,7 +9244,7 @@ snapshots:
|
|||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
is-promise: 4.0.0
|
is-promise: 4.0.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
@ -9311,7 +9295,7 @@ snapshots:
|
|||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
@ -9379,7 +9363,7 @@ snapshots:
|
|||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
fs-extra: 11.3.4
|
fs-extra: 11.3.4
|
||||||
fuzzysort: 3.1.0
|
fuzzysort: 3.1.0
|
||||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
https-proxy-agent: 7.0.6
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
msw: 2.12.14(@types/node@25.5.0)(typescript@5.9.3)
|
msw: 2.12.14(@types/node@25.5.0)(typescript@5.9.3)
|
||||||
node-fetch: 3.3.2
|
node-fetch: 3.3.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user