Jelajahi Sumber

feat(dashboard): Add surcharge functionality to order modification page (#4044)

Bibiana Sebestianova 1 bulan lalu
induk
melakukan
0188735c5a

+ 139 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx

@@ -0,0 +1,139 @@
+import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
+import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
+import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import { Form } from '@/vdb/components/ui/form.js';
+import { Input } from '@/vdb/components/ui/input.js';
+import { Switch } from '@/vdb/components/ui/switch.js';
+import { DetailFormGrid } from '@/vdb/framework/layout-engine/page-layout.js';
+import { useChannel } from '@/vdb/hooks/use-channel.js';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Trans } from '@lingui/react/macro';
+import { VariablesOf } from 'gql.tada';
+import { Plus } from 'lucide-react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { modifyOrderDocument } from '../orders.graphql.js';
+
+type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
+type SurchargeInput = NonNullable<ModifyOrderInput['surcharges']>[number];
+
+const surchargeFormSchema = z.object({
+    description: z.string().min(1),
+    sku: z.string().optional(),
+    price: z.string().refine(val => !isNaN(Number(val)) && Number(val) > 0, {
+        message: 'Price must be a positive number',
+    }),
+    priceIncludesTax: z.boolean().default(false),
+    taxRate: z.number().nonnegative().max(100),
+    taxDescription: z.string().optional(),
+});
+
+type SurchargeFormValues = z.infer<typeof surchargeFormSchema>;
+
+export interface AddSurchargeFormProps {
+    onAddSurcharge: (surcharge: SurchargeInput) => void;
+}
+
+export function AddSurchargeForm({ onAddSurcharge }: Readonly<AddSurchargeFormProps>) {
+    const { activeChannel } = useChannel();
+
+    const surchargeForm = useForm<SurchargeFormValues>({
+        resolver: zodResolver(surchargeFormSchema),
+        mode: 'onChange',
+        defaultValues: {
+            description: '',
+            sku: '',
+            price: '0',
+            priceIncludesTax: false,
+            taxRate: 0,
+            taxDescription: '',
+        },
+    });
+
+    const taxRate = surchargeForm.watch('taxRate') || 0;
+
+    const handleAddSurcharge = () => {
+        surchargeForm.handleSubmit(values => {
+            onAddSurcharge({
+                description: values.description,
+                sku: values.sku || undefined,
+                price: Number(values.price), // already in minor units from MoneyInput
+                priceIncludesTax: values.priceIncludesTax,
+                taxRate: values.taxRate ?? undefined,
+                taxDescription: values.taxDescription || undefined,
+            });
+            surchargeForm.reset();
+        })();
+    };
+
+    return (
+        <Form {...surchargeForm}>
+            <div className="space-y-4">
+                <DetailFormGrid>
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="description"
+                        label={<Trans>Description</Trans>}
+                        render={({ field }) => <Input {...field} />}
+                    />
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="sku"
+                        label={<Trans>SKU</Trans>}
+                        render={({ field }) => <Input {...field} />}
+                    />
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="price"
+                        label={<Trans>Price</Trans>}
+                        render={({ field }) => (
+                            <MoneyInput
+                                {...field}
+                                value={Number(field.value) || 0}
+                                onChange={value => field.onChange(value.toString())}
+                                currency={activeChannel?.defaultCurrencyCode}
+                            />
+                        )}
+                    />
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="priceIncludesTax"
+                        label={<Trans>Includes tax at {taxRate}%</Trans>}
+                        render={({ field }) => (
+                            <Switch checked={field.value} onCheckedChange={field.onChange} />
+                        )}
+                    />
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="taxRate"
+                        label={<Trans>Tax rate</Trans>}
+                        render={({ field }) => (
+                            <AffixedInput
+                                {...field}
+                                type="number"
+                                suffix="%"
+                                value={field.value}
+                                onChange={e => field.onChange(e.target.valueAsNumber)}
+                            />
+                        )}
+                    />
+                    <FormFieldWrapper
+                        control={surchargeForm.control}
+                        name="taxDescription"
+                        label={<Trans>Tax description</Trans>}
+                        render={({ field }) => <Input {...field} />}
+                    />
+                </DetailFormGrid>
+                <Button
+                    type="button"
+                    onClick={handleAddSurcharge}
+                    disabled={!surchargeForm.formState.isValid}
+                >
+                    <Plus className="w-4 h-4 mr-2" />
+                    <Trans>Add surcharge</Trans>
+                </Button>
+            </div>
+        </Form>
+    );
+}

