Skip to content

Commit

Permalink
feat: add history transaction (#8)
Browse files Browse the repository at this point in the history
* feat: add history transaction query

* feat: add transaction to navbar

* feat: change /transaction to /history

* feat: update checkout redirect and enhance transaction index page

* feat: add filter by category and date in TransactionController

* feat: add history transaction page

* feat: add empty transaction state

* feat: add search filter to transaction index page

* feat: update category filter to support multiple category

* feat: add filter transaction by category

* feat: add filter by purchase date

* fix: cart cant checkout one item

* fix: fix timezone

---------

Co-authored-by: Ainun Nadhifah Syamsiyah <[email protected]>
  • Loading branch information
nisrinasalm and ainunns authored Dec 2, 2024
1 parent bae42a7 commit 3c2f6f0
Show file tree
Hide file tree
Showing 18 changed files with 566 additions and 32 deletions.
4 changes: 3 additions & 1 deletion app/Http/Controllers/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Illuminate\Support\Carbon;

class CartController extends Controller
{
Expand Down Expand Up @@ -94,6 +95,7 @@ function checkout(Request $request)
}

$transaction->total_price = $total_price;
$transaction->created_at = Carbon::now('Asia/Jakarta');
$transaction->save();

for($i = 0; $i < count($validatedCart['product_id']); $i++) {
Expand All @@ -105,7 +107,7 @@ function checkout(Request $request)

DB::commit();

return redirect()->back();
return redirect()->intended(route('transaction.index'));
} catch (\Exception $e) {
DB::rollBack();
throw $e;
Expand Down
49 changes: 49 additions & 0 deletions app/Http/Controllers/TransactionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace App\Http\Controllers;

use App\Models\Transaction;
use App\Models\Category;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;

class TransactionController extends Controller
{
public function index(Request $request)
{
$categoryIds = $request->query('category_id', []);
$startDate = $request->query('start_date');
$endDate = $request->query('end_date');

$category = Category::all();

$transaction = Transaction::with(['cart_product.product.category'])
->whereHas('cart_product.product.category', function ($query) use ($categoryIds) {
if (!empty($categoryIds)) {
$query->whereIn('category_id', $categoryIds);
}
})
->whereHas('cart_product', function ($query) {
$query->where('user_id', Auth::id());
});

if ($startDate && $endDate) {
$transaction = $transaction->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay()
]);
}

$transaction = $transaction->orderBy('created_at', 'desc')->get();

return Inertia::render('Transaction/Index', [
'transaction' => $transaction,
'category' => $category,
'categoryIds' => $categoryIds,
'startDate' => $startDate,
'endDate' => $endDate,
]);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lodash.get": "^4.4.2",
"lucide-react": "^0.451.0",
"react-datepicker": "^7.5.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion resources/js/Components/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline",
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline hover:cursor-pointer",
className,
)}
{...props}
Expand Down
7 changes: 5 additions & 2 deletions resources/js/Components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from "react";
import Typography from "./Typography";

