From 7c9bc1f78642fe28f9119e438b0e2b64884178da Mon Sep 17 00:00:00 2001 From: tedraykov Date: Mon, 8 Jul 2024 17:48:27 +0300 Subject: [PATCH] add core return to orders --- app/account/(orderId)/orders/[id]/page.tsx | 6 + app/account/(orders)/page.tsx | 8 +- components/divider.tsx | 11 +- components/form/combobox-button.tsx | 18 ++ components/form/combobox-input.tsx | 21 +++ components/form/combobox-option.tsx | 16 ++ components/form/combobox-options.tsx | 18 ++ components/orders/actions.tsx | 60 +++++++ components/orders/activate-warranty.tsx | 9 +- components/orders/core-return-modal.tsx | 93 ++++++++++ components/orders/core-return-status.tsx | 8 + components/orders/core-return.tsx | 15 ++ .../orders/order-confirmation-modal.tsx | 4 +- components/orders/order-confirmation-pdf.tsx | 4 +- .../orders/order-confirmation-status.tsx | 8 + components/orders/order-statuses.tsx | 14 ++ components/orders/payment-details.tsx | 4 +- .../orders/warranty-activated-badge.tsx | 12 -- .../orders/warranty-activated-status.tsx | 21 +++ components/states-combobox.tsx | 73 ++++++++ components/ui/card.tsx | 2 +- components/ui/chip.tsx | 63 +++++++ components/ui/combobox.tsx | 163 ++++++++++++++++++ components/ui/heading.tsx | 2 +- components/ui/index.ts | 2 + components/ui/input.tsx | 7 +- components/ui/text.tsx | 10 +- lib/shopify/auth.ts | 7 - lib/shopify/fragments/order-metafields.ts | 27 +++ lib/shopify/index.ts | 43 ++++- lib/shopify/types.ts | 24 ++- lib/utils.ts | 6 +- 32 files changed, 722 insertions(+), 57 deletions(-) create mode 100644 components/form/combobox-button.tsx create mode 100644 components/form/combobox-input.tsx create mode 100644 components/form/combobox-option.tsx create mode 100644 components/form/combobox-options.tsx create mode 100644 components/orders/core-return-modal.tsx create mode 100644 components/orders/core-return-status.tsx create mode 100644 components/orders/core-return.tsx create mode 100644 components/orders/order-confirmation-status.tsx create mode 100644 components/orders/order-statuses.tsx delete mode 100644 components/orders/warranty-activated-badge.tsx create mode 100644 components/orders/warranty-activated-status.tsx create mode 100644 components/states-combobox.tsx create mode 100644 components/ui/chip.tsx create mode 100644 components/ui/combobox.tsx diff --git a/app/account/(orderId)/orders/[id]/page.tsx b/app/account/(orderId)/orders/[id]/page.tsx index 9d889d95d..59241fcae 100644 --- a/app/account/(orderId)/orders/[id]/page.tsx +++ b/app/account/(orderId)/orders/[id]/page.tsx @@ -14,6 +14,9 @@ import { toPrintDate } from 'lib/utils'; import Image from 'next/image'; import Link from 'next/link'; import ActivateWarranty from 'components/orders/activate-warranty'; +import OrderStatuses from 'components/orders/order-statuses'; +import Divider from 'components/divider'; +import { CoreReturn } from 'components/orders/core-return'; function Unfulfilled({ order }: { order: Order }) { // Build a map of line item IDs to quantities fulfilled @@ -222,10 +225,13 @@ export default async function OrderPage({ params }: { params: { id: string } })
+ +
+
diff --git a/app/account/(orders)/page.tsx b/app/account/(orders)/page.tsx index 9cce684f0..1de53e29f 100644 --- a/app/account/(orders)/page.tsx +++ b/app/account/(orders)/page.tsx @@ -1,7 +1,9 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline'; +import Divider from 'components/divider'; import ActivateWarranty from 'components/orders/activate-warranty'; import MobileOrderActions from 'components/orders/mobile-order-actions'; import OrderConfirmation from 'components/orders/order-confirmation'; +import OrderStatuses from 'components/orders/order-statuses'; import OrdersHeader from 'components/orders/orders-header'; import Price from 'components/price'; import { Button } from 'components/ui'; @@ -28,7 +30,7 @@ export default async function AccountPage() {

Order placed on

-
+
Order
@@ -66,6 +68,10 @@ export default async function AccountPage() {
+
+ +
+

Items

    diff --git a/components/divider.tsx b/components/divider.tsx index 944f7378d..f9adc72a3 100644 --- a/components/divider.tsx +++ b/components/divider.tsx @@ -12,7 +12,7 @@ const divider = tv({ element: 'w-full h-[1px] ' }, vertical: { - root: 'flex justify-between items-stretch text-tremor-default text-tremor-content', + root: 'flex justify-between items-stretch text-tremor-default text-tremor-content h-full', element: 'h-full w-[1px]' } }, @@ -42,12 +42,17 @@ const divider = tv({ type DividerProps = { orientation?: 'horizontal' | 'vertical'; hasSpacing?: boolean; + className?: string; }; -export default function Divider({ orientation = 'horizontal', hasSpacing = true }: DividerProps) { +export default function Divider({ + orientation = 'horizontal', + hasSpacing = true, + className +}: DividerProps) { const { root, element } = divider({ orientation, hasSpacing }); return ( -
    +
    ); diff --git a/components/form/combobox-button.tsx b/components/form/combobox-button.tsx new file mode 100644 index 000000000..b6c3a90fb --- /dev/null +++ b/components/form/combobox-button.tsx @@ -0,0 +1,18 @@ +import { ComboboxButtonProps, ComboboxButton as HeadlessComboboxButton } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/16/solid'; +import clsx from 'clsx'; + +export default function ComboboxButton({ className, ...props }: ComboboxButtonProps) { + return ( + + + + ); +} diff --git a/components/form/combobox-input.tsx b/components/form/combobox-input.tsx new file mode 100644 index 000000000..33a34e1e2 --- /dev/null +++ b/components/form/combobox-input.tsx @@ -0,0 +1,21 @@ +import { ComboboxInputProps } from '@headlessui/react'; +import clsx from 'clsx'; + +export default function ComboboxInput({ className, ...props }: ComboboxInputProps) { + return ( + + ); +} diff --git a/components/form/combobox-option.tsx b/components/form/combobox-option.tsx new file mode 100644 index 000000000..70413f31a --- /dev/null +++ b/components/form/combobox-option.tsx @@ -0,0 +1,16 @@ +import { ComboboxOption as HeadlessCombobox, ComboboxOptionProps } from '@headlessui/react'; +import clsx from 'clsx'; + +export default function ComboboxOption({ className, ...props }: ComboboxOptionProps) { + return ( + + ); +} diff --git a/components/form/combobox-options.tsx b/components/form/combobox-options.tsx new file mode 100644 index 000000000..0de5309c1 --- /dev/null +++ b/components/form/combobox-options.tsx @@ -0,0 +1,18 @@ +import { + ComboboxOptionsProps, + ComboboxOptions as HeadlessComboboxOptions +} from '@headlessui/react'; +import clsx from 'clsx'; + +export default function ComboboxOption({ className, ...props }: ComboboxOptionsProps) { + return ( + + ); +} diff --git a/components/orders/actions.tsx b/components/orders/actions.tsx index 56b2d3ca8..928a9e345 100644 --- a/components/orders/actions.tsx +++ b/components/orders/actions.tsx @@ -164,3 +164,63 @@ export const confirmOrder = async ({ order, content, formData }: ConfirmOrderOpt console.log('activateWarranty action', error); } }; + +export async function returnCore(order: Order, formData: FormData) { + const rawFormData = [ + getMetafieldValue( + 'coreReturnZip', + { + key: 'warranty_activation_odometer', + value: formData.get('name') as string | null, + type: 'file_reference' + }, + order + ), + getMetafieldValue( + 'warrantyActivationInstallation', + { + key: 'warranty_activation_installation', + value: installationFileId, + type: 'file_reference' + }, + order + ), + getMetafieldValue( + 'warrantyActivationSelfInstall', + { + key: 'warranty_activation_self_install', + value: formData.get('warranty_activation_self_install') === 'on' ? 'true' : 'false', + type: 'boolean' + }, + order + ), + getMetafieldValue( + 'warrantyActivationMileage', + { + key: 'warranty_activation_mileage', + value: formData.get('warranty_activation_mileage') as string | null, + type: 'number_integer' + }, + order + ), + getMetafieldValue( + 'warrantyActivationVIN', + { + key: 'warranty_activation_vin', + value: formData.get('warranty_activation_vin') as string | null, + type: 'single_line_text_field' + }, + order + ) + ]; + try { + await updateOrderMetafields({ + orderId: order.id, + metafields: rawFormData + }); + + revalidateTag(TAGS.orderMetafields); + } catch (error) { + console.log('activateWarranty action', error); + } +} diff --git a/components/orders/activate-warranty.tsx b/components/orders/activate-warranty.tsx index c894ea588..ac99c1b82 100644 --- a/components/orders/activate-warranty.tsx +++ b/components/orders/activate-warranty.tsx @@ -4,7 +4,6 @@ import { Order, WarrantyStatus } from 'lib/shopify/types'; import { isBeforeToday } from 'lib/utils'; import { useState } from 'react'; import ActivateWarrantyModal from './activate-warranty-modal'; -import WarrantyActivatedBadge from './warranty-activated-badge'; import { Button } from 'components/ui'; type ActivateWarrantyModalProps = { @@ -13,7 +12,7 @@ type ActivateWarrantyModalProps = { const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => { const [isOpen, setIsOpen] = useState(false); - const isWarrantyActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated; + const isActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated; const isPassDeadline = isBeforeToday(order?.warrantyActivationDeadline?.value); const isOrderConfirmed = order?.orderConfirmation?.value; @@ -21,11 +20,7 @@ const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => { return null; } - if (isWarrantyActivated) { - return ; - } - - if (isPassDeadline) { + if (isPassDeadline || isActivated) { return null; } diff --git a/components/orders/core-return-modal.tsx b/components/orders/core-return-modal.tsx new file mode 100644 index 000000000..56321ab0a --- /dev/null +++ b/components/orders/core-return-modal.tsx @@ -0,0 +1,93 @@ +import { Dialog, DialogBackdrop, DialogPanel, Fieldset, Legend } from '@headlessui/react'; +import { Order } from 'lib/shopify/types'; +import { Button, Heading, Input } from 'components/ui'; +import StatesCombobox from 'components/states-combobox'; +import { useTransition } from 'react'; +import { returnCore } from './actions'; + +export function CoreReturnModal({ + isOpen, + onClose, + order +}: { + isOpen: boolean; + onClose: () => void; + order: Order; +}) { + const [submitting, startTransition] = useTransition(); + + async function submitCoreReturn(formData: FormData) { + startTransition(async () => { + returnCore(order, formData); + }); + } + + return ( + + +
    +
    + +
    + Core Return + + Order {order.name} + +
    +
    +
    + + Core Pickup Address + + + + + + + + +
    +
    + + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/components/orders/core-return-status.tsx b/components/orders/core-return-status.tsx new file mode 100644 index 000000000..c969c6395 --- /dev/null +++ b/components/orders/core-return-status.tsx @@ -0,0 +1,8 @@ +import { Chip } from 'components/ui'; +import { Order } from 'lib/shopify/types'; + +export default function CoreReturnStatus({ order }: { order: Order }) { + if (!order.coreReturnStatus?.value) return null; + + return Core Return: {order.coreReturnStatus.value}; +} diff --git a/components/orders/core-return.tsx b/components/orders/core-return.tsx new file mode 100644 index 000000000..da6dcfba6 --- /dev/null +++ b/components/orders/core-return.tsx @@ -0,0 +1,15 @@ +'use client'; +import { Button } from 'components/ui'; +import { Order } from 'lib/shopify/types'; +import { useState } from 'react'; +import { CoreReturnModal } from './core-return-modal'; + +export function CoreReturn({ order }: { order: Order }) { + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + setIsOpen(false)} /> + + ); +} diff --git a/components/orders/order-confirmation-modal.tsx b/components/orders/order-confirmation-modal.tsx index 892f17a1a..b94d9d48b 100644 --- a/components/orders/order-confirmation-modal.tsx +++ b/components/orders/order-confirmation-modal.tsx @@ -8,7 +8,7 @@ import Markdown from 'markdown-to-jsx'; import { Order, OrderConfirmationContent } from 'lib/shopify/types'; import { FormEventHandler, useEffect, useRef, useState, useTransition } from 'react'; import { confirmOrder, fetchOrderConfirmationContent } from 'components/orders/actions'; -import { Button, Heading, Text, Label, Input, Skeleton } from 'components/ui'; +import { Button, Heading, Text, Label, Input } from 'components/ui'; import LoadingDots from 'components/loading-dots'; function OrderConfirmationDetails({ @@ -228,7 +228,7 @@ export default function OrderConfirmationModal({
    diff --git a/components/orders/order-confirmation-pdf.tsx b/components/orders/order-confirmation-pdf.tsx index 88f53fd6e..bc834a259 100644 --- a/components/orders/order-confirmation-pdf.tsx +++ b/components/orders/order-confirmation-pdf.tsx @@ -156,7 +156,9 @@ export default function OrderConfirmationPdf({ Payment - Ending with {order.transactions[0]!.paymentDetails.last4} - + {order.transactions[0]?.paymentDetails + ? `Ending with ${order.transactions[0]!.paymentDetails.last4} - ` + : 'Manual - '} Order Not Confirmed; +} diff --git a/components/orders/order-statuses.tsx b/components/orders/order-statuses.tsx new file mode 100644 index 000000000..4620ebd84 --- /dev/null +++ b/components/orders/order-statuses.tsx @@ -0,0 +1,14 @@ +import { Order } from 'lib/shopify/types'; +import CoreReturnStatus from './core-return-status'; +import WarrantyActivatedStatus from './warranty-activated-status'; +import OrderConfirmedStatus from './order-confirmation-status'; + +export default function OrderStatuses({ order, className }: { order: Order; className?: string }) { + return ( +
    + + + +
    + ); +} diff --git a/components/orders/payment-details.tsx b/components/orders/payment-details.tsx index 0284757c5..2f303212a 100644 --- a/components/orders/payment-details.tsx +++ b/components/orders/payment-details.tsx @@ -20,7 +20,9 @@ export default function PaymentsDetails({ order, hideIcon }: { order: Order; hid
    - Ending with {transaction.paymentDetails.last4} - + {transaction?.paymentDetails + ? `Ending with ${transaction.paymentDetails.last4} - ` + : 'Manual - '} { - return ( - - - ); -}; - -export default WarrantyActivatedBadge; diff --git a/components/orders/warranty-activated-status.tsx b/components/orders/warranty-activated-status.tsx new file mode 100644 index 000000000..4568ca3bb --- /dev/null +++ b/components/orders/warranty-activated-status.tsx @@ -0,0 +1,21 @@ +import Chip, { ChipProps } from 'components/ui/chip'; +import { Order, WarrantyStatus } from 'lib/shopify/types'; + +const WarrantyActivatedStatus = ({ order }: { order: Order }) => { + const warrantyStatus = order?.warrantyStatus?.value; + const isOrderConfirmed = order?.orderConfirmation?.value; + + if (!isOrderConfirmed || !warrantyStatus) { + return null; + } + + let level: ChipProps['level'] = 'success'; + + if (warrantyStatus === WarrantyStatus.NotActivated) { + level = 'warn'; + } + + return Warranty: {warrantyStatus}; +}; + +export default WarrantyActivatedStatus; diff --git a/components/states-combobox.tsx b/components/states-combobox.tsx new file mode 100644 index 000000000..291deb8c1 --- /dev/null +++ b/components/states-combobox.tsx @@ -0,0 +1,73 @@ +import Combobox from './ui/combobox'; + +const states = [ + { name: 'Alabama', code: 'AL' }, + { name: 'Alaska', code: 'AK' }, + { name: 'Arizona', code: 'AZ' }, + { name: 'Arkansas', code: 'AR' }, + { name: 'California', code: 'CA' }, + { name: 'Colorado', code: 'CO' }, + { name: 'Connecticut', code: 'CT' }, + { name: 'Delaware', code: 'DE' }, + { name: 'Florida', code: 'FL' }, + { name: 'Georgia', code: 'GA' }, + { name: 'Hawaii', code: 'HI' }, + { name: 'Idaho', code: 'ID' }, + { name: 'Illinois', code: 'IL' }, + { name: 'Indiana', code: 'IN' }, + { name: 'Iowa', code: 'IA' }, + { name: 'Kansas', code: 'KS' }, + { name: 'Kentucky', code: 'KY' }, + { name: 'Louisiana', code: 'LA' }, + { name: 'Maine', code: 'ME' }, + { name: 'Maryland', code: 'MD' }, + { name: 'Massachusetts', code: 'MA' }, + { name: 'Michigan', code: 'MI' }, + { name: 'Minnesota', code: 'MN' }, + { name: 'Mississippi', code: 'MS' }, + { name: 'Missouri', code: 'MO' }, + { name: 'Montana', code: 'MT' }, + { name: 'Nebraska', code: 'NE' }, + { name: 'Nevada', code: 'NV' }, + { name: 'New Hampshire', code: 'NH' }, + { name: 'New Jersey', code: 'NJ' }, + { name: 'New Mexico', code: 'NM' }, + { name: 'New York', code: 'NY' }, + { name: 'North Carolina', code: 'NC' }, + { name: 'North Dakota', code: 'ND' }, + { name: 'Ohio', code: 'OH' }, + { name: 'Oklahoma', code: 'OK' }, + { name: 'Oregon', code: 'OR' }, + { name: 'Pennsylvania', code: 'PA' }, + { name: 'Rhode Island', code: 'RI' }, + { name: 'South Carolina', code: 'SC' }, + { name: 'South Dakota', code: 'SD' }, + { name: 'Tennessee', code: 'TN' }, + { name: 'Texas', code: 'TX' }, + { name: 'Utah', code: 'UT' }, + { name: 'Vermont', code: 'VT' }, + { name: 'Virginia', code: 'VA' }, + { name: 'Washington', code: 'WA' }, + { name: 'West Virginia', code: 'WV' }, + { name: 'Wisconsin', code: 'WI' }, + { name: 'Wyoming', code: 'WY' } +]; + +function findState(code: string) { + return states.find((state) => state.code === code); +} + +export default function StatesCombobox({ defaultStateCode }: { defaultStateCode: string }) { + return ( + + ); +} diff --git a/components/ui/card.tsx b/components/ui/card.tsx index bb65e3345..9b2275649 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -7,7 +7,7 @@ const cardStyles = tv({ base: 'rounded p-6 text-left w-full', variants: { outlined: { - true: 'border bg-white', + true: 'border bg-white shadow-sm', false: '' }, elevated: { diff --git a/components/ui/chip.tsx b/components/ui/chip.tsx new file mode 100644 index 000000000..b7ba93d07 --- /dev/null +++ b/components/ui/chip.tsx @@ -0,0 +1,63 @@ +import { CheckCircleIcon, ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/24/solid'; +import clsx from 'clsx'; +import { VariantProps, tv } from 'tailwind-variants'; + +const chip = tv({ + slots: { + root: 'inline-flex items-center gap-x-2 rounded-md px-2.5 py-2 text-sm font-medium ring-1 ring-inset', + leadingIcon: 'h-5 w-5' + }, + variants: { + level: { + success: { + root: 'bg-green-50 text-green-700 ring-green-600/20' + }, + warn: { + root: 'bg-yellow-50 text-yellow-700 ring-yellow-600/20' + }, + info: { + root: 'bg-content/5 text-content-emphasis ring-content/20' + }, + error: { + root: 'bg-red-50 text-red-700 ring-red-600/20' + } + } + } +}); + +export interface LevelLeadingProps extends VariantProps { + className?: string; +} + +export interface ChipProps extends VariantProps { + children: React.ReactNode; + className?: string; +} + +function LevelLeadingIcon({ level, className }: LevelLeadingProps) { + if (level === 'success') { + return