finish bills , po , jobcards relational fields creation

This commit is contained in:
2026-04-13 18:48:15 +03:00
parent 3eb471a604
commit 020ffccfd6
41 changed files with 11973 additions and 467 deletions
@@ -0,0 +1,2 @@
export { ResourceSelectorDialog, type ResourceSelectorDialogProps } from "./resource-selector-dialog"
export { RhfResourceField, type RhfResourceFieldProps } from "./rhf-resource-field"
@@ -0,0 +1,93 @@
"use client"
import { useState, useRef } from "react"
import { Button } from "@/shared/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/shared/components/ui/dialog"
import { CrudResource, type CrudResourceProps } from "@/shared/data-view/resource-page"
import type { ResourcePageClient, ResourceItem } from "@/shared/data-view/resource-page"
import { Card, CardContent } from "@/shared/components/ui/card"
export type ResourceSelectorDialogProps<TClient extends ResourcePageClient> = {
/** Dialog title shown in the header */
title: string
/** Opens/closes the dialog */
open: boolean
onOpenChange: (open: boolean) => void
/** Called when user confirms selection */
onConfirm: (selectedRows: ResourceItem<TClient>[]) => void
/** CrudResource config for the table (columns, routeKey, getClient, etc.) */
crudProps: Omit<CrudResourceProps<TClient>, "render" | "tableProps">
/** Optional rowKey for selection identity. Defaults to "id" */
rowKey?: keyof ResourceItem<TClient>
}
export function ResourceSelectorDialog<TClient extends ResourcePageClient>({
title,
open,
onOpenChange,
onConfirm,
crudProps,
rowKey,
}: ResourceSelectorDialogProps<TClient>) {
type TItem = ResourceItem<TClient>
const selectedRef = useRef<TItem[]>([])
const [count, setCount] = useState(0)
const handleSelectionChange = (rows: TItem[]) => {
selectedRef.current = rows
setCount(rows.length)
}
const handleConfirm = () => {
onConfirm(selectedRef.current)
selectedRef.current = []
setCount(0)
onOpenChange(false)
}
const handleCancel = () => {
selectedRef.current = []
setCount(0)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) handleCancel() }}>
<DialogContent className="min-w-4xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">{title}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
<Card className="rounded-none border-0 shadow-none">
<CardContent className="p-0">
<CrudResource<TClient>
{...crudProps}
tableProps={{
selection: {
onSelectionChange: handleSelectionChange,
...(rowKey ? { rowKey } : {}),
},
}}
/>
</CardContent>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={count === 0}>
Confirm{count > 0 ? ` (${count})` : ""}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,131 @@
"use client"
import { useState, type ReactNode } from "react"
import { useController, useFormContext, type FieldValues, type FieldPath } from "react-hook-form"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Field, FieldError } from "@/shared/components/ui/field"
import { Plus } from "lucide-react"
import {
ResourceSelectorDialog,
type ResourceSelectorDialogProps,
} from "./resource-selector-dialog"
import type { ResourcePageClient, ResourceItem } from "@/shared/data-view/resource-page"
export type RhfResourceFieldProps<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
TClient extends ResourcePageClient,
> = {
/** RHF field name — value is an array of items after mapping */
name: TName
/** Label displayed on the card header */
label: string
/** Button text for the trigger */
triggerLabel?: string
/** Map a selected resource row into the form item shape */
mapSelected: (row: ResourceItem<TClient>) => TValues[TName][number]
/** Render the list of selected items inside the card */
renderItems: (
items: TValues[TName],
helpers: {
remove: (index: number) => void
update: (index: number, item: TValues[TName][number]) => void
replace: (items: TValues[TName]) => void
},
) => ReactNode
/** Props forwarded to ResourceSelectorDialog (columns, routeKey, getClient, etc.) */
dialogProps: Omit<
ResourceSelectorDialogProps<TClient>,
"open" | "onOpenChange" | "onConfirm"
>
/** Deduplicate by this key when merging new selections. Defaults to "id" */
itemKey?: string
}
export function RhfResourceField<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
TClient extends ResourcePageClient,
>({
name,
label,
triggerLabel,
mapSelected,
renderItems,
dialogProps,
itemKey = "id",
}: RhfResourceFieldProps<TValues, TName, TClient>) {
const { control } = useFormContext<TValues>()
const {
field,
fieldState: { error },
} = useController({ name, control })
const [open, setOpen] = useState(false)
const items: TValues[TName] = field.value ?? ([] as unknown as TValues[TName])
const handleConfirm = (rows: ResourceItem<TClient>[]) => {
const mapped = rows.map(mapSelected)
// Merge: keep existing items, add new ones (deduplicate by itemKey)
const existingKeys = new Set(
(items as any[]).map((item: any) => String(item[itemKey])),
)
const newItems = mapped.filter(
(item: any) => !existingKeys.has(String(item[itemKey])),
)
field.onChange([...items, ...newItems])
}
const helpers = {
remove: (index: number) => {
const next = [...(items as any[])]
next.splice(index, 1)
field.onChange(next)
},
update: (index: number, item: TValues[TName][number]) => {
const next = [...(items as any[])]
next[index] = item
field.onChange(next)
},
replace: (newItems: TValues[TName]) => {
field.onChange(newItems)
},
}
return (
<Field data-invalid={!!error || undefined}>
<Card>
<CardHeader className=" flex justify-between items-center">
<span className="text-base font-medium">{label}</span>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setOpen(true)}
>
<Plus className="mr-1.5 h-4 w-4" />
{triggerLabel ?? label}
</Button>
</CardHeader>
<CardContent>
{(items as any[]).length > 0
? renderItems(items, helpers)
: (
<p className="text-sm text-muted-foreground text-center py-4">
No items added yet.
</p>
)}
</CardContent>
</Card>
{error && <FieldError>{error.message}</FieldError>}
<ResourceSelectorDialog<TClient>
{...dialogProps}
open={open}
onOpenChange={setOpen}
onConfirm={handleConfirm}
/>
</Field>
)
}