mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
allow customer to check on self installed field
This commit is contained in:
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring peer h-4 w-4 shrink-0 rounded-sm border border-dark focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-dark data-[state=checked]:text-white',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
25
components/form/checkbox-field.tsx
Normal file
25
components/form/checkbox-field.tsx
Normal file
@@ -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 (
|
||||
<Field className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
name={name}
|
||||
className="group size-5 rounded bg-white p-1 ring-1 ring-inset ring-gray-300 data-[checked]:bg-primary data-[checked]:ring-primary"
|
||||
{...props}
|
||||
>
|
||||
{/* Checkmark icon */}
|
||||
<CheckIcon className="hidden size-3 fill-white group-data-[checked]:block" />
|
||||
</Checkbox>
|
||||
<Label className="block text-sm font-medium leading-6 text-gray-900">{label}</Label>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxField;
|
@@ -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<ShopifyFile | undefined> => {
|
||||
if (!fileId) return undefined;
|
||||
try {
|
||||
const file = await getFile(fileId);
|
||||
return file;
|
||||
} catch (error) {
|
||||
console.log('getFileDetails action', error);
|
||||
}
|
||||
};
|
||||
|
@@ -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<File | undefined>();
|
||||
const [defaultFileDetails, setDefaultFileDetails] = useState<ShopifyFile | undefined>();
|
||||
|
||||
const [loading, startTransition] = useTransition();
|
||||
|
||||
const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div>
|
||||
<label className="block text-sm font-medium leading-6 text-gray-900">{label}</label>
|
||||
@@ -34,7 +52,9 @@ const FileInput = ({ name, label }: FileInputProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{file && <p className="mt-2 text-sm text-gray-500">{file.name}</p>}
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
{loading ? <LoadingDots className="bg-dark" /> : file?.name || defaultFileDetails?.alt}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Field, Input as HeadlessInput, Label } from '@headlessui/react';
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
|
@@ -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 {
|
||||
|
@@ -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<HTMLFormElement>(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
|
||||
<DialogTitle className="mb-2 font-bold">Activate Warranty</DialogTitle>
|
||||
<form onSubmit={handleSubmit} ref={formRef}>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<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" />
|
||||
<FileInput
|
||||
label="Odometer"
|
||||
name="warranty_activation_odometer"
|
||||
fileId={orderMetafields?.warrantyActivationOdometer?.value}
|
||||
/>
|
||||
<FileInput
|
||||
label="Installation Receipt"
|
||||
name="warranty_activation_installation"
|
||||
fileId={orderMetafields?.warrantyActivationInstallation?.value}
|
||||
/>
|
||||
<CheckboxField
|
||||
label="Self Installed"
|
||||
name="warranty_activation_self_install"
|
||||
defaultChecked={orderMetafields?.warrantyActivationSelfInstall?.value === 'true'}
|
||||
/>
|
||||
<Input
|
||||
label="Customer Mileage"
|
||||
name="warranty_activation_mileage"
|
||||
type="number"
|
||||
defaultValue={orderMetafields?.warrantyActivationMileage?.value}
|
||||
/>
|
||||
<Input
|
||||
label="Customer VIN"
|
||||
name="warranty_activation_vin"
|
||||
defaultValue={orderMetafields?.warrantyActivationVIN?.value}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-end gap-4">
|
||||
<button
|
||||
@@ -71,7 +101,7 @@ function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyMod
|
||||
disabled={pending}
|
||||
>
|
||||
{pending && <LoadingDots className="bg-white" />}
|
||||
Activate
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -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 <WarrantyActivatedBadge />;
|
||||
@@ -32,7 +32,12 @@ const ActivateWarranty = ({ order, orderMetafields }: ActivateWarrantyModalProps
|
||||
>
|
||||
Activate Warranty
|
||||
</button>
|
||||
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} orderId={order.id} />
|
||||
<ActivateWarrantyModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
orderId={order.id}
|
||||
orderMetafields={orderMetafields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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 = ({
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} orderId={order.id} />
|
||||
<ActivateWarrantyModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
orderId={order.id}
|
||||
orderMetafields={orderMetafields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user