mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-23 10:02:00 +00:00
- Added new PaymentPage component for processing payments and displaying devices to pay. - Introduced DeviceDetails component for viewing individual device information. - Implemented PriceCalculator component for calculating costs based on user input. - Integrated Jotai for state management across components, including device cart functionality. - Updated layout to include Jotai Provider for state management. - Enhanced DevicesTable with AddDevicesToCartButton for adding devices to the cart. - Refactored sidebar to include a link to the new Price Calculator page. - Updated Prisma schema to include Payment and BillFormula models for better data handling. - Added new UI components for device cart management and drawer functionality. - Improved overall user experience with responsive design adjustments and new UI elements.
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
"use client";
|
|
import {
|
|
discountPercentageAtom,
|
|
formulaResultAtom,
|
|
initialPriceAtom,
|
|
numberOfDaysAtom,
|
|
numberOfDevicesAtom,
|
|
} from "@/lib/atoms";
|
|
import { useAtom } from "jotai";
|
|
import { Minus, Plus } from "lucide-react";
|
|
import { useEffect } from "react";
|
|
import {
|
|
Button,
|
|
Group,
|
|
Input,
|
|
Label,
|
|
NumberField,
|
|
} from "react-aria-components";
|
|
|
|
|
|
export default function PriceCalculator() {
|
|
const [initialPrice, setInitialPrice] = useAtom(initialPriceAtom);
|
|
const [discountPercentage, setDiscountPercentage] = useAtom(
|
|
discountPercentageAtom,
|
|
);
|
|
const [numberOfDevices, setNumberOfDevices] = useAtom(numberOfDevicesAtom);
|
|
const [numberOfDays, setNumberOfDays] = useAtom(numberOfDaysAtom);
|
|
const [formulaResult, setFormulaResult] = useAtom(formulaResultAtom);
|
|
|
|
useEffect(() => {
|
|
const basePrice = initialPrice + (numberOfDevices - 1) * discountPercentage;
|
|
setFormulaResult(
|
|
`Price for ${numberOfDevices} device(s) over ${numberOfDays} day(s): MVR ${basePrice.toFixed(2)}`,
|
|
);
|
|
}, [
|
|
initialPrice,
|
|
discountPercentage,
|
|
numberOfDevices,
|
|
numberOfDays,
|
|
setFormulaResult,
|
|
]);
|
|
|
|
return (
|
|
<div className="border p-2 rounded-xl">
|
|
<div className="flex flex-col justify-between items-start text-gray-500 title-bg p-2 mb-4">
|
|
<h3 className="text-2xl font-semibold">Price Calculator</h3>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{/* Initial Price Input */}
|
|
<NumberInput
|
|
label="Initial Price"
|
|
value={initialPrice}
|
|
onChange={(value) => setInitialPrice(value)}
|
|
/>
|
|
{/* Number of Devices Input */}
|
|
<NumberInput
|
|
label="Number of Devices"
|
|
value={numberOfDevices}
|
|
onChange={(value) => setNumberOfDevices(value)}
|
|
/>
|
|
{/* Number of Days Input */}
|
|
<NumberInput
|
|
label="Number of Days"
|
|
value={numberOfDays}
|
|
onChange={(value) => setNumberOfDays(value)}
|
|
/>
|
|
|
|
{/* Discount Percentage Input */}
|
|
<NumberInput
|
|
label="Discount Percentage"
|
|
value={discountPercentage}
|
|
onChange={(value) => setDiscountPercentage(value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<div className="title-bg relative rounded-lg border border-input shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
|
|
<label
|
|
htmlFor=""
|
|
className="block px-3 pt-2 text-md font-medium text-foreground"
|
|
>
|
|
Total
|
|
</label>
|
|
<input
|
|
className="flex font-mono font-semibold h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
|
|
value={formulaResult}
|
|
readOnly
|
|
placeholder={"Result"}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Dependencies: pnpm install lucide-react react-aria-components
|
|
|
|
function NumberInput({
|
|
label,
|
|
value,
|
|
onChange,
|
|
}: { label: string; value: number; onChange: (value: number) => void }) {
|
|
return (
|
|
<NumberField value={value} minValue={0} onChange={onChange}>
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium text-foreground">{label}</Label>
|
|
<Group className="relative inline-flex h-9 w-full items-center overflow-hidden whitespace-nowrap rounded-lg border border-input text-sm shadow-sm shadow-black/5 transition-shadow data-[focus-within]:border-ring data-[disabled]:opacity-50 data-[focus-within]:outline-none data-[focus-within]:ring-[3px] data-[focus-within]:ring-ring/20">
|
|
<Button
|
|
slot="decrement"
|
|
className="-ms-px flex aspect-square h-[inherit] items-center justify-center rounded-s-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<Minus size={16} strokeWidth={2} aria-hidden="true" />
|
|
</Button>
|
|
<Input className="w-full grow bg-background px-3 py-2 text-center tabular-nums text-foreground focus:outline-none" />
|
|
<Button
|
|
slot="increment"
|
|
className="-me-px flex aspect-square h-[inherit] items-center justify-center rounded-e-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<Plus size={16} strokeWidth={2} aria-hidden="true" />
|
|
</Button>
|
|
</Group>
|
|
</div>
|
|
</NumberField>
|
|
);
|
|
}
|