mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-06-29 13:43:58 +00:00
Add payment processing and device management features
- Introduced createPayment action for handling payment creation. - Added PaymentsTable component for displaying payment records with pagination. - Implemented new PaymentPage for viewing individual payment details and associated devices. - Refactored DeviceCartDrawer to integrate payment creation and device selection. - Enhanced DevicesToPay component to display devices based on payment status. - Updated PriceCalculator component for better user input handling. - Introduced NumberInput component for consistent number input across forms. - Modified Prisma schema to include new fields for payments and devices. - Improved overall user experience with responsive design adjustments and new UI elements.
This commit is contained in:
@ -1,24 +0,0 @@
|
||||
import DevicesToPay from '@/components/devices-to-pay';
|
||||
import prisma from '@/lib/db';
|
||||
import React from 'react'
|
||||
|
||||
export default async function PaymentPage() {
|
||||
const formula = await prisma.billFormula.findFirst();
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
|
||||
<h3>
|
||||
Payment
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="user-filters"
|
||||
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||
>
|
||||
<DevicesToPay billFormula={formula ?? undefined} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
37
app/(dashboard)/payments/[paymentId]/page.tsx
Normal file
37
app/(dashboard)/payments/[paymentId]/page.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import DevicesToPay from "@/components/devices-to-pay";
|
||||
import { hasSession } from "@/lib/auth-guard";
|
||||
import prisma from "@/lib/db";
|
||||
import React from "react";
|
||||
|
||||
export default async function PaymentPage({
|
||||
params,
|
||||
}: { params: Promise<{ paymentId: string }> }) {
|
||||
const paymentId = (await params).paymentId;
|
||||
const payment = await prisma.payment.findUnique({
|
||||
where: {
|
||||
id: paymentId,
|
||||
},
|
||||
include: {
|
||||
devices: true,
|
||||
},
|
||||
});
|
||||
await hasSession();
|
||||
const formula = await prisma.billFormula.findFirst();
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
|
||||
<h3>Payment</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="user-filters"
|
||||
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||
>
|
||||
<DevicesToPay
|
||||
billFormula={formula ?? undefined}
|
||||
payment={payment || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,14 +1,33 @@
|
||||
"use client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import React from "react";
|
||||
import { PaymentsTable } from "@/components/payments-table";
|
||||
import Search from "@/components/search";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function MyPayments() {
|
||||
const session = authClient.useSession();
|
||||
export default async function Devices({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{
|
||||
query: string;
|
||||
page: number;
|
||||
sortBy: string;
|
||||
status: string;
|
||||
}>;
|
||||
}) {
|
||||
const query = (await searchParams)?.query || "";
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
|
||||
<h3>My Payments</h3>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Client session</h3>
|
||||
<pre>{JSON.stringify(session.data, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
<div
|
||||
id="user-filters"
|
||||
className=" border-b-2 pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||
>
|
||||
<Search />
|
||||
</div>
|
||||
<Suspense key={query} fallback={"loading...."}>
|
||||
<PaymentsTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,125 +1,8 @@
|
||||
"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,
|
||||
]);
|
||||
import PriceCalculator from '@/components/price-calculator'
|
||||
import React from 'react'
|
||||
|
||||
export default function Pricing() {
|
||||
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>
|
||||
);
|
||||
<PriceCalculator />
|
||||
)
|
||||
}
|
||||
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user