+ 49 - 11
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx

@@ -1,3 +1,5 @@
+import { Money } from '@/vdb/components/data-display/money.js';
+import { Textarea } from '@/vdb/components/ui/textarea.js';
 import { Trans } from '@lingui/react/macro';
 import { ResultOf, VariablesOf } from 'gql.tada';
 import { modifyOrderDocument, orderDetailDocument } from '../orders.graphql.js';
@@ -15,6 +17,7 @@ interface OrderModificationSummaryProps {
         id: string;
         name: string;
     }>;
+    onNoteChange?: (note: string) => void;
 }
 
 interface LineAdjustment {
@@ -31,6 +34,7 @@ export function OrderModificationSummary({
     modifyOrderInput,
     addedVariants,
     eligibleShippingMethods,
+    onNoteChange,
 }: Readonly<OrderModificationSummaryProps>) {
     // Map by line id for quick lookup
     const originalLineMap = new Map(originalOrder.lines.map(line => [line.id, line]));
@@ -101,6 +105,20 @@ export function OrderModificationSummary({
             modifiedShippingMethodId;
     }
 
+    // Added surcharges
+    const addedSurcharges = modifyOrderInput.surcharges ?? [];
+
+    const hasNoModifications =
+        adjustedLines.length === 0 &&
+        addedLines.length === 0 &&
+        removedLines.length === 0 &&
+        addedCouponCodes.length === 0 &&
+        removedCouponCodes.length === 0 &&
+        addedSurcharges.length === 0 &&
+        !modifyOrderInput.updateShippingAddress &&
+        !modifyOrderInput.updateBillingAddress &&
+        !shippingMethodChanged;
+
     return (
         <div className="text-sm">
             {/* Address changes */}
@@ -206,18 +224,38 @@ export function OrderModificationSummary({
                     </ul>
                 </div>
             )}
-            {adjustedLines.length === 0 &&
-                addedLines.length === 0 &&
-                removedLines.length === 0 &&
-                addedCouponCodes.length === 0 &&
-                removedCouponCodes.length === 0 &&
-                !modifyOrderInput.updateShippingAddress &&
-                !modifyOrderInput.updateBillingAddress &&
-                !shippingMethodChanged && (
-                    <div className="text-muted-foreground">
-                        <Trans>No modifications made</Trans>
+            {addedSurcharges.length > 0 && (
+                <div className="mb-2">
+                    <div className="font-medium">
+                        <Trans>Adding {addedSurcharges.length} surcharge(s)</Trans>
                     </div>
-                )}
+                    <ul className="list-disc ml-4">
+                        {addedSurcharges.map((surcharge, index) => (
+                            <li key={`surcharge-${index}`}>
+                                <div className="flex items-center gap-1">
+                                    <span>{surcharge.description}:</span>
+                                    <Money value={surcharge.price} currency={originalOrder.currencyCode} />
+                                </div>
+                            </li>
+                        ))}
+                    </ul>
+                </div>
+            )}
+            {hasNoModifications && (
+                <div className="text-muted-foreground">
+                    <Trans>No modifications made</Trans>
+                </div>
+            )}
+            <div className="mb-4 mt-4">
+                <div className="font-medium mb-2">
+                    <Trans>Note</Trans>
+                </div>
+                <Textarea
+                    disabled={hasNoModifications}
+                    value={modifyOrderInput.note ?? ''}
+                    onChange={e => onNoteChange?.(e.target.value)}
+                />
+            </div>
         </div>
     );
 }

+ 9 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx

@@ -16,6 +16,7 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
 import { User } from 'lucide-react';
 import { useState } from 'react';
 import { toast } from 'sonner';
+import { AddSurchargeForm } from './components/add-surcharge-form.js';
 import { CustomerAddressSelector } from './components/customer-address-selector.js';
 import { EditOrderTable } from './components/edit-order-table.js';
 import { OrderAddress } from './components/order-address.js';
@@ -83,6 +84,8 @@ function ModifyOrderPage() {
         removeCouponCode,
         updateShippingAddress: updateShippingAddressInInput,
         updateBillingAddress: updateBillingAddressInInput,
+        addSurcharge,
+        setNote,
         hasModifications,
     } = useModifyOrder(entity);
 
@@ -181,6 +184,11 @@ function ModifyOrderPage() {
                         displayTotals={false}
                     />
                 </PageBlock>
+
+                <PageBlock column="main" blockId="add-surcharge" title={<Trans>Add surcharge</Trans>}>
+                    <AddSurchargeForm onAddSurcharge={addSurcharge} />
+                </PageBlock>
+
                 <PageBlock
                     column="side"
                     blockId="modification-summary"
@@ -196,6 +204,7 @@ function ModifyOrderPage() {
                                 name: m.name,
                             })) ?? []
                         }
