From 6c01d8825df7ed5495cd260952093a54c6a65b6d Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 27 Jun 2024 15:05:59 +0700 Subject: [PATCH] allow customer to check on self installed field --- .vscode/settings.json | 3 +- components/checkbox.tsx | 28 ------- components/form/checkbox-field.tsx | 25 ++++++ components/form/file-input/actions.ts | 14 +++- components/form/file-input/index.tsx | 26 ++++++- .../form/{input.tsx => input-field.tsx} | 2 + components/orders/actions.ts | 77 +++++++++++++++---- components/orders/activate-warranty-modal.tsx | 46 +++++++++-- components/orders/activate-warranty.tsx | 15 ++-- components/orders/mobile-order-actions.tsx | 15 ++-- lib/constants.ts | 2 +- lib/shopify/fragments/order.ts | 38 +++++++++ lib/shopify/index.ts | 52 +++++++------ lib/shopify/queries/node.ts | 12 +++ lib/shopify/types.ts | 28 +++++-- package.json | 1 - 16 files changed, 288 insertions(+), 96 deletions(-) delete mode 100644 components/checkbox.tsx create mode 100644 components/form/checkbox-field.tsx rename components/form/{input.tsx => input-field.tsx} (97%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8345c107c..10c6975cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "source.fixAll": "explicit", "source.organizeImports": "explicit", "source.sortMembers": "explicit" - } + }, + "cSpell.words": ["Metafield", "Metafields"] } diff --git a/components/checkbox.tsx b/components/checkbox.tsx deleted file mode 100644 index 197cb3b1d..000000000 --- a/components/checkbox.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { CheckIcon } from '@heroicons/react/24/outline'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import { cn } from 'lib/utils'; -import { forwardRef } from 'react'; - -const Checkbox = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)); - -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/components/form/checkbox-field.tsx b/components/form/checkbox-field.tsx new file mode 100644 index 000000000..9d017811c --- /dev/null +++ b/components/form/checkbox-field.tsx @@ -0,0 +1,25 @@ +import { Checkbox, CheckboxProps, Field, Label } from '@headlessui/react'; +import { CheckIcon } from '@heroicons/react/24/solid'; + +type CheckboxFieldProps = CheckboxProps & { + label: string; + name: string; +}; + +const CheckboxField = ({ label, name, ...props }: CheckboxFieldProps) => { + return ( + + + {/* Checkmark icon */} + + + + + ); +}; + +export default CheckboxField; diff --git a/components/form/file-input/actions.ts b/components/form/file-input/actions.ts index 3f8e3a914..c69819e53 100644 --- a/components/form/file-input/actions.ts +++ b/components/form/file-input/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { createFile, stageUploadFile, uploadFile } from 'lib/shopify'; -import { StagedUploadsCreatePayload, UploadInput } from 'lib/shopify/types'; +import { createFile, getFile, stageUploadFile, uploadFile } from 'lib/shopify'; +import { File as ShopifyFile, StagedUploadsCreatePayload, UploadInput } from 'lib/shopify/types'; const prepareFilePayload = ({ stagedFileUpload, @@ -84,3 +84,13 @@ export const handleUploadFile = async ({ file }: { file: File }) => { console.log('handleUploadFile action', error); } }; + +export const getFileDetails = async (fileId?: string | null): Promise => { + if (!fileId) return undefined; + try { + const file = await getFile(fileId); + return file; + } catch (error) { + console.log('getFileDetails action', error); + } +}; diff --git a/components/form/file-input/index.tsx b/components/form/file-input/index.tsx index f55be2281..cceada2e4 100644 --- a/components/form/file-input/index.tsx +++ b/components/form/file-input/index.tsx @@ -1,14 +1,23 @@ +'use client'; + import { PhotoIcon } from '@heroicons/react/24/outline'; -import { ChangeEvent, useId, useState } from 'react'; +import LoadingDots from 'components/loading-dots'; +import { File as ShopifyFile } from 'lib/shopify/types'; +import { ChangeEvent, useEffect, useId, useState, useTransition } from 'react'; +import { getFileDetails } from './actions'; type FileInputProps = { name: string; label: string; + fileId?: string | null; }; -const FileInput = ({ name, label }: FileInputProps) => { +const FileInput = ({ name, label, fileId }: FileInputProps) => { const id = useId(); const [file, setFile] = useState(); + const [defaultFileDetails, setDefaultFileDetails] = useState(); + + const [loading, startTransition] = useTransition(); const onFileChange = (e: ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { @@ -16,6 +25,15 @@ const FileInput = ({ name, label }: FileInputProps) => { } }; + useEffect(() => { + if (!fileId) return; + + startTransition(async () => { + const fileResponse = await getFileDetails(fileId); + setDefaultFileDetails(fileResponse); + }); + }, [fileId]); + return (
@@ -34,7 +52,9 @@ const FileInput = ({ name, label }: FileInputProps) => {
- {file &&

{file.name}

} +

+ {loading ? : file?.name || defaultFileDetails?.alt} +

); }; diff --git a/components/form/input.tsx b/components/form/input-field.tsx similarity index 97% rename from components/form/input.tsx rename to components/form/input-field.tsx index 55dfe1204..678aed6d3 100644 --- a/components/form/input.tsx +++ b/components/form/input-field.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Field, Input as HeadlessInput, Label } from '@headlessui/react'; import { InputHTMLAttributes } from 'react'; diff --git a/components/orders/actions.ts b/components/orders/actions.ts index c5158103c..4fad38bfe 100644 --- a/components/orders/actions.ts +++ b/components/orders/actions.ts @@ -3,9 +3,24 @@ import { handleUploadFile } from 'components/form/file-input/actions'; import { TAGS } from 'lib/constants'; import { updateOrderMetafields } from 'lib/shopify'; +import { ShopifyOrderMetafield, UpdateOrderMetafieldInput } from 'lib/shopify/types'; import { revalidateTag } from 'next/cache'; -export const activateWarranty = async (orderId: string, formData: FormData) => { +const getMetafieldValue = ( + key: keyof ShopifyOrderMetafield, + newValue: { value?: string | null; type: string; key: string }, + orderMetafields?: ShopifyOrderMetafield +): UpdateOrderMetafieldInput => { + return orderMetafields?.[key]?.id + ? { id: orderMetafields[key]?.id!, value: newValue.value, key: newValue.key } + : { ...newValue, namespace: 'custom' }; +}; + +export const activateWarranty = async ( + orderId: string, + formData: FormData, + orderMetafields?: ShopifyOrderMetafield +) => { let odometerFileId = null; let installationFileId = null; const odometerFile = formData.get('warranty_activation_odometer'); @@ -17,20 +32,54 @@ export const activateWarranty = async (orderId: string, formData: FormData) => { if (installationFile) { installationFileId = await handleUploadFile({ file: installationFile as File }); } - + console.log(formData.get('warranty_activation_self_install')); + // https://shopify.dev/docs/api/admin-graphql/2024-01/mutations/orderUpdate const rawFormData = [ - { key: 'warranty_activation_odometer', value: odometerFileId, type: 'file_reference' }, - { key: 'warranty_activation_installation', value: installationFileId, type: 'file_reference' }, - { - key: 'warranty_activation_mileage', - value: formData.get('warranty_activation_mileage') as string | null, - type: 'number_integer' - }, - { - key: 'warranty_activation_vin', - value: formData.get('warranty_activation_vin') as string | null, - type: 'single_line_text_field' - } + getMetafieldValue( + 'warrantyActivationOdometer', + { + key: 'warranty_activation_odometer', + value: odometerFileId, + type: 'file_reference' + }, + orderMetafields + ), + getMetafieldValue( + 'warrantyActivationInstallation', + { + key: 'warranty_activation_installation', + value: installationFileId, + type: 'file_reference' + }, + orderMetafields + ), + getMetafieldValue( + 'warrantyActivationSelfInstall', + { + key: 'warranty_activation_self_install', + value: formData.get('warranty_activation_self_install') === 'on' ? 'true' : 'false', + type: 'boolean' + }, + orderMetafields + ), + getMetafieldValue( + 'warrantyActivationMileage', + { + key: 'warranty_activation_mileage', + value: formData.get('warranty_activation_mileage') as string | null, + type: 'number_integer' + }, + orderMetafields + ), + getMetafieldValue( + 'warrantyActivationVIN', + { + key: 'warranty_activation_vin', + value: formData.get('warranty_activation_vin') as string | null, + type: 'single_line_text_field' + }, + orderMetafields + ) ]; try { diff --git a/components/orders/activate-warranty-modal.tsx b/components/orders/activate-warranty-modal.tsx index ab7356d19..a06264afe 100644 --- a/components/orders/activate-warranty-modal.tsx +++ b/components/orders/activate-warranty-modal.tsx @@ -2,9 +2,11 @@ import { Button, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'; import clsx from 'clsx'; +import CheckboxField from 'components/form/checkbox-field'; import FileInput from 'components/form/file-input'; -import Input from 'components/form/input'; +import Input from 'components/form/input-field'; import LoadingDots from 'components/loading-dots'; +import { ShopifyOrderMetafield } from 'lib/shopify/types'; import { FormEventHandler, useRef, useTransition } from 'react'; import { activateWarranty } from './actions'; @@ -12,9 +14,15 @@ type ActivateWarrantyModalProps = { isOpen: boolean; onClose: () => void; orderId: string; + orderMetafields?: ShopifyOrderMetafield; }; -function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyModalProps) { +function ActivateWarrantyModal({ + onClose, + isOpen, + orderId, + orderMetafields +}: ActivateWarrantyModalProps) { const [pending, startTransition] = useTransition(); const formRef = useRef(null); @@ -25,7 +33,7 @@ function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyMod const formData = new FormData(form); startTransition(async () => { - await activateWarranty(orderId, formData); + await activateWarranty(orderId, formData, orderMetafields); form.reset(); onClose(); }); @@ -48,10 +56,32 @@ function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyMod Activate Warranty
- - - - + + + + +
diff --git a/components/orders/activate-warranty.tsx b/components/orders/activate-warranty.tsx index a8f9bb707..b98c4cac2 100644 --- a/components/orders/activate-warranty.tsx +++ b/components/orders/activate-warranty.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Order, OrderMetafield, WarrantyStatus } from 'lib/shopify/types'; +import { Order, ShopifyOrderMetafield, WarrantyStatus } from 'lib/shopify/types'; import { isBeforeToday } from 'lib/utils'; import { useState } from 'react'; import ActivateWarrantyModal from './activate-warranty-modal'; @@ -8,13 +8,13 @@ import WarrantyActivatedBadge from './warranty-activated-badge'; type ActivateWarrantyModalProps = { order: Order; - orderMetafields?: OrderMetafield; + orderMetafields?: ShopifyOrderMetafield; }; const ActivateWarranty = ({ order, orderMetafields }: ActivateWarrantyModalProps) => { const [isOpen, setIsOpen] = useState(false); - const isWarrantyActivated = orderMetafields?.warrantyStatus === WarrantyStatus.Activated; - const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline); + const isWarrantyActivated = orderMetafields?.warrantyStatus?.value === WarrantyStatus.Activated; + const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline?.value); if (isWarrantyActivated) { return ; @@ -32,7 +32,12 @@ const ActivateWarranty = ({ order, orderMetafields }: ActivateWarrantyModalProps > Activate Warranty - setIsOpen(false)} orderId={order.id} /> + setIsOpen(false)} + orderId={order.id} + orderMetafields={orderMetafields} + /> ); }; diff --git a/components/orders/mobile-order-actions.tsx b/components/orders/mobile-order-actions.tsx index b3b3b2c90..e49b01463 100644 --- a/components/orders/mobile-order-actions.tsx +++ b/components/orders/mobile-order-actions.tsx @@ -3,7 +3,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import clsx from 'clsx'; -import { Order, OrderMetafield, WarrantyStatus } from 'lib/shopify/types'; +import { Order, ShopifyOrderMetafield, WarrantyStatus } from 'lib/shopify/types'; import { isBeforeToday } from 'lib/utils'; import Link from 'next/link'; import { useState } from 'react'; @@ -14,11 +14,11 @@ const MobileOrderActions = ({ orderMetafields }: { order: Order; - orderMetafields?: OrderMetafield; + orderMetafields?: ShopifyOrderMetafield; }) => { const [isOpen, setIsOpen] = useState(false); - const isWarrantyActivated = orderMetafields?.warrantyStatus === WarrantyStatus.Activated; - const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline); + const isWarrantyActivated = orderMetafields?.warrantyStatus?.value === WarrantyStatus.Activated; + const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline?.value); return ( <> @@ -66,7 +66,12 @@ const MobileOrderActions = ({ - setIsOpen(false)} orderId={order.id} /> + setIsOpen(false)} + orderId={order.id} + orderMetafields={orderMetafields} + /> ); }; diff --git a/lib/constants.ts b/lib/constants.ts index 793f90552..5029b37ba 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -64,7 +64,7 @@ export const ADD_ON_PRODUCT_TYPES = { export const WARRANTY_FIELDS = [ 'warranty_activation_odometer', - 'warranty_activation_installation', + ['warranty_activation_installation', 'warranty_activation_self_install'], 'warranty_activation_vin', 'warranty_activation_mileage' ]; diff --git a/lib/shopify/fragments/order.ts b/lib/shopify/fragments/order.ts index 9ed12a895..d15409862 100644 --- a/lib/shopify/fragments/order.ts +++ b/lib/shopify/fragments/order.ts @@ -33,12 +33,50 @@ export const orderMetafields = /* GraphQL */ ` id warrantyStatus: metafield(namespace: "custom", key: "warranty_status") { value + id + key } warrantyActivationDeadline: metafield( namespace: "custom" key: "warranty_activation_deadline" ) { value + id + key + } + warrantyActivationOdometer: metafield( + namespace: "custom" + key: "warranty_activation_odometer" + ) { + value + id + key + } + warrantyActivationInstallation: metafield( + namespace: "custom" + key: "warranty_activation_installation" + ) { + value + id + key + } + warrantyActivationSelfInstall: metafield( + namespace: "custom" + key: "warranty_activation_self_install" + ) { + value + id + key + } + warrantyActivationVIN: metafield(namespace: "custom", key: "warranty_activation_vin") { + value + id + key + } + warrantyActivationMileage: metafield(namespace: "custom", key: "warranty_activation_mileage") { + value + id + key } } `; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 33f8075c4..005d70a76 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -37,7 +37,7 @@ import { import { getCustomerQuery } from './queries/customer'; import { getMenuQuery } from './queries/menu'; import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject'; -import { getImageQuery, getMetaobjectsByIdsQuery } from './queries/node'; +import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node'; import { getCustomerOrderQuery, getOrderMetafieldsQuery } from './queries/order'; import { getCustomerOrderMetafieldsQuery, getCustomerOrdersQuery } from './queries/orders'; import { getPageQuery, getPagesQuery } from './queries/page'; @@ -53,6 +53,7 @@ import { Collection, Connection, Customer, + File, FileCreateInput, Filter, Fulfillment, @@ -63,7 +64,6 @@ import { Metaobject, Money, Order, - OrderMetafield, Page, PageInfo, Product, @@ -105,6 +105,7 @@ import { ShopifyUpdateOrderMetafieldsOperation, Transaction, TransmissionType, + UpdateOrderMetafieldInput, UploadInput, WarrantyStatus } from './types'; @@ -1092,17 +1093,14 @@ export const updateOrderMetafields = async ({ metafields }: { orderId: string; - metafields: { key: string; value: string | undefined | null; type: string }[]; + metafields: Array; }) => { - const validMetafields = ( - metafields.filter((field) => Boolean(field)) as Array> - ).map((field) => ({ - ...field, - namespace: 'custom' - })); + const validMetafields = metafields.filter((field) => Boolean(field.value)) as Array; + + if (validMetafields.length === 0) return null; const shouldSetWarrantyStatusToActivated = WARRANTY_FIELDS.every((field) => - validMetafields.find(({ key }) => key === field) + validMetafields.find(({ key }) => (Array.isArray(field) ? field.includes(key) : key === field)) ); const response = await adminFetch({ @@ -1127,7 +1125,7 @@ export const updateOrderMetafields = async ({ return response.body.data.orderUpdate.order.id; }; -export const getOrdersMetafields = async (): Promise<{ [key: string]: OrderMetafield }> => { +export const getOrdersMetafields = async (): Promise<{ [key: string]: ShopifyOrderMetafield }> => { const customer = await getCustomer(); const res = await adminFetch<{ data: { @@ -1153,16 +1151,13 @@ export const getOrdersMetafields = async (): Promise<{ [key: string]: OrderMetaf return res.body.data.customer.orders.nodes.reduce( (acc, order) => ({ ...acc, - [order.id]: { - warrantyStatus: order.warrantyStatus?.value ?? null, - warrantyActivationDeadline: order.warrantyActivationDeadline?.value ?? null - } + [order.id]: order }), - {} as { [key: string]: OrderMetafield } + {} as { [key: string]: ShopifyOrderMetafield } ); }; -export const getOrderMetafields = async (orderId: string): Promise => { +export const getOrderMetafields = async (orderId: string): Promise => { const res = await adminFetch<{ data: { order: { @@ -1175,13 +1170,26 @@ export const getOrderMetafields = async (orderId: string): Promise({ query: getOrderMetafieldsQuery, variables: { id: `gid://shopify/Order/${orderId}` }, - tags: [`${TAGS.orderMetafields}/${orderId}`] + tags: [TAGS.orderMetafields] }); const order = res.body.data.order; - return { - warrantyStatus: order.warrantyStatus?.value ?? null, - warrantyActivationDeadline: order.warrantyActivationDeadline?.value ?? null - }; + return order; +}; + +export const getFile = async (id: string) => { + const res = await shopifyFetch<{ + data: { + node: File; + }; + variables: { + id: string; + }; + }>({ + query: getFileQuery, + variables: { id } + }); + + return res.body.data.node; }; diff --git a/lib/shopify/queries/node.ts b/lib/shopify/queries/node.ts index 0a53ff934..7334b5871 100644 --- a/lib/shopify/queries/node.ts +++ b/lib/shopify/queries/node.ts @@ -32,3 +32,15 @@ export const getMetaobjectsByIdsQuery = /* GraphQL */ ` } } `; + +export const getFileQuery = /* GraphQL */ ` + query getFile($id: ID!) { + node(id: $id) { + ... on GenericFile { + id + url + alt + } + } + } +`; diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index 2980d1a4c..049c7b715 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -858,12 +858,28 @@ export enum WarrantyStatus { LimitedActivated = 'Limited Activation' } -export type ShopifyOrderMetafield = { - warrantyStatus: { value: WarrantyStatus } | null; - warrantyActivationDeadline: { value: string } | null; +export type OrderMetafieldValue = { + value: T; + id: string; + key: string; }; -export type OrderMetafield = { - warrantyStatus: WarrantyStatus | null; - warrantyActivationDeadline: string | null; +export type ShopifyOrderMetafield = { + warrantyStatus: OrderMetafieldValue | null; + warrantyActivationDeadline: OrderMetafieldValue | null; + warrantyActivationOdometer: OrderMetafieldValue | null; + warrantyActivationInstallation: OrderMetafieldValue | null; + warrantyActivationSelfInstall: OrderMetafieldValue | null; + warrantyActivationVIN: OrderMetafieldValue | null; + warrantyActivationMileage: OrderMetafieldValue | null; }; + +export type File = { + id: string; + url: string; + alt: string; +}; + +export type UpdateOrderMetafieldInput = + | { key: string; value?: string | null; type: string; namespace: string } + | { id: string; value?: string | null; key: string }; diff --git a/package.json b/package.json index 96a421952..5251cbf2b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@headlessui/react": "^2.1.0", "@heroicons/react": "^2.1.3", "@hookform/resolvers": "^3.6.0", - "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", "clsx": "^2.1.0", "geist": "^1.3.0",