update billing
This commit is contained in:
@@ -169,10 +169,10 @@ function List({ items, search }) {
|
|||||||
}, [search, fuse, items])
|
}, [search, fuse, items])
|
||||||
|
|
||||||
const charges = filtered
|
const charges = filtered
|
||||||
?.filter((item) => 'course' in item && item?.unit_price > 0)
|
?.filter((item) => item?.unit_price > 0)
|
||||||
?.sort(sortBy('enrolled_at'))
|
?.sort(sortBy('enrolled_at'))
|
||||||
const credits = filtered
|
const credits = filtered
|
||||||
?.filter((item) => 'course' in item && item?.unit_price < 0)
|
?.filter((item) => item?.unit_price < 0)
|
||||||
?.sort(sortBy('created_at'))
|
?.sort(sortBy('created_at'))
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -207,6 +207,16 @@ function List({ items, search }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subtotal = filtered
|
||||||
|
?.filter(({ unit_price }) => unit_price > 0)
|
||||||
|
?.reduce((acc, { unit_price }) => acc + unit_price, 0)
|
||||||
|
|
||||||
|
const discounts = filtered
|
||||||
|
?.filter(({ unit_price }) => unit_price < 0)
|
||||||
|
?.reduce((acc, { unit_price }) => acc + unit_price, 0)
|
||||||
|
|
||||||
|
const total = filtered?.reduce((acc, { unit_price }) => acc + unit_price, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="table-auto w-full">
|
<Table className="table-auto w-full">
|
||||||
{charges.length ? (
|
{charges.length ? (
|
||||||
@@ -288,16 +298,28 @@ function List({ items, search }) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-right pointer-events-none">
|
||||||
|
Subtotal
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Currency>{subtotal}</Currency>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-right pointer-events-none">
|
||||||
|
Descontos
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Currency>{discounts}</Currency>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-right pointer-events-none">
|
<TableCell colSpan={4} className="text-right pointer-events-none">
|
||||||
Total
|
Total
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Currency>
|
<Currency>{total}</Currency>
|
||||||
{filtered
|
|
||||||
?.filter((x) => 'course' in x)
|
|
||||||
?.reduce((acc, { unit_price }) => acc + unit_price, 0)}
|
|
||||||
</Currency>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { ErrorMessage } from '@hookform/error-message'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
|
||||||
import { Form } from '@repo/ui/components/ui/form'
|
import { Form } from '@repo/ui/components/ui/form'
|
||||||
import { InputGroup, InputGroupInput } from '@repo/ui/components/ui/input-group'
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupInput
|
||||||
|
} from '@repo/ui/components/ui/input-group'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Separator } from '@repo/ui/components/ui/separator'
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
MAX_ITEMS,
|
MAX_ITEMS,
|
||||||
|
formSchema,
|
||||||
type Course,
|
type Course,
|
||||||
type User
|
type User
|
||||||
} from '../_.$orgid.enrollments.add/data'
|
} from '../_.$orgid.enrollments.add/data'
|
||||||
@@ -39,11 +44,11 @@ type AssignedProps = {
|
|||||||
export function Assigned({ courses }: AssignedProps) {
|
export function Assigned({ courses }: AssignedProps) {
|
||||||
const { orgid } = useParams()
|
const { orgid } = useParams()
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: { enrollments: [emptyRow] }
|
defaultValues: { enrollments: [emptyRow] }
|
||||||
})
|
})
|
||||||
const { formState, control, handleSubmit } = form
|
const { formState, control, handleSubmit } = form
|
||||||
const { fields, insert, remove, append } = useFieldArray({
|
const { fields, remove, append } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: 'enrollments'
|
name: 'enrollments'
|
||||||
})
|
})
|
||||||
@@ -51,6 +56,10 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
control,
|
control,
|
||||||
name: 'enrollments'
|
name: 'enrollments'
|
||||||
})
|
})
|
||||||
|
const subtotal = items.reduce(
|
||||||
|
(acc, { course }) => acc + (course?.unit_price || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
const onSearch = async (search: string) => {
|
const onSearch = async (search: string) => {
|
||||||
const params = new URLSearchParams({ q: search })
|
const params = new URLSearchParams({ q: search })
|
||||||
@@ -108,6 +117,7 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
{/* Separator only for mobile */}
|
{/* Separator only for mobile */}
|
||||||
{index >= 1 && <div className="h-2.5 lg:hidden"></div>}
|
{index >= 1 && <div className="h-2.5 lg:hidden"></div>}
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`enrollments.${index}.user`}
|
name={`enrollments.${index}.user`}
|
||||||
@@ -134,6 +144,7 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Course */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`enrollments.${index}.course`}
|
name={`enrollments.${index}.course`}
|
||||||
@@ -161,6 +172,7 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Scheduled for */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`enrollments.${index}.scheduled_for`}
|
name={`enrollments.${index}.scheduled_for`}
|
||||||
@@ -169,7 +181,12 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Unit price */}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Valor unit.
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
className="pointer-events-none"
|
className="pointer-events-none"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -178,6 +195,7 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
<Button
|
<Button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -190,19 +208,86 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
{/* Add button */}
|
||||||
type="button"
|
<div className="max-lg:mb-2.5">
|
||||||
// @ts-ignore
|
<Button
|
||||||
onClick={() => append(emptyRow)}
|
type="button"
|
||||||
className="cursor-pointer"
|
// @ts-ignore
|
||||||
disabled={fields.length == MAX_ITEMS}
|
onClick={() => append(emptyRow)}
|
||||||
variant="outline"
|
className="cursor-pointer"
|
||||||
size="sm"
|
disabled={fields.length == MAX_ITEMS}
|
||||||
>
|
variant="outline"
|
||||||
<PlusIcon /> Adicionar
|
size="sm"
|
||||||
</Button>
|
>
|
||||||
|
<PlusIcon /> Adicionar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtotal */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Subtotal
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Subtotal
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="subtotal"
|
||||||
|
value={currency.format(subtotal)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Discount */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Cupom
|
||||||
|
</div>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Cupom
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="discount"
|
||||||
|
value={currency.format(0)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs cursor-pointer h-6 px-2"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Adicionar
|
||||||
|
</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Total
|
||||||
|
</div>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Total
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="total"
|
||||||
|
value={currency.format(subtotal)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { ErrorMessage } from '@hookform/error-message'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { Form } from '@repo/ui/components/ui/form'
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
|
import { Input } from '@repo/ui/components/ui/input'
|
||||||
|
import { Form } from '@repo/ui/components/ui/form'
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Separator } from '@repo/ui/components/ui/separator'
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
|
|
||||||
import { Cell } from '../_.$orgid.enrollments.add/route'
|
import { Cell } from '../_.$orgid.enrollments.add/route'
|
||||||
@@ -70,6 +71,13 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
control,
|
control,
|
||||||
name: 'items'
|
name: 'items'
|
||||||
})
|
})
|
||||||
|
const subtotal = items.reduce(
|
||||||
|
(acc, { course, quantity }) =>
|
||||||
|
acc +
|
||||||
|
(course?.unit_price || 0) *
|
||||||
|
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
@@ -98,6 +106,7 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
{/* Separator only for mobile */}
|
{/* Separator only for mobile */}
|
||||||
{index >= 1 && <div className="h-2.5 lg:hidden"></div>}
|
{index >= 1 && <div className="h-2.5 lg:hidden"></div>}
|
||||||
|
|
||||||
|
{/* Course */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`items.${index}.course`}
|
name={`items.${index}.course`}
|
||||||
@@ -128,7 +137,12 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Quantity */}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Qtd.
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -182,7 +196,11 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Unit price */}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Valor unit.
|
||||||
|
</InputGroupAddon>
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="pointer-events-none"
|
className="pointer-events-none"
|
||||||
@@ -191,7 +209,12 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Total
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="pointer-events-none"
|
className="pointer-events-none"
|
||||||
@@ -205,6 +228,7 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
<Button
|
<Button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -217,24 +241,90 @@ export function Bulk({ courses }: BulkProps) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
{/* Add button */}
|
||||||
type="button"
|
<div className="max-lg:mb-2.5">
|
||||||
onClick={() => {
|
<Button
|
||||||
// @ts-ignore
|
type="button"
|
||||||
append(emptyRow, { shouldFocus: false })
|
onClick={() => {
|
||||||
queueMicrotask(() => {
|
// @ts-ignore
|
||||||
setFocus(`items.${fields.length}.course`)
|
append(emptyRow, { shouldFocus: false })
|
||||||
})
|
queueMicrotask(() => {
|
||||||
}}
|
setFocus(`items.${fields.length}.course`)
|
||||||
className="cursor-pointer"
|
})
|
||||||
disabled={fields.length == MAX_ITEMS}
|
}}
|
||||||
variant="outline"
|
className="cursor-pointer"
|
||||||
size="sm"
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<PlusIcon /> Adicionar
|
>
|
||||||
</Button>
|
<PlusIcon /> Adicionar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtotal */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Subtotal
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Subtotal
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="subtotal"
|
||||||
|
value={currency.format(subtotal)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Discount */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Cupom
|
||||||
|
</div>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Cupom
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="discount"
|
||||||
|
value={currency.format(0)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs cursor-pointer h-6 px-2"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Adicionar
|
||||||
|
</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<>
|
||||||
|
<div className="col-start-3 flex items-center justify-end text-sm font-medium max-lg:hidden">
|
||||||
|
Total
|
||||||
|
</div>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon className="border-r pr-2.5 w-1/3 lg:hidden justify-end">
|
||||||
|
Total
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputGroupInput
|
||||||
|
name="total"
|
||||||
|
value={currency.format(subtotal)}
|
||||||
|
className="pointer-events-none text-muted-foreground"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export const enrollment = z.object({
|
|||||||
{
|
{
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
access_period: z.number()
|
access_period: z.number(),
|
||||||
|
unit_price: z.number().optional()
|
||||||
},
|
},
|
||||||
{ error: 'Escolha um curso' }
|
{ error: 'Escolha um curso' }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -349,19 +349,21 @@ export default function Route({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<div className="max-lg:mt-2.5">
|
||||||
type="button"
|
<Button
|
||||||
// @ts-ignore
|
type="button"
|
||||||
onClick={() => append(emptyRow)}
|
// @ts-ignore
|
||||||
className="cursor-pointer"
|
onClick={() => append(emptyRow)}
|
||||||
disabled={fields.length == MAX_ITEMS}
|
className="cursor-pointer"
|
||||||
variant="outline"
|
disabled={fields.length == MAX_ITEMS}
|
||||||
size="sm"
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<PlusIcon /> Adicionar
|
>
|
||||||
</Button>
|
<PlusIcon /> Adicionar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user