finish activate warranty step

This commit is contained in:
Chloe 2024-06-25 21:57:57 +07:00
parent 5385b5ed67
commit 2477fdf84e
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
13 changed files with 255 additions and 82 deletions

View File

@ -61,7 +61,7 @@ export default async function AccountPage() {
<span>View Order</span>
<span className="sr-only">{order.normalizedId}</span>
</Link>
<ActivateWarranty orderId={order.id} />
<ActivateWarranty order={order} />
</div>
</div>

View File

@ -1,9 +1,28 @@
'use server';
import { createFile, stageUploadFile, uploadFile } from 'lib/shopify';
import { UploadInput } from 'lib/shopify/types';
import { StagedUploadsCreatePayload, UploadInput } from 'lib/shopify/types';
export const createStagedUploadFiles = async (params: UploadInput) => {
const prepareFilePayload = ({
stagedFileUpload,
file
}: {
stagedFileUpload: StagedUploadsCreatePayload;
file: File;
}) => {
const formData = new FormData();
const url = stagedFileUpload.url;
stagedFileUpload.parameters.forEach(({ name, value }) => {
formData.append(name, value);
});
formData.append('file', file);
return { url, formData };
};
const createStagedUploadFiles = async (params: UploadInput) => {
try {
const stagedTargets = await stageUploadFile(params);
if (!stagedTargets || stageUploadFile.length === 0) return null;
@ -14,25 +33,54 @@ export const createStagedUploadFiles = async (params: UploadInput) => {
}
};
export const onUploadFile = async ({
const onUploadFile = async ({
url,
formData,
fileName,
resourceUrl
resourceUrl,
contentType = 'FILE'
}: {
url: string;
formData: FormData;
fileName: string;
resourceUrl: string;
contentType?: 'FILE' | 'IMAGE';
}) => {
try {
await uploadFile({ url, formData });
await createFile({
return await createFile({
alt: fileName,
contentType: 'FILE',
contentType,
originalSource: resourceUrl
});
} catch (error) {
console.log(error);
}
};
export const handleUploadFile = async ({ file }: { file: File }) => {
if (!file) return;
try {
const stagedTarget = await createStagedUploadFiles({
filename: file.name,
fileSize: String(file.size),
httpMethod: 'POST',
resource: 'FILE',
mimeType: file.type
});
if (stagedTarget) {
const data = prepareFilePayload({ file, stagedFileUpload: stagedTarget });
const result = await onUploadFile({
...data,
fileName: file.name,
resourceUrl: stagedTarget.resourceUrl
});
return result?.[0]?.id;
}
} catch (error) {
console.log(error);
}
};

View File

@ -1,42 +0,0 @@
import { StagedUploadsCreatePayload } from 'lib/shopify/types';
import { createStagedUploadFiles, onUploadFile } from './actions';
export const prepareFilePayload = ({
stagedFileUpload,
file
}: {
stagedFileUpload: StagedUploadsCreatePayload;
file: File;
}) => {
const formData = new FormData();
const url = stagedFileUpload.url;
stagedFileUpload.parameters.forEach(({ name, value }) => {
formData.append(name, value);
});
formData.append('file', file);
return { url, formData };
};
export const handleUploadFile = async ({ file }: { file: File }) => {
if (!file) return;
const stagedTarget = await createStagedUploadFiles({
filename: file.name,
fileSize: String(file.size),
httpMethod: 'POST',
resource: 'FILE',
mimeType: file.type
});
if (stagedTarget) {
const data = prepareFilePayload({ file, stagedFileUpload: stagedTarget });
await onUploadFile({
...data,
fileName: file.name,
resourceUrl: stagedTarget.resourceUrl
});
}
};

View File

@ -0,0 +1,44 @@
'use server';
import { handleUploadFile } from 'components/form/file-input/actions';
import { updateOrderMetafields } from 'lib/shopify';
import { revalidatePath } from 'next/cache';
export const activateWarranty = async (orderId: string, formData: FormData) => {
let odometerFileId = null;
let installationFileId = null;
const odometerFile = formData.get('warranty_activation_odometer');
const installationFile = formData.get('warranty_activation_installation');
if (odometerFile) {
odometerFileId = await handleUploadFile({ file: odometerFile as File });
}
if (installationFile) {
installationFileId = await handleUploadFile({ file: installationFile as File });
}
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'
}
];
try {
await updateOrderMetafields({
orderId,
metafields: rawFormData
});
revalidatePath('/account');
} catch (error) {
console.log(error);
}
};

View File

@ -1,15 +1,36 @@
'use client';
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
import { Button, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
import clsx from 'clsx';
import FileInput from 'components/form/file-input';
import Input from 'components/form/input';
import LoadingDots from 'components/loading-dots';
import { FormEventHandler, useRef, useTransition } from 'react';
import { activateWarranty } from './actions';
type ActivateWarrantyModalProps = {
isOpen: boolean;
onClose: () => void;
orderId: string;
};
function ActivateWarrantyModal({ onClose, isOpen }: ActivateWarrantyModalProps) {
function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyModalProps) {
const [pending, startTransition] = useTransition();
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const form = formRef.current;
if (!form) return;
const formData = new FormData(form);
startTransition(async () => {
await activateWarranty(orderId, formData);
form.reset();
onClose();
});
};
return (
<Dialog
open={isOpen}
@ -25,14 +46,13 @@ function ActivateWarrantyModal({ onClose, isOpen }: ActivateWarrantyModalProps)
{/* The actual dialog panel */}
<DialogPanel className="w-full max-w-lg bg-white p-5 sm:w-[500px]">
<DialogTitle className="mb-2 font-bold">Activate Warranty</DialogTitle>
<form>
<form onSubmit={handleSubmit} ref={formRef}>
<div className="flex w-full flex-col gap-4">
<FileInput label="Odometer" name="odometer" />
<FileInput label="Installation Receipt" name="installation-receipt" />
<Input label="Customer Mileage" name="customer-mileage" type="number" />
<Input label="Customer VIN" name="customer-vin" />
<FileInput label="Odometer" name="warranty_activation_odometer" />
<FileInput label="Installation Receipt" name="warranty_activation_installation" />
<Input label="Customer Mileage" name="warranty_activation_mileage" type="number" />
<Input label="Customer VIN" name="warranty_activation_vin" />
</div>
</form>
<div className="mt-4 flex w-full justify-end gap-4">
<button
type="button"
@ -41,13 +61,20 @@ function ActivateWarrantyModal({ onClose, isOpen }: ActivateWarrantyModalProps)
>
Cancel
</button>
<button
<Button
type="submit"
className="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
className={clsx(
'flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
{ 'cursor-not-allowed opacity-60': pending },
{ 'cursor-pointer opacity-100': !pending }
)}
disabled={pending}
>
{pending && <LoadingDots className="bg-white" />}
Activate
</button>
</Button>
</div>
</form>
</DialogPanel>
</div>
</Dialog>

View File

@ -1,13 +1,14 @@
'use client';
import { Order } from 'lib/shopify/types';
import { useState } from 'react';
import ActivateWarrantyModal from './activate-warranty-modal';
type ActivateWarrantyModalProps = {
orderId: string;
order: Order;
};
const ActivateWarranty = ({ orderId }: ActivateWarrantyModalProps) => {
const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
@ -17,7 +18,7 @@ const ActivateWarranty = ({ orderId }: ActivateWarrantyModalProps) => {
>
Activate Warranty
</button>
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} orderId={order.id} />
</>
);
};

View File

@ -55,7 +55,7 @@ const MobileOrderActions = ({ order }: { order: Order }) => {
</div>
</MenuItems>
</Menu>
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} orderId={order.id} />
</>
);
};

View File

@ -35,7 +35,7 @@ export const TAGS = {
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
export const DEFAULT_OPTION = 'Default Title';
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2024-04/graphql.json';
export const SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT = '/account/customer/api/2024-07/graphql';
export const SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT = '/account/customer/api/2024-04/graphql';
export const SHOPIFY_GRAPHQL_ADMIN_ADMIN_API_ENDPOINT = '/admin/api/2024-04/graphql.json';
export const CORE_WAIVER = 'core-waiver';
@ -60,3 +60,10 @@ export const ADD_ON_PRODUCT_TYPES = {
addOn: 'Add On',
coreCharge: 'Core Charge'
};
export const WARRANTY_FIELDS = [
'warranty_activation_odometer',
'warranty_activation_installation',
'warranty_activation_vin',
'warranty_activation_mileage'
];

View File

@ -11,6 +11,7 @@ import {
SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT,
TAGS,
VARIANT_METAFIELD_PREFIX,
WARRANTY_FIELDS,
YEAR_FILTER_ID
} from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
@ -26,6 +27,7 @@ import {
setCartAttributesMutation
} from './mutations/cart';
import { createFileMutation, createStageUploads } from './mutations/file';
import { updateOrderMetafieldsMutation } from './mutations/order';
import { getCartQuery } from './queries/cart';
import {
getCollectionProductsQuery,
@ -57,6 +59,7 @@ import {
Image,
LineItem,
Menu,
Metafield,
Metaobject,
Money,
Order,
@ -97,9 +100,11 @@ import {
ShopifySetCartAttributesOperation,
ShopifyStagedUploadOperation,
ShopifyUpdateCartOperation,
ShopifyUpdateOrderMetafieldsOperation,
Transaction,
TransmissionType,
UploadInput
UploadInput,
WarrantyStatus
} from './types';
const domain = process.env.SHOPIFY_STORE_DOMAIN
@ -592,6 +597,7 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
totalPrice: reshapeMoney(item.totalPrice)
})) || [];
console.log(shopifyOrder);
const order: Order = {
id: shopifyOrder.id,
normalizedId: shopifyOrder.id.replace('gid://shopify/Order/', ''),
@ -1072,5 +1078,45 @@ export const createFile = async (params: FileCreateInput) => {
variables: { files: [params] }
});
return res.body.data;
return res.body.data.fileCreate.files;
};
export const updateOrderMetafields = async ({
orderId,
metafields
}: {
orderId: string;
metafields: { key: string; value: string | undefined | null; type: string }[];
}) => {
const validMetafields = (
metafields.filter((field) => Boolean(field)) as Array<Omit<Metafield, 'namespace'>>
).map((field) => ({
...field,
namespace: 'custom'
}));
const shouldSetWarrantyStatusToActivated = WARRANTY_FIELDS.every((field) =>
validMetafields.find(({ key }) => key === field)
);
const response = await adminFetch<ShopifyUpdateOrderMetafieldsOperation>({
query: updateOrderMetafieldsMutation,
variables: {
input: {
metafields: shouldSetWarrantyStatusToActivated
? validMetafields.concat([
{
key: 'warranty_status',
value: WarrantyStatus.Activated,
namespace: 'custom',
type: 'single_line_text_field'
}
])
: validMetafields,
id: orderId
}
}
});
return response.body.data.orderUpdate.order.id;
};

View File

@ -18,10 +18,8 @@ export const createFileMutation = /* GraphQL */ `
fileCreate(files: $files) {
files {
fileStatus
... on MediaImage {
id
}
}
userErrors {
field
message

View File

@ -0,0 +1,13 @@
export const updateOrderMetafieldsMutation = /* GraphQL */ `
mutation updateOrderMetafields($input: OrderInput!) {
orderUpdate(input: $input) {
order {
id
}
userErrors {
message
field
}
}
}
`;

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
export type Maybe<T> = T | null;
export type Connection<T> = {
@ -825,3 +826,33 @@ export type ShopifyCreateFileOperation = {
};
variables: { files: FileCreateInput[] };
};
export type Metafield = {
namespace: string;
value: string;
key: string;
type: string;
};
export type ShopifyUpdateOrderMetafieldsOperation = {
data: {
orderUpdate: {
order: {
id: string;
};
userErrors: { field: string; message: string }[];
};
};
variables: {
input: {
metafields: Metafield[];
id: string;
};
};
};
export enum WarrantyStatus {
Activated = 'Activated',
NotActivated = 'Not Activated',
LimitedActivated = 'Limited Activation'
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,