diff --git a/admin-spa/src/hooks/useShortcuts.tsx b/admin-spa/src/hooks/useShortcuts.tsx
index a62d97c..b199ac8 100644
--- a/admin-spa/src/hooks/useShortcuts.tsx
+++ b/admin-spa/src/hooks/useShortcuts.tsx
@@ -19,14 +19,14 @@ export function useShortcuts({ toggleFullscreen }: { toggleFullscreen?: () => vo
// Always handle Command Palette toggle first so it works everywhere
if (mod && key === "k") {
e.preventDefault();
- try { useCommandStore.getState().toggle(); } catch {}
+ try { useCommandStore.getState().toggle(); } catch { /* ignore if store not available */ }
return;
}
// If Command Palette is open, ignore the rest
try {
if (useCommandStore.getState().open) return;
- } catch {}
+ } catch { /* ignore if store not available */ }
// Do not trigger single-key shortcuts while typing
const ae = (document.activeElement as HTMLElement | null);
diff --git a/admin-spa/src/lib/api.ts b/admin-spa/src/lib/api.ts
index 833ef90..1c54b59 100644
--- a/admin-spa/src/lib/api.ts
+++ b/admin-spa/src/lib/api.ts
@@ -16,7 +16,7 @@ export const api = {
try {
const text = await res.text();
responseData = text ? JSON.parse(text) : null;
- } catch {}
+ } catch { /* ignore JSON parse errors */ }
if (window.WNW_API?.isDev) {
console.error('[WooNooW] API error', { url, status: res.status, statusText: res.statusText, data: responseData });
diff --git a/admin-spa/src/lib/currency.ts b/admin-spa/src/lib/currency.ts
index 41ac9c5..54579a8 100644
--- a/admin-spa/src/lib/currency.ts
+++ b/admin-spa/src/lib/currency.ts
@@ -150,7 +150,6 @@ export function makeMoneyFormatter(opts: MoneyOptions) {
* Use inside components to avoid repeating memo logic.
*/
export function useMoneyFormatter(opts: MoneyOptions) {
- // eslint-disable-next-line react-hooks/rules-of-hooks
// Note: file lives in /lib so we keep dependency-free; simple memo by JSON key is fine.
const key = JSON.stringify({
c: opts.currency,
@@ -162,7 +161,6 @@ export function useMoneyFormatter(opts: MoneyOptions) {
ds: opts.decimalSep,
pos: opts.position,
});
- // eslint-disable-next-line react-hooks/rules-of-hooks
const ref = (globalThis as any).__wnw_money_cache || ((globalThis as any).__wnw_money_cache = new Map());
if (!ref.has(key)) ref.set(key, makeMoneyFormatter(opts));
return ref.get(key) as (v: MoneyInput) => string;
diff --git a/admin-spa/src/routes/Orders/Detail.tsx b/admin-spa/src/routes/Orders/Detail.tsx
index 3ae6408..4784a75 100644
--- a/admin-spa/src/routes/Orders/Detail.tsx
+++ b/admin-spa/src/routes/Orders/Detail.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
-import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom';
+import { useParams, useSearchParams, Link } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api, OrdersApi } from '@/lib/api';
import { formatRelativeOrDate } from '@/lib/dates';
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib/errorHandling';
import { ErrorCard } from '@/components/ErrorCard';
import { InlineLoadingState } from '@/components/LoadingState';
-import { __, sprintf } from '@/lib/i18n';
+import { __ } from '@/lib/i18n';
function Money({ value, currency, symbol }: { value?: number; currency?: string; symbol?: string }) {
return <>{formatMoney(value, { currency, symbol })}>;
@@ -19,7 +19,7 @@ function Money({ value, currency, symbol }: { value?: number; currency?: string;
function StatusBadge({ status }: { status?: string }) {
const s = (status || '').toLowerCase();
- let cls = 'inline-flex items-center rounded px-2 py-1 text-xs font-medium border';
+ const cls = 'inline-flex items-center rounded px-2 py-1 text-xs font-medium border';
let tone = 'bg-gray-100 text-gray-700 border-gray-200';
if (s === 'completed' || s === 'paid') tone = 'bg-green-100 text-green-800 border-green-200';
else if (s === 'processing') tone = 'bg-yellow-100 text-yellow-800 border-yellow-200';
@@ -33,7 +33,6 @@ const STATUS_OPTIONS = ['pending', 'processing', 'completed', 'on-hold', 'cancel
export default function OrderShow() {
const { id } = useParams<{ id: string }>();
- const nav = useNavigate();
const qc = useQueryClient();
const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW';
diff --git a/admin-spa/src/routes/Orders/partials/OrderForm.tsx b/admin-spa/src/routes/Orders/partials/OrderForm.tsx
index 13a569b..c7b6f58 100644
--- a/admin-spa/src/routes/Orders/partials/OrderForm.tsx
+++ b/admin-spa/src/routes/Orders/partials/OrderForm.tsx
@@ -15,7 +15,7 @@ import { makeMoneyFormatter, getStoreCurrency } from '@/lib/currency';
import { useQuery } from '@tanstack/react-query';
import { api, ProductsApi, CustomersApi } from '@/lib/api';
import { cn } from '@/lib/utils';
-import { __, sprintf } from '@/lib/i18n';
+import { __ } from '@/lib/i18n';
import { toast } from 'sonner';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -107,7 +107,7 @@ export default function OrderForm({
shippings = [],
onSubmit,
className,
- leftTop,
+ leftTop: _leftTop,
rightTop,
itemsEditable = true,
showCoupons = true,
@@ -169,7 +169,6 @@ export default function OrderForm({
const [submitting, setSubmitting] = React.useState(false);
const [items, setItems] = React.useState
(initial?.items || []);
- const [coupons, setCoupons] = React.useState('');
const [couponInput, setCouponInput] = React.useState('');
const [validatedCoupons, setValidatedCoupons] = React.useState([]);
const [couponValidating, setCouponValidating] = React.useState(false);
diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx
index 744b6c8..26a5778 100644
--- a/admin-spa/src/routes/Settings/Payments.tsx
+++ b/admin-spa/src/routes/Settings/Payments.tsx
@@ -1,19 +1,237 @@
-import React from 'react';
-import { __ } from '@/lib/i18n';
+import React, { useState } from 'react';
+import { SettingsLayout } from './components/SettingsLayout';
+import { SettingsCard } from './components/SettingsCard';
+import { ToggleField } from './components/ToggleField';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { CreditCard, DollarSign, Banknote, Settings } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface PaymentProvider {
+ id: string;
+ name: string;
+ description: string;
+ icon: React.ReactNode;
+ enabled: boolean;
+ connected: boolean;
+ fees?: string;
+ testMode?: boolean;
+}
+
+export default function PaymentsPage() {
+ const [testMode, setTestMode] = useState(false);
+ const [providers] = useState([
+ {
+ id: 'stripe',
+ name: 'Stripe Payments',
+ description: 'Accept Visa, Mastercard, Amex, and more',
+ icon: ,
+ enabled: false,
+ connected: false,
+ fees: '2.9% + $0.30 per transaction',
+ },
+ {
+ id: 'paypal',
+ name: 'PayPal',
+ description: 'Accept PayPal payments worldwide',
+ icon: ,
+ enabled: true,
+ connected: true,
+ fees: '3.49% + fixed fee per transaction',
+ },
+ ]);
+
+ const [manualMethods, setManualMethods] = useState([
+ { id: 'bacs', name: 'Bank Transfer (BACS)', enabled: true },
+ { id: 'cod', name: 'Cash on Delivery', enabled: true },
+ { id: 'cheque', name: 'Check Payments', enabled: false },
+ ]);
+
+ const handleSave = async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ toast.success('Payment settings have been updated successfully.');
+ };
+
+
+ const toggleManualMethod = (id: string) => {
+ setManualMethods((prev) =>
+ prev.map((m) => (m.id === id ? { ...m, enabled: !m.enabled } : m))
+ );
+ };
-export default function SettingsPayments() {
return (
-
-
{__('Payment Settings')}
-
- {__('Configure payment gateways and options.')}
-
-
-
+
+ {/* Test Mode Banner */}
+ {testMode && (
+
+
+
+ ⚠️ Test Mode Active
+
+
+ No real charges will be processed
+
+
+
+ )}
+
+ {/* Payment Providers */}
+
+
+ {providers.map((provider) => (
+
+
+
+
+ {provider.icon}
+
+
+
+
{provider.name}
+ {provider.connected ? (
+
+ ● Connected
+
+ ) : (
+ ○ Not connected
+ )}
+
+
+ {provider.description}
+
+ {provider.fees && (
+
+ {provider.fees}
+
+ )}
+
+
+
+ {provider.connected ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ {/* Manual Payment Methods */}
+
+
+ {manualMethods.map((method) => (
+
+
+
+
+
+
+
+
{method.name}
+ {method.enabled && (
+
+ Customers can choose this at checkout
+
+ )}
+
+
+
+ {method.enabled && (
+
+ )}
+ toggleManualMethod(method.id)}
+ />
+
+
+
+ ))}
+
+
+
+ {/* Payment Settings */}
+
+
+
+
+
+
+
+ Choose when to capture payment from customers
+
+
+
+
+
+
+
+
+
+ {/* Help Card */}
+
+
💡 Need help setting up payments?
- {__('Payment settings interface coming soon. This will include payment gateway configuration.')}
+ Our setup wizard can help you connect Stripe or PayPal in minutes.
+
-
+
);
}
diff --git a/admin-spa/src/routes/Settings/Shipping.tsx b/admin-spa/src/routes/Settings/Shipping.tsx
index d8ef701..a215be7 100644
--- a/admin-spa/src/routes/Settings/Shipping.tsx
+++ b/admin-spa/src/routes/Settings/Shipping.tsx
@@ -1,19 +1,233 @@
-import React from 'react';
-import { __ } from '@/lib/i18n';
+import React, { useState } from 'react';
+import { SettingsLayout } from './components/SettingsLayout';
+import { SettingsCard } from './components/SettingsCard';
+import { ToggleField } from './components/ToggleField';
+import { Button } from '@/components/ui/button';
+import { Globe, Truck, MapPin, Edit, Trash2 } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface ShippingRate {
+ id: string;
+ name: string;
+ price: string;
+ condition?: string;
+ transitTime?: string;
+}
+
+interface ShippingZone {
+ id: string;
+ name: string;
+ regions: string;
+ rates: ShippingRate[];
+}
+
+export default function ShippingPage() {
+ const [zones] = useState
([
+ {
+ id: 'domestic',
+ name: 'Domestic (Indonesia)',
+ regions: 'All Indonesia',
+ rates: [
+ {
+ id: 'standard',
+ name: 'Standard Shipping',
+ price: 'Rp 15,000',
+ transitTime: '3-5 business days',
+ },
+ {
+ id: 'express',
+ name: 'Express Shipping',
+ price: 'Rp 30,000',
+ transitTime: '1-2 business days',
+ },
+ {
+ id: 'free',
+ name: 'Free Shipping',
+ price: 'Free',
+ condition: 'Order total > Rp 500,000',
+ },
+ ],
+ },
+ {
+ id: 'international',
+ name: 'International',
+ regions: 'Rest of world',
+ rates: [
+ {
+ id: 'intl',
+ name: 'International Shipping',
+ price: 'Calculated',
+ transitTime: '7-14 business days',
+ },
+ ],
+ },
+ ]);
+
+ const [localPickup, setLocalPickup] = useState(true);
+ const [showDeliveryEstimates, setShowDeliveryEstimates] = useState(true);
+
+ const handleSave = async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ toast.success('Shipping settings have been updated successfully.');
+ };
-export default function SettingsShipping() {
return (
-
-
{__('Shipping Settings')}
-
- {__('Configure shipping zones, methods, and rates.')}
-
-
-
-
- {__('Shipping settings interface coming soon. This will include zones, methods, and rates configuration.')}
-
+
+ {/* Shipping Zones */}
+
+
+ {zones.map((zone) => (
+
+
+
+
+
+
+
+
{zone.name}
+
+ Regions: {zone.regions}
+
+
+ Rates: {zone.rates.length} shipping rate{zone.rates.length !== 1 ? 's' : ''}
+
+
+
+
+
+ {zone.id !== 'domestic' && (
+
+ )}
+
+
+
+ {/* Shipping Rates */}
+
+ {zone.rates.map((rate) => (
+
+
+
+
+ {rate.name}
+ {rate.transitTime && (
+
+ • {rate.transitTime}
+
+ )}
+ {rate.condition && (
+
+ • {rate.condition}
+
+ )}
+
+
+
{rate.price}
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+ {/* Local Pickup */}
+
+
+
+ {localPickup && (
+
+
+
+
+
Main Store
+
+ Jl. Example No. 123, Jakarta 12345
+
+
+ Mon-Fri: 9:00 AM - 5:00 PM
+
+
+
+
+
+
+ )}
+
+
+ {/* Shipping Options */}
+
+
+
+ { /* TODO: Implement */ }}
+ />
+
+ { /* TODO: Implement */ }}
+ />
+
+
+ {/* Help Card */}
+
+
💡 Shipping tips
+
+ - • Offer free shipping for orders above a certain amount to increase average order value
+ - • Provide multiple shipping options to give customers flexibility
+ - • Set realistic delivery estimates to manage customer expectations
+
-
+
);
}
diff --git a/admin-spa/src/routes/Settings/Store.tsx b/admin-spa/src/routes/Settings/Store.tsx
new file mode 100644
index 0000000..5e10939
--- /dev/null
+++ b/admin-spa/src/routes/Settings/Store.tsx
@@ -0,0 +1,381 @@
+import React, { useState, useEffect } from 'react';
+import { SettingsLayout } from './components/SettingsLayout';
+import { SettingsCard } from './components/SettingsCard';
+import { SettingsSection } from './components/SettingsSection';
+import { Input } from '@/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { toast } from 'sonner';
+
+interface StoreSettings {
+ storeName: string;
+ contactEmail: string;
+ supportEmail: string;
+ phone: string;
+ country: string;
+ address: string;
+ city: string;
+ state: string;
+ postcode: string;
+ currency: string;
+ currencyPosition: 'left' | 'right' | 'left_space' | 'right_space';
+ thousandSep: string;
+ decimalSep: string;
+ decimals: number;
+ timezone: string;
+ weightUnit: string;
+ dimensionUnit: string;
+}
+
+export default function StoreDetailsPage() {
+ const [isLoading, setIsLoading] = useState(true);
+ const [settings, setSettings] = useState
({
+ storeName: '',
+ contactEmail: '',
+ supportEmail: '',
+ phone: '',
+ country: 'ID',
+ address: '',
+ city: '',
+ state: '',
+ postcode: '',
+ currency: 'IDR',
+ currencyPosition: 'left',
+ thousandSep: ',',
+ decimalSep: '.',
+ decimals: 0,
+ timezone: 'Asia/Jakarta',
+ weightUnit: 'kg',
+ dimensionUnit: 'cm',
+ });
+
+ useEffect(() => {
+ // TODO: Load settings from API
+ setTimeout(() => {
+ setSettings({
+ storeName: 'WooNooW Store',
+ contactEmail: 'contact@example.com',
+ supportEmail: 'support@example.com',
+ phone: '+62 812 3456 7890',
+ country: 'ID',
+ address: 'Jl. Example No. 123',
+ city: 'Jakarta',
+ state: 'DKI Jakarta',
+ postcode: '12345',
+ currency: 'IDR',
+ currencyPosition: 'left',
+ thousandSep: '.',
+ decimalSep: ',',
+ decimals: 0,
+ timezone: 'Asia/Jakarta',
+ weightUnit: 'kg',
+ dimensionUnit: 'cm',
+ });
+ setIsLoading(false);
+ }, 500);
+ }, []);
+
+ const handleSave = async () => {
+ // TODO: Save to API
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ toast.success('Your store details have been updated successfully.');
+ };
+
+ const updateSetting = (
+ key: K,
+ value: StoreSettings[K]
+ ) => {
+ setSettings((prev) => ({ ...prev, [key]: value }));
+ };
+
+ // Currency preview
+ const formatCurrency = (amount: number) => {
+ const formatted = amount.toFixed(settings.decimals)
+ .replace('.', settings.decimalSep)
+ .replace(/\B(?=(\d{3})+(?!\d))/g, settings.thousandSep);
+
+ const symbol = settings.currency === 'IDR' ? 'Rp' : settings.currency === 'USD' ? '$' : '€';
+
+ switch (settings.currencyPosition) {
+ case 'left':
+ return `${symbol}${formatted}`;
+ case 'right':
+ return `${formatted}${symbol}`;
+ case 'left_space':
+ return `${symbol} ${formatted}`;
+ case 'right_space':
+ return `${formatted} ${symbol}`;
+ default:
+ return `${symbol}${formatted}`;
+ }
+ };
+
+ return (
+
+ {/* Store Identity */}
+
+
+ updateSetting('storeName', e.target.value)}
+ placeholder="My Awesome Store"
+ />
+
+
+
+ updateSetting('contactEmail', e.target.value)}
+ placeholder="contact@example.com"
+ />
+
+
+
+ updateSetting('supportEmail', e.target.value)}
+ placeholder="support@example.com"
+ />
+
+
+
+ updateSetting('phone', e.target.value)}
+ placeholder="+62 812 3456 7890"
+ />
+
+
+
+ {/* Store Address */}
+
+
+
+
+
+
+ updateSetting('address', e.target.value)}
+ placeholder="Jl. Example No. 123"
+ />
+
+
+
+
+ updateSetting('city', e.target.value)}
+ placeholder="Jakarta"
+ />
+
+
+
+ updateSetting('state', e.target.value)}
+ placeholder="DKI Jakarta"
+ />
+
+
+
+ updateSetting('postcode', e.target.value)}
+ placeholder="12345"
+ />
+
+
+
+
+ {/* Currency & Formatting */}
+
+
+
+
+
+
+
+
+
+
+
+ updateSetting('thousandSep', e.target.value)}
+ maxLength={1}
+ placeholder=","
+ />
+
+
+
+ updateSetting('decimalSep', e.target.value)}
+ maxLength={1}
+ placeholder="."
+ />
+
+
+
+
+
+
+
+ {/* Live Preview */}
+
+
Preview:
+
{formatCurrency(1234567.89)}
+
+
+
+ {/* Standards & Formats */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Summary Card */}
+
+
+ 🇮🇩 Your store is located in {settings.country === 'ID' ? 'Indonesia' : settings.country}
+
+
+ Prices will be displayed in {settings.currency} • Timezone: {settings.timezone}
+
+
+
+ );
+}
diff --git a/admin-spa/src/routes/Settings/components/SettingsCard.tsx b/admin-spa/src/routes/Settings/components/SettingsCard.tsx
new file mode 100644
index 0000000..aaf556a
--- /dev/null
+++ b/admin-spa/src/routes/Settings/components/SettingsCard.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+
+interface SettingsCardProps {
+ title: string;
+ description?: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function SettingsCard({ title, description, children, className = '' }: SettingsCardProps) {
+ return (
+
+
+ {title}
+ {description && {description}}
+
+
+ {children}
+
+
+ );
+}
diff --git a/admin-spa/src/routes/Settings/components/SettingsLayout.tsx b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx
new file mode 100644
index 0000000..556e4b0
--- /dev/null
+++ b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx
@@ -0,0 +1,82 @@
+import React, { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Loader2 } from 'lucide-react';
+
+interface SettingsLayoutProps {
+ title: string;
+ description?: string;
+ children: React.ReactNode;
+ onSave?: () => Promise;
+ saveLabel?: string;
+ isLoading?: boolean;
+}
+
+export function SettingsLayout({
+ title,
+ description,
+ children,
+ onSave,
+ saveLabel = 'Save changes',
+ isLoading = false,
+}: SettingsLayoutProps) {
+ const [isSaving, setIsSaving] = useState(false);
+
+ const handleSave = async () => {
+ if (!onSave) return;
+ setIsSaving(true);
+ try {
+ await onSave();
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+ {/* Sticky Header with Save Button */}
+ {onSave && (
+
+
+
+
{title}
+
+
+
+
+ )}
+
+ {/* Content */}
+
+ {!onSave && (
+
+
{title}
+ {description && (
+
{description}
+ )}
+
+ )}
+
+ {isLoading ? (
+
+
+
+ ) : (
+
{children}
+ )}
+
+
+ );
+}
diff --git a/admin-spa/src/routes/Settings/components/SettingsSection.tsx b/admin-spa/src/routes/Settings/components/SettingsSection.tsx
new file mode 100644
index 0000000..784c2fa
--- /dev/null
+++ b/admin-spa/src/routes/Settings/components/SettingsSection.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Label } from '@/components/ui/label';
+
+interface SettingsSectionProps {
+ label: string;
+ description?: string;
+ required?: boolean;
+ children: React.ReactNode;
+ htmlFor?: string;
+}
+
+export function SettingsSection({
+ label,
+ description,
+ required = false,
+ children,
+ htmlFor,
+}: SettingsSectionProps) {
+ return (
+
+
+ {description && (
+
{description}
+ )}
+
{children}
+
+ );
+}
diff --git a/admin-spa/src/routes/Settings/components/ToggleField.tsx b/admin-spa/src/routes/Settings/components/ToggleField.tsx
new file mode 100644
index 0000000..e5c79f7
--- /dev/null
+++ b/admin-spa/src/routes/Settings/components/ToggleField.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Switch } from '@/components/ui/switch';
+import { Label } from '@/components/ui/label';
+
+interface ToggleFieldProps {
+ id: string;
+ label: string;
+ description?: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+ disabled?: boolean;
+}
+
+export function ToggleField({
+ id,
+ label,
+ description,
+ checked,
+ onCheckedChange,
+ disabled = false,
+}: ToggleFieldProps) {
+ return (
+
+
+
+ {description && (
+
{description}
+ )}
+
+
+
+ );
+}