export default function Badge({ children }: { children: React.ReactNode }) {
export default function Badge({
children,
className,
}: { children: React.ReactNode; className?: string }) {
return (
<div className="py-1 px-3 bg-secondary-500 border-primary-500 border rounded-lg">
<Typography variant="s3" className="">
<Typography variant="s3" className={className}>
{children}
</Typography>
</div>
Expand Down
59 changes: 59 additions & 0 deletions resources/js/Components/DatepickerFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import RangeDatePicker from "./Forms/RangeDatepicker";

export type PopupFilterProps = {
date: Date[];
setDate: React.Dispatch<React.SetStateAction<Date[]>>;
onResetFilter?: () => void;
} & React.ComponentPropsWithoutRef<"div">;

type FormData = {
filter: Date[];
};

export default function DatepickerFilter({
date,
setDate,
onResetFilter,
}: PopupFilterProps) {
//#region //*=========== Form ===========t
const methods = useForm<FormData>({
mode: "onTouched",
defaultValues: {
filter: date,
},
});
const { control, setValue } = methods;

const filter =
useWatch({
control,
name: "filter",
}) ?? [];
//#endregion //*======== Form ===========

React.useEffect(() => {
setDate(filter);
}, [filter]);

const resetFilter = () => {
onResetFilter?.();
setValue("filter", []);
};

return (
<FormProvider {...methods}>
<div className="relative w-80">
<RangeDatePicker
id="filter"
label={null}
placeholder="Filter by date"
containerClassName="w-full"
isClearable={filter.length > 0}
onClearDate={resetFilter}
/>
</div>
</FormProvider>
);
}
22 changes: 13 additions & 9 deletions resources/js/Components/Forms/RangeDatepicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from "clsx";
import get from "lodash.get";
import { Calendar } from "lucide-react";
import { Calendar, XCircle } from "lucide-react";
import { Controller, RegisterOptions, useFormContext } from "react-hook-form";

import "react-datepicker/dist/react-datepicker.css";
Expand All @@ -16,12 +16,12 @@ type ReactDatePickerProps = {
placeholder?: string;
defaultYear?: number;
defaultMonth?: number;
defaultValue?: string;
helperText?: string;
readOnly?: boolean;
/** Disable error style (not disabling error validation) */
hideError?: boolean;
containerClassName?: string;
onClearDate?: () => void;
} & Omit<DatePickerProps, "onChange">;

export default function RangeDatePicker({
Expand All @@ -31,11 +31,12 @@ export default function RangeDatePicker({
placeholder,
defaultYear,
defaultMonth,
defaultValue,
helperText,
readOnly = false,
hideError = false,
disabled,
isClearable,
onClearDate,
containerClassName,
}: ReactDatePickerProps) {
const {
Expand Down Expand Up @@ -76,7 +77,6 @@ export default function RangeDatePicker({
<Controller
control={control}
rules={validation}
defaultValue={defaultValue}
name={id}
render={({ field: { onChange, onBlur, value } }) => {
const startDate = Array.isArray(value)
Expand All @@ -100,7 +100,7 @@ export default function RangeDatePicker({
endDate={endDate}
className={clsx(
"flex w-full rounded-lg shadow-sm",
"min-h-[2.25rem] py-0 md:min-h-[2.5rem]",
"min-h-[2.5rem] py-0 md:min-h-[2.75rem]",
"border-gray-300 focus:border-primary-500 focus:ring-primary-500",
(readOnly || disabled) &&
"cursor-not-allowed border-gray-300 bg-gray-100 focus:border-gray-300 focus:ring-0",
Expand All @@ -118,10 +118,14 @@ export default function RangeDatePicker({
disabled={disabled}
selectsRange
/>
<Calendar
size={18}
className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 transform text-typo-icons"
/>
<div className="absolute flex gap-2 right-4 top-1/2 -translate-y-1/2 transform text-typo-icons">
{isClearable && (
<button type="button" onClick={() => onClearDate?.()}>
<XCircle size={16} className="text-typo-icons" />
</button>
)}
<Calendar size={18} />
</div>
</div>
{helperText && (
<Typography variant="c1" color="secondary" className="mt-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,43 @@ export type PopupFilterProps<T extends Record<string, string[]>> = {
name: string;
}[];
}[];
filterQuery: T;
setFilterQuery: React.Dispatch<React.SetStateAction<T>>;
onResetFilter?: () => void;
title?: string;
} & React.ComponentPropsWithoutRef<"div">;

type FormData = {
filter: string[];
};

export default function PopupFilter<T extends Record<string, string[]>>({
filterOption,
filterQuery,
setFilterQuery,
onResetFilter,
title = "Filter",
}: PopupFilterProps<T>) {
//#region //*=========== Form ===========
const methods = useForm({
const defaultFilterValues = React.useMemo(() => {
return Object.entries(filterQuery).reduce((acc, [key, values]) => {
return [...acc, ...values.map((value) => `${key}.${value}`)];
}, [] as string[]);
}, [filterQuery]);

const methods = useForm<FormData>({
mode: "onTouched",
defaultValues: {
filter: defaultFilterValues,
},
});
const { control, setValue } = methods;

const filter: string[] = useWatch({
control,
name: "filter[]",
});
const filter =
useWatch({
control,
name: "filter",
}) ?? [];
//#endregion //*======== Form ===========

React.useEffect(() => {
Expand All @@ -56,7 +74,10 @@ export default function PopupFilter<T extends Record<string, string[]>>({
setFilterQuery(parsedFilter);
}, [filter, filterOption, setFilterQuery]);

const resetFilter = () => setValue("filter[]", []);
const resetFilter = () => {
onResetFilter?.();
setValue("filter", []);
};

return (
<Popover>
Expand Down
9 changes: 8 additions & 1 deletion resources/js/Components/UnstyledLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ export type UnstyledLinkProps = {

const UnstyledLink = React.forwardRef<HTMLAnchorElement, UnstyledLinkProps>(
(
{ children, href, openNewTab, className, inertiaLinkProps, ...rest },
{
children,
href,
openNewTab = false,
className,
inertiaLinkProps,
...rest
},
ref,
) => {
const isNewTab =
Expand Down
8 changes: 8 additions & 0 deletions resources/js/Layouts/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export default function Navbar() {
>
Cart
</NavLink>
)}{" "}
{auth.user !== null && auth.user.role === "user" && (
<NavLink
href={route("transaction.index")}
active={route().current("transaction.index")}
>
Transaction
</NavLink>
)}
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion resources/js/Pages/Cart/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const CartIndex = ({ cart }: { cart: CartType[] }) => {
const readyToCheckout =
cart.filter((item) => checkedOut.includes(item.id.toString())) ?? [];

transform((data) => ({ ...data, ...getValues() }));
transform((data) => ({
...data,
product_id:
typeof getValues("product_id") === "string"
? [getValues("product_id")]
: getValues("product_id"),
}));

const onSubmit: SubmitHandler<CheckoutProductType> = async () => {
post(route("cart.checkout"), {
Expand Down
Loading

0 comments on commit 3c2f6f0

Please sign in to comment.