+                        onNoteChange={setNote}
                     />
                     <div className="mt-4 flex justify-end">
                         <Button

+ 23 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts

@@ -18,6 +18,8 @@ export type ProductVariantInfo = {
     priceWithTax?: number;
 };
 
+type SurchargeInput = NonNullable<ModifyOrderInput['surcharges']>[number];
+
 export interface UseModifyOrderReturn {
     modifyOrderInput: ModifyOrderInput;
     addedVariants: Map<string, ProductVariantInfo>;
@@ -33,6 +35,8 @@ export interface UseModifyOrderReturn {
     removeCouponCode: (params: { couponCode: string }) => void;
     updateShippingAddress: (address: AddressFragment) => void;
     updateBillingAddress: (address: AddressFragment) => void;
+    addSurcharge: (surcharge: SurchargeInput) => void;
+    setNote: (note: string) => void;
     hasModifications: boolean;
 }
 
@@ -284,6 +288,22 @@ export function useModifyOrder(order: Order | null | undefined): UseModifyOrderR
         }));
     }, []);
 
+    // Add surcharge
+    const addSurcharge = useCallback((surcharge: SurchargeInput) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            surcharges: [...(prev.surcharges ?? []), surcharge],
+        }));
+    }, []);
+
+    // Set note
+    const setNote = useCallback((note: string) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            note: note || '',
+        }));
+    }, []);
+
     // Check if there are modifications
     const hasModifications = useMemo(() => {
         return (
@@ -291,6 +311,7 @@ export function useModifyOrder(order: Order | null | undefined): UseModifyOrderR
             (modifyOrderInput.adjustOrderLines?.length ?? 0) > 0 ||
             (modifyOrderInput.couponCodes?.length ?? 0) > 0 ||
             (modifyOrderInput.shippingMethodIds?.length ?? 0) > 0 ||
+            (modifyOrderInput.surcharges?.length ?? 0) > 0 ||
             !!modifyOrderInput.updateShippingAddress ||
             !!modifyOrderInput.updateBillingAddress
         );
@@ -307,6 +328,8 @@ export function useModifyOrder(order: Order | null | undefined): UseModifyOrderR
         removeCouponCode,
         updateShippingAddress,
         updateBillingAddress,
+        addSurcharge,
+        setNote,
         hasModifications,
     };
 }