feat: Tax settings + unified addon guide + Biteship spec

## 1. Created BITESHIP_ADDON_SPEC.md 
- Complete plugin specification
- Database schema, API endpoints
- WooCommerce integration
- React components
- Implementation timeline

## 2. Merged Addon Documentation 
Created ADDON_DEVELOPMENT_GUIDE.md (single source of truth):
- Merged ADDON_INJECTION_GUIDE.md + ADDON_HOOK_SYSTEM.md
- Two addon types: Route Injection + Hook System
- Clear examples for each type
- Best practices and troubleshooting
- Deleted old documents

## 3. Tax Settings 
Frontend (admin-spa/src/routes/Settings/Tax.tsx):
- Enable/disable tax calculation toggle
- Display standard/reduced/zero tax rates
- Show tax options (prices include tax, based on, display)
- Link to WooCommerce for advanced config
- Clean, simple UI

Backend (includes/Api/TaxController.php):
- GET /settings/tax - Fetch tax settings
- POST /settings/tax/toggle - Enable/disable taxes
- Fetches rates from woocommerce_tax_rates table
- Clears WooCommerce cache on update

## 4. Advanced Local Pickup - TODO
Will be simple: Admin adds multiple pickup locations

## Key Decisions:
 Hook system = No hardcoding, zero coupling
 Tax settings = Simple toggle + view, advanced in WC
 Single addon guide = One source of truth

Next: Advanced Local Pickup locations
This commit is contained in:
dwindown
2025-11-09 23:13:52 +07:00
parent 17afd3911f
commit 603d94b73c
8 changed files with 1447 additions and 1306 deletions

View File

