diff --git a/.github/workflows/sahil-pay.yml b/.github/sahil-pay.yml similarity index 100% rename from .github/workflows/sahil-pay.yml rename to .github/sahil-pay.yml diff --git a/apps/client/package.json b/apps/client/package.json index 75b5b4b8..2a19ccc8 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -32,10 +32,12 @@ "next-auth-hasura-adapter": "^2.0.0", "postcss": "8.4.28", "react": "^18.3.1", + "react-day-picker": "8.10.0", "react-dom": "^18.3.1", "react-hook-form": "^7.45.4", "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", + "recharts": "^2.15.0", "subscriptions-transport-ws": "^0.11.0", "swr": "^2.2.1", "tailwindcss": "3.3.3", diff --git a/apps/client/src/Layout/layout.tsx b/apps/client/src/Layout/layout.tsx index defd3c38..a3fbc0be 100644 --- a/apps/client/src/Layout/layout.tsx +++ b/apps/client/src/Layout/layout.tsx @@ -13,6 +13,7 @@ import { HiOutlineBuildingOffice, HiOutlineCube, HiOutlineCreditCard, + HiOutlineDocumentChartBar, } from "react-icons/hi2"; const links = [ @@ -31,16 +32,15 @@ const links = [ href: "/account", icon: HiOutlineUserCircle, }, - { - - name: "Billing", - href: "/billing", - icon: HiOutlineCreditCard, - }, { name: "Inventory", href: "/inventory", icon: HiOutlineCube +}, +{ + name: "Reports", + href: "/reports", + icon: HiOutlineDocumentChartBar } ]; diff --git a/apps/client/src/pages/account/index.tsx b/apps/client/src/pages/account/index.tsx index 559d5618..7e7aa8cd 100644 --- a/apps/client/src/pages/account/index.tsx +++ b/apps/client/src/pages/account/index.tsx @@ -1,51 +1,87 @@ -import { - BusinessProfileOverview, - BusinessOrderHistory, -} from "@sahil/features/businesses"; -// import { useGetAccountBalance, useGetMomoAccountInfo } from "@/hooks/accounts"; -import { useFetchBusinessByPK } from "@sahil/lib/hooks/businesses"; -import { Card, JoinGrid } from "ui"; -import { useState } from "react"; -import { +"use client" + +import { useState } from "react" +import { Card, JoinGrid } from "ui" +import { + HiOutlineCurrencyDollar, + HiOutlineCreditCard, + HiOutlineDocumentText, + HiPlus, HiArrowSmallLeft, HiArrowSmallRight, HiOutlineMinusCircle, HiOutlineXCircle, - HiOutlineCheckCircle, -} from "react-icons/hi2"; -import { formatDateTime } from "@sahil/lib/dates"; -import { formatCurrency } from "@sahil/lib"; + HiOutlineCheckCircle +} from "react-icons/hi2" +import { formatDateTime } from "@sahil/lib/dates" +import { formatCurrency } from "@sahil/lib" +import { BusinessProfileOverview } from "@sahil/features/businesses" +import { useFetchBusinessByPK } from "@sahil/lib/hooks/businesses" -export default function Account() { - const { - data: business, - error, - loading, - } = useFetchBusinessByPK("e87924e8-69e4-4171-bd89-0c8963e03d08"); +const PaymentMethodCard = ({ method, isPreferred, onEdit, onSetPreferred }) => ( + +
+
+ {method.type} ending in {method.last4} + {isPreferred && Preferred} +
+
+ + {!isPreferred && ( + + )} +
+
+
+) - if (error) { - return

An error occurred while fetching you account details!

; +const TransactionCard = ({ transaction }) => { + const cardIcon = (status: string) => { + switch (status) { + case "Pending": + return + case "Canceled": + return + case "Confirmed": + return + default: + return null + } } return ( -
-
-
- { - // @ts-ignore - business && - } + +
+
+ {cardIcon(transaction.status)}
-
- - +
+ +
+
+

Method

+

{transaction.method}

+
+
+

Amount

+

{formatCurrency(transaction.amount)}

+
+
-
- ); +
+ ) } -const TransactionsHistory = () => { +export default function BillingDashboard() { + const [paymentMethods, setPaymentMethods] = useState([ + { id: 1, type: 'Visa', last4: '1234' }, + { id: 2, type: 'Mastercard', last4: '5678' }, + ]) + const [preferredMethodId, setPreferredMethodId] = useState(1) + const transactions = [ { amount: 1000, @@ -71,89 +107,114 @@ const TransactionsHistory = () => { status: "Confirmed", method: "Cash", }, - ]; + ] + + const handleAddPaymentMethod = () => { + // Implement add payment method logic + } + + const handleEditPaymentMethod = (id) => { + // Implement edit payment method logic + } + + const handleSetPreferred = (id) => { + setPreferredMethodId(id) + } + + const { + data: business, + error, + loading, + } = useFetchBusinessByPK("e87924e8-69e4-4171-bd89-0c8963e03d08") + + if (error) { + return

An error occurred while fetching your account details!

+ } + + if (loading) { + return

Loading...

+ } + return ( -
-
-

Latest Transactions

-
-
-
-
4 Transactions
-
-
- - +
+ +
+
+ {business && } + +
+

+ Payment Methods +

+ {paymentMethods.map((method) => ( + handleEditPaymentMethod(method.id)} + onSetPreferred={() => handleSetPreferred(method.id)} + /> + ))} - -
-
-
- {transactions.map((item, index) => ( - - ))} -
-
- ); -}; - -type cardProps = { - transaction: { - amount: number; - date: Date; - status: string; - method: string; - }; -}; - -const TransactionCard = ({ transaction }: cardProps) => { - const cardIcon = (status: string) => { - switch (status) { - case "Pending": - return ; - break; - case "Canceled": - return ; - break; - case "Confirmed": - return ; - } - }; + - return ( - -
-
- {cardIcon(transaction.status)} +
+

+ Summary +

+ +
+
+ Total Earned + $1,234.56 +
+
+ Total Spent + $567.89 +
+
+
+
-
- -
-
-

Method

-

{transaction.method}

+ +
+
+
+

+ Latest Transactions +

-
-

Amount

-

{formatCurrency(transaction.amount)}

+ +
+
+
{transactions.length} Transactions
+
+
+ + + + +
+
+ +
+ {transactions.map((transaction, index) => ( + + ))}
- - ); -}; +
+ ) +} + diff --git a/apps/client/src/pages/inventory/index.tsx b/apps/client/src/pages/inventory/index.tsx index 1970d5b1..975056d9 100644 --- a/apps/client/src/pages/inventory/index.tsx +++ b/apps/client/src/pages/inventory/index.tsx @@ -9,6 +9,7 @@ import { useRouter } from 'next/router'; import { formatDateTime } from "@sahil/lib/dates"; import BusinessInventoryHeader from '@sahil/features/Inventory/BusinessInventoryHeader'; import FilterPanel from '@sahil/features/Inventory/FilterPanel'; +import { CollectionControls } from "@sahil/features/Shared/CollectionControls"; interface ProductsTableProps { products: any[]; @@ -172,13 +173,7 @@ export default function InventoryPage() { return (
- +
, + href: "/orders/new/order_details", + primary: true, + }, +]; + +export default function OrdersPage() { + const router = useRouter(); + const { data: sessionData } = useSession(); + const { data: currentUser, loading: userLoading } = useGetUserById(sessionData?.user?.id); + + + const [filters, setFilters] = useState(initialFilters); + const [activeBusiness, setActiveBusiness] = useState(null); + + const { + suppliers, + activeSupplier, + switchSupplier, + loading: suppliersLoading + } = useUserSuppliers( + sessionData?.user?.id, + currentUser?.role + ); + + const stats = { + totalOrders: 1234, + pendingOrders: 56, + completedOrders: 1178 + }; -export default function Orders() { return ( -
-
-

- Orders Page -

-
-
+
+ + + + +
); } diff --git a/apps/client/src/pages/products/index.tsx b/apps/client/src/pages/products/index.tsx index a6f3d1be..97712e37 100644 --- a/apps/client/src/pages/products/index.tsx +++ b/apps/client/src/pages/products/index.tsx @@ -2,10 +2,15 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { ProductsCatalogue } from "@sahil/features/Products/ProductsCatalogue"; import { HiMagnifyingGlass, HiOutlineShoppingCart } from "react-icons/hi2"; +import { CollectionControls } from "@sahil/features/Shared/CollectionControls"; +import { useSession } from "next-auth/react"; +import { useGetUserById } from "@sahil/lib/hooks/users"; export default function Products() { const [name, setName] = useState(""); const router = useRouter(); + const { data: sessionData } = useSession(); + const { data: currentUser, loading: userLoading } = useGetUserById(sessionData?.user?.id); const onInputChange = (e: { target: { value: string } }) => { const value = e.target.value; @@ -31,17 +36,7 @@ export default function Products() { return (
-
-

Available Products

-
-
- 5 - -
-
-
+
diff --git a/apps/client/src/pages/reports/index.tsx b/apps/client/src/pages/reports/index.tsx new file mode 100644 index 00000000..7940b67b --- /dev/null +++ b/apps/client/src/pages/reports/index.tsx @@ -0,0 +1,167 @@ +import { useFetchSupplierOrders } from "@sahil/lib/hooks/suppliers"; +import { useState } from "react"; +import { useGetUserById } from "@sahil/lib/hooks/users"; +import { useSession } from "next-auth/react"; +import { useUserSuppliers } from "@sahil/lib/hooks/useUserOrganizations"; + +import * as React from "react" + +import { CollectionControls } from "@sahil/features/Shared/CollectionControls"; +import { TopClientsByRevenue } from "@sahil/features/Reports/TopClientsByRevenue"; +import { RevenueByProduct } from "@sahil/features/Reports/RevenueByProduct"; +import { OrderStatusDistribution } from "@sahil/features/Reports/OrderStatusDistribution"; + + +const orders = [ + { + "business": { + "id": "e87924e8-69e4-4171-bd89-0c8963e03d08", + "name": "Radisson Blu", + "contactName": "Emmanuel Gatwech", + "business_type": { + "type": "hotel" + }, + "type": "hotel" + }, + "id": "d634372a-6a81-402e-9fa5-231bf7c0444c", + "fulfillment_type": null, + "order_items": [ + { + "price": 15, + "product": { + "name": "Routers", + "price": 10000, + "quantity": 20, + "discount": 0 + } + }, + { + "price": 15, + "product": { + "name": "Laptops", + "price": 1000000, + "quantity": 6, + "discount": 15 + } + }, + { + "price": 15, + "product": { + "name": "iPhone 11 Pro Max", + "price": 10000000, + "quantity": 3, + "discount": 1 + } + } + ], + "status": "PENDING", + "origin": "Souq Munuki", + "created_at": "2024-09-14T13:42:07.748051+00:00", + "destination": "Souq Custom" + }, + { + "business": { + "id": "e87924e8-69e4-4171-bd89-0c8963e03d08", + "name": "Radisson Blu", + "contactName": "Emmanuel Gatwech", + "business_type": { + "type": "hotel" + }, + "type": "hotel" + }, + "id": "c3ce2967-53fc-4fc6-922b-350adf4c773c", + "fulfillment_type": null, + "order_items": [ + { + "price": 15, + "product": { + "name": "1kg Sugar", + "price": 2500, + "quantity": 250, + "discount": null + } + }, + { + "price": 15, + "product": { + "name": "1kg Brazillian Chicken", + "price": 300, + "quantity": 25, + "discount": null + } + }, + { + "price": 15, + "product": { + "name": "1kg Powder Milk", + "price": 2500, + "quantity": 10, + "discount": null + } + } + ], + "status": "PENDING", + "origin": "Souq Munuki", + "created_at": "2024-09-16T18:21:09.961872+00:00", + "destination": "Souq Custom" + }, + { + "business": { + "id": "e87924e8-69e4-4171-bd89-0c8963e03d08", + "name": "Radisson Blu", + "contactName": "Emmanuel Gatwech", + "business_type": { + "type": "hotel" + }, + "type": "hotel" + }, + "id": "99738f85-6c6a-4c79-896d-bea5220108ef", + "fulfillment_type": null, + "order_items": [ + { + "price": 15, + "product": { + "name": "Solar Batteries", + "price": 350000, + "quantity": 10, + "discount": null + } + }, + { + "price": 15, + "product": { + "name": "250w Solar Panel", + "price": 300000, + "quantity": 20, + "discount": null + } + } + ], + "status": "PENDING", + "origin": "Souq Munuki", + "created_at": "2024-09-17T14:41:35.21956+00:00", + "destination": "Souq Custom" + } +]; + + + + +export default function Reports() { + const { data: sessionData } = useSession(); + const { data: currentUser, loading: userLoading } = useGetUserById(sessionData?.user?.id); + + const supplierOrders = useFetchSupplierOrders(); + console.log(supplierOrders); + + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/packages/features/Orders/OrderProgress/OrderProgress.tsx b/packages/features/Orders/OrderProgress/OrderProgress.tsx index ce69b1a1..2d0c8e48 100644 --- a/packages/features/Orders/OrderProgress/OrderProgress.tsx +++ b/packages/features/Orders/OrderProgress/OrderProgress.tsx @@ -1,53 +1,100 @@ -import { Card, Timeline } from "ui"; -import { Orders } from "@sahil/lib/graphql/__generated__/graphql"; -import { formatDateTime } from "@sahil/lib/dates"; -import { allStatuses, statusLabels } from "./constants"; +"use client" + +import { Card } from "ui" +import { Orders } from "@sahil/lib/graphql/__generated__/graphql" +import { formatDateTime } from "@sahil/lib/dates" +import { + HiOutlineCheckCircle, + HiOutlineTruck, + HiOutlineClipboard, + HiOutlineXCircle +} from "react-icons/hi2" type Props = { - order: Orders; -}; + order: Orders +} + +const getStatusIcon = (status: string) => { + switch (status) { + case "PENDING": + return + case "CONFIRMED": + return + case "ENROUTE": + return + case "CANCELED": + return + default: + return + } +} + +const getStatusColor = (status: string) => { + switch (status) { + case "PENDING": + return "text-primary" + case "CONFIRMED": + return "text-success" + case "ENROUTE": + return "text-primary" + case "CANCELED": + return "text-error" + default: + return "text-primary" + } +} + +const getStatusDescription = (status: string) => { + switch (status) { + case "PENDING": + return "Order received, awaiting confirmation." + case "CONFIRMED": + return "Order confirmed, preparing for shipment." + case "ENROUTE": + return "Order in transit, awaiting delivery." + case "CANCELED": + return "Order canceled, we're sorry for any inconvenience." + default: + return "" + } +} export const OrderProgress = ({ order }: Props) => { - const { status_histories } = order; - const timeline = createTimeline(status_histories); + const { status_histories } = order + const sortedHistory = [...status_histories].sort((a, b) => + new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ) + return ( -
- - - +
+ {sortedHistory.map((history, index) => ( +
+
+
+ {getStatusIcon(history.status)} +
+ {index < sortedHistory.length - 1 && ( +
+ )} +
+
+
+ {formatDateTime(history.created_at)} +
+
+ {history.status} +
+
+ {getStatusDescription(history.status)} +
+
+
+ ))}
- ); -}; - -type Timeline = { - prefix: string; - label: (typeof allStatuses)[number] | "CANCELED"; - description: string; - status: "completed" | "pending" | "cancelled"; -}; - -export const createTimeline = (statusHistories: Orders["status_histories"]) => { - const timeline: Timeline[] = statusHistories - .map((history) => { - let status: "completed" | "pending" | "cancelled"; - switch (history.status) { - case "CANCELED": - status = "cancelled"; - break; - case "FULFILLED": - status = "completed"; - break; - default: - status = "completed"; - break; - } - return { - prefix: formatDateTime(history.created_at), - label: history.status, - description: statusLabels[history.status], - status: status, - }; - }) - .reverse(); - return timeline; -}; + ) +} + diff --git a/packages/features/Orders/OrderProgress/UpdateOrderStatusForm.tsx b/packages/features/Orders/OrderProgress/UpdateOrderStatusForm.tsx index fea40c05..7ab0418c 100644 --- a/packages/features/Orders/OrderProgress/UpdateOrderStatusForm.tsx +++ b/packages/features/Orders/OrderProgress/UpdateOrderStatusForm.tsx @@ -1,35 +1,100 @@ -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { Select, Card } from "ui"; +"use client" + +import { useState } from "react" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { Button, Card, IconButton } from "ui" import { Orders, Order_Status_Enum, -} from "@sahil/lib/graphql/__generated__/graphql"; -import { allStatuses } from "./constants"; -import { useAppendOrderStatus } from "@sahil/lib/hooks/orders"; -import { OrderInfoItem } from "../OrderDetails"; -import { formatDateTime } from "@sahil/lib/dates"; -import { HiOutlineHandRaised, HiCalendarDays } from "react-icons/hi2"; -import toast, { Toaster } from "react-hot-toast"; - -const status = z.enum(allStatuses); +} from "@sahil/lib/graphql/__generated__/graphql" +import { useAppendOrderStatus } from "@sahil/lib/hooks/orders" +import { formatDateTime } from "@sahil/lib/dates" +import { + HiOutlineClipboard, + HiOutlineCheckCircle, + HiOutlineTruck, + HiOutlineArchiveBox, + HiOutlineXCircle, + HiCalendarDays, + HiArrowRight, + HiArrowPathRoundedSquare, + HiOutlineInformationCircle +} from "react-icons/hi2" +import toast from "react-hot-toast" const statusSchema = z.object({ - status: status, - // note: z.string().min(10, 'Note must be at least 10 characters.'), -}); + status: z.enum([ + "PENDING", + "CONFIRMED", + "ENROUTE", + "DELIVERED", + "FULFILLED", + "CANCELED" + ]), + note: z.string().optional(), +}) -type FormData = z.infer; +type FormData = z.infer type Props = { - order: Orders; -}; + order: Orders +} + +const STATUS_FLOW = { + PENDING: ["CONFIRMED", "CANCELED"], + CONFIRMED: ["ENROUTE", "CANCELED"], + ENROUTE: ["DELIVERED", "CANCELED"], + DELIVERED: ["FULFILLED", "CANCELED"], + FULFILLED: ["CANCELED"], + CANCELED: [], +} as const + +const STATUS_INFO = { + PENDING: { + icon: HiOutlineClipboard, + color: "text-primary", + bgColor: "bg-primary/10", + description: "Order received, awaiting confirmation" + }, + CONFIRMED: { + icon: HiOutlineCheckCircle, + color: "text-success", + bgColor: "bg-success/10", + description: "Order confirmed, preparing for shipment" + }, + ENROUTE: { + icon: HiOutlineTruck, + color: "text-primary", + bgColor: "bg-primary/10", + description: "Order in transit, awaiting delivery" + }, + DELIVERED: { + icon: HiOutlineArchiveBox, + color: "text-success", + bgColor: "bg-success/10", + description: "Order delivered successfully" + }, + FULFILLED: { + icon: HiOutlineCheckCircle, + color: "text-success", + bgColor: "bg-success/10", + description: "Order completed and fulfilled" + }, + CANCELED: { + icon: HiOutlineXCircle, + color: "text-error", + bgColor: "bg-error/10", + description: "Order has been canceled" + }, +} as const export const UpdateOrderStatusForm = ({ order }: Props) => { - const { appendOrderStatus, loading } = useAppendOrderStatus(); - const isCanceled = - order.status_histories?.[0]?.status === Order_Status_Enum.Canceled; + const { appendOrderStatus, loading } = useAppendOrderStatus() + const [showNote, setShowNote] = useState(false) + const [showStatusInfo, setShowStatusInfo] = useState(false) + const currentStatus = order.status_histories?.[0]?.status || "PENDING" const { register, @@ -37,95 +102,209 @@ export const UpdateOrderStatusForm = ({ order }: Props) => { formState: { errors }, } = useForm({ resolver: zodResolver(statusSchema), - }); - - const orderInfoItems = [ - { - icon: , - label: "Order Date", - value: formatDateTime(order?.created_at), - }, - { - icon: , - label: "Status", - value: order?.status_histories?.[0]?.status ?? "Pending", - }, - ]; + }) - const onSubmit = async (data: FormData) => { - if (loading) return; - const validatedInput = statusSchema.parse(data); + const availableStatuses = STATUS_FLOW[currentStatus as keyof typeof STATUS_FLOW] + const currentStatusInfo = STATUS_INFO[currentStatus as keyof typeof STATUS_INFO] + const onSubmit = async (data: FormData) => { + if (loading) return + try { await appendOrderStatus({ variables: { object: { order_id: order.id, - status: validatedInput.status, + status: data.status, + note: data.note, }, }, - }); - toast.success("Order status updated successfully"); + }) + toast.success("Order status updated successfully") } catch (error) { - console.error("Error appending order status:", error); - toast.error("Order status couldn't be updated, try again later."); + console.error("Error updating order status:", error) + toast.error("Failed to update order status") } - }; + } return ( - <> - + +
+ {/* Current Status */}
-
-

Current Order Status

-
- {orderInfoItems.map((item, index) => ( - +
+
+ - ))} +
+
+
+ {currentStatus} +
+
+ {currentStatusInfo.description} +
+
+ Last updated: {formatDateTime(order.status_histories?.[0]?.created_at)} +
+
+
- -

Manage Order Status

- -
- +
+ +
+
+
{status}
+
+ {statusInfo.description} +
+
+ + ) + })} +
- {!isCanceled && ( + {showNote && ( +
+ +