@@ -196,6 +196,7 @@ import SettingsIndex from '@/routes/Settings';
import SettingsStore from '@/routes/Settings/Store';
import SettingsPayments from '@/routes/Settings/Payments';
import SettingsShipping from '@/routes/Settings/Shipping';
import SettingsTax from '@/routes/Settings/Tax';
import MorePage from '@/routes/More';
// Addon Route Component - Dynamically loads addon components
@@ -426,7 +427,7 @@ function AppRoutes() {
<Route path="/settings/store" element={<SettingsStore />} />
<Route path="/settings/payments" element={<SettingsPayments />} />
<Route path="/settings/shipping" element={<SettingsShipping />} />
<Route path="/settings/taxes" element={<SettingsIndex />} />
<Route path="/settings/tax" element={<SettingsTax />} />
<Route path="/settings/checkout" element={<SettingsIndex />} />
<Route path="/settings/customers" element={<SettingsIndex />} />
<Route path="/settings/notifications" element={<SettingsIndex />} />

View File

@@ -0,0 +1,309 @@
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { SettingsLayout } from './components/SettingsLayout';
import { SettingsCard } from './components/SettingsCard';
import { ToggleField } from './components/ToggleField';
import { Button } from '@/components/ui/button';
import { ExternalLink, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
export default function TaxSettings() {
const queryClient = useQueryClient();
const wcAdminUrl = (window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin';
// Fetch tax settings
const { data: settings, isLoading, refetch } = useQuery({
queryKey: ['tax-settings'],
queryFn: () => api.get('/settings/tax'),
});
// Toggle tax calculation
const toggleMutation = useMutation({
mutationFn: async (enabled: boolean) => {
return api.post('/settings/tax/toggle', { enabled });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tax-settings'] });
toast.success(__('Tax settings updated'));
},
onError: (error: any) => {
toast.error(error?.message || __('Failed to update tax settings'));
},
});
if (isLoading) {
return (
<SettingsLayout
title={__('Tax')}
description={__('Configure tax calculation and rates')}
>
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
</SettingsLayout>
);
}
return (
<SettingsLayout
title={__('Tax')}
description={__('Configure tax calculation and rates')}
action={
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
>
<RefreshCw className="h-4 w-4 mr-2" />
{__('Refresh')}
</Button>
}
>
<div className="space-y-6">
{/* Enable Tax Calculation */}
<SettingsCard
title={__('Tax Calculation')}
description={__('Enable or disable tax calculation for your store')}
>
<ToggleField
label={__('Enable tax rates and calculations')}
description={__('When enabled, taxes will be calculated based on customer location and product tax class')}
checked={settings?.calc_taxes === 'yes'}
onChange={(checked) => toggleMutation.mutate(checked)}
disabled={toggleMutation.isPending}
/>
</SettingsCard>
{/* Tax Rates */}
{settings?.calc_taxes === 'yes' && (
<SettingsCard
title={__('Tax Rates')}
description={__('Configure tax rates for different locations and tax classes')}
>
<div className="space-y-4">
<div className="rounded-lg border p-4 bg-muted/50">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="font-medium mb-1">{__('Standard Rates')}</h3>
<p className="text-sm text-muted-foreground">
{__('Tax rates applied to standard products')}
</p>
{settings?.standard_rates && settings.standard_rates.length > 0 ? (
<div className="mt-3 space-y-2">
{settings.standard_rates.map((rate: any, index: number) => (
<div key={index} className="flex items-center justify-between text-sm">
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
<span className="font-medium">{rate.rate}%</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground mt-2">
{__('No standard rates configured')}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax&section=standard`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4 mr-2" />
{__('Manage')}
</a>
</Button>
</div>
</div>
<div className="rounded-lg border p-4 bg-muted/50">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="font-medium mb-1">{__('Reduced Rates')}</h3>
<p className="text-sm text-muted-foreground">
{__('Lower tax rates for specific products')}
</p>
{settings?.reduced_rates && settings.reduced_rates.length > 0 ? (
<div className="mt-3 space-y-2">
{settings.reduced_rates.map((rate: any, index: number) => (
<div key={index} className="flex items-center justify-between text-sm">
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
<span className="font-medium">{rate.rate}%</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground mt-2">
{__('No reduced rates configured')}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax&section=reduced-rate`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4 mr-2" />
{__('Manage')}
</a>
</Button>
</div>
</div>
<div className="rounded-lg border p-4 bg-muted/50">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="font-medium mb-1">{__('Zero Rates')}</h3>
<p className="text-sm text-muted-foreground">
{__('No tax for specific products or locations')}
</p>
{settings?.zero_rates && settings.zero_rates.length > 0 ? (
<div className="mt-3 space-y-2">
{settings.zero_rates.map((rate: any, index: number) => (
<div key={index} className="flex items-center justify-between text-sm">
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
<span className="font-medium">0%</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground mt-2">
{__('No zero rates configured')}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax&section=zero-rate`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4 mr-2" />
{__('Manage')}
</a>
</Button>
</div>
</div>
</div>
</SettingsCard>
)}
{/* Tax Options */}
{settings?.calc_taxes === 'yes' && (
<SettingsCard
title={__('Tax Options')}
description={__('Additional tax calculation settings')}
>
<div className="space-y-3">
<div className="flex items-center justify-between py-2">
<div>
<p className="font-medium text-sm">{__('Prices entered with tax')}</p>
<p className="text-xs text-muted-foreground">
{settings?.prices_include_tax === 'yes'
? __('Product prices include tax')
: __('Product prices exclude tax')}
</p>
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
target="_blank"
rel="noopener noreferrer"
>
{__('Change')}
</a>
</Button>
</div>
<div className="flex items-center justify-between py-2 border-t">
<div>
<p className="font-medium text-sm">{__('Calculate tax based on')}</p>
<p className="text-xs text-muted-foreground">
{settings?.tax_based_on === 'shipping' && __('Customer shipping address')}
{settings?.tax_based_on === 'billing' && __('Customer billing address')}
{settings?.tax_based_on === 'base' && __('Shop base address')}
</p>
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
target="_blank"
rel="noopener noreferrer"
>
{__('Change')}
</a>
</Button>
</div>
<div className="flex items-center justify-between py-2 border-t">
<div>
<p className="font-medium text-sm">{__('Display prices in shop')}</p>
<p className="text-xs text-muted-foreground">
{settings?.tax_display_shop === 'incl' && __('Including tax')}
{settings?.tax_display_shop === 'excl' && __('Excluding tax')}
</p>
</div>
<Button
variant="outline"
size="sm"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
target="_blank"
rel="noopener noreferrer"
>
{__('Change')}
</a>
</Button>
</div>
</div>
</SettingsCard>
)}
{/* Advanced Settings Link */}
<div className="rounded-lg border border-dashed p-6 text-center">
<p className="text-sm text-muted-foreground mb-4">
{__('For advanced tax configuration, use the WooCommerce settings page')}
</p>
<Button
variant="outline"
asChild
>
<a
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4 mr-2" />
{__('Open Tax Settings in WooCommerce')}
</a>
</Button>
</div>
</div>
</SettingsLayout>
);
}