fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout

This commit is contained in:
Dwindi Ramadhana
2026-02-05 00:09:40 +07:00
parent a0b5f8496d
commit 5f08c18ec7
77 changed files with 7027 additions and 4546 deletions

View File

@@ -39,7 +39,7 @@ function StatusBadge({ status }: { status?: string }) {
return <span className={`${cls} ${tone}`}>{status ? status[0].toUpperCase() + status.slice(1) : '—'}</span>;
}
const STATUS_OPTIONS = ['pending', 'processing', 'completed', 'on-hold', 'cancelled', 'refunded', 'failed'];
const STATUS_OPTIONS = ['pending', 'processing', 'completed', 'on-hold', 'cancelled', 'refunded', 'failed', 'draft'];
export default function OrderShow() {
const { id } = useParams<{ id: string }>();
@@ -315,6 +315,69 @@ export default function OrderShow() {
</div>
)}
{/* Related Items (Subscription & Licenses) */}
{(order.related_subscription || (order.related_licenses && order.related_licenses.length > 0)) && (
<div className="rounded border overflow-hidden">
<div className="px-4 py-3 border-b font-medium">{__('Related Items')}</div>
<div className="p-4 space-y-4">
{/* Related Subscription */}
{order.related_subscription && (
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium flex items-center gap-2">
<RefreshCw className="w-4 h-4 text-muted-foreground" />
{__('Subscription')}
</div>
<div className="text-xs text-muted-foreground mt-1">
{order.related_subscription.billing_schedule} <span className="capitalize">{order.related_subscription.status}</span>
</div>
</div>
<Link to={`/subscriptions/${order.related_subscription.id}`}>
<Button variant="outline" size="sm" className="h-8">
#{order.related_subscription.id}
</Button>
</Link>
</div>
)}
{/* Separator if both exist */}
{order.related_subscription && order.related_licenses && order.related_licenses.length > 0 && (
<div className="border-t"></div>
)}
{/* Related Licenses */}
{order.related_licenses && order.related_licenses.length > 0 && (
<div className="space-y-3">
{order.related_licenses.map((lic: any) => (
<div key={lic.id} className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="text-sm font-medium flex items-center gap-2">
<Ticket className="w-4 h-4 text-muted-foreground" />
{__('License Key')}
</div>
<div className="text-xs font-mono bg-gray-100 px-1.5 py-0.5 rounded mt-1.5 inline-block break-all select-all">
{lic.license_key}
</div>
<div className="text-xs text-muted-foreground mt-1 truncate">
{lic.product_name}
</div>
</div>
<div className="text-right">
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium border uppercase ${lic.status === 'active' ? 'bg-green-100 text-green-800 border-green-200' :
lic.status === 'expired' ? 'bg-red-100 text-red-800 border-red-200' :
'bg-gray-100 text-gray-700 border-gray-200'
}`}>
{lic.status}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Items */}
<div className="rounded border overflow-hidden">
<div className="px-4 py-3 border-b font-medium">{__('Items')}</div>

View File

@@ -1,24 +1,31 @@
import React, { useState, useCallback } from 'react';
import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Filter, PackageOpen, Trash2, RefreshCw } from 'lucide-react';
import { Filter, PackageOpen, Trash2, RefreshCw, MoreHorizontal, Eye, Edit } from 'lucide-react';
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
import { __ } from '@/lib/i18n';
import { useFABConfig } from '@/hooks/useFABConfig';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { toast } from 'sonner';
import {
Select,
@@ -94,8 +101,8 @@ export default function Orders() {
const [status, setStatus] = useState<string | undefined>(initial.status || undefined);
const [dateStart, setDateStart] = useState<string | undefined>(initial.date_start || undefined);
const [dateEnd, setDateEnd] = useState<string | undefined>(initial.date_end || undefined);
const [orderby, setOrderby] = useState<'date'|'id'|'modified'|'total'>((initial.orderby as any) || 'date');
const [order, setOrder] = useState<'asc'|'desc'>((initial.order as any) || 'desc');
const [orderby, setOrderby] = useState<'date' | 'id' | 'modified' | 'total'>((initial.orderby as any) || 'date');
const [order, setOrder] = useState<'asc' | 'desc'>((initial.order as any) || 'desc');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
@@ -136,7 +143,7 @@ export default function Orders() {
const rows = data?.rows;
if (!rows) return [];
if (!searchQuery.trim()) return rows;
const query = searchQuery.toLowerCase();
return rows.filter((order: any) =>
order.number?.toString().includes(query) ||
@@ -255,8 +262,8 @@ export default function Orders() {
{__('Delete')} ({selectedIds.length})
</button>
)}
<button
<button
className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2"
onClick={handleRefresh}
disabled={q.isLoading || isRefreshing}
@@ -305,7 +312,7 @@ export default function Orders() {
setOrder((v.order ?? 'desc') as 'asc' | 'desc');
}}
/>
{activeFiltersCount > 0 && (
<button
className="text-sm text-muted-foreground hover:text-foreground underline text-nowrap"
@@ -432,7 +439,7 @@ export default function Orders() {
/>
</td>
<td className="p-3">
<Link className="underline underline-offset-2" to={`/orders/${row.id}`}>#{row.number}</Link>
<Link className="font-medium hover:underline" to={`/orders/${row.id}`}>#{row.number}</Link>
</td>
<td className="p-3 min-w-32">
<span title={row.date ?? ""}>
@@ -454,9 +461,36 @@ export default function Orders() {
decimals: store.decimals,
})}
</td>
<td className="p-3 text-center space-x-2">
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}`}>{__('Open')}</Link>
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}/edit`}>{__('Edit')}</Link>
<td className="p-3 text-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{__('Open menu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => nav(`/orders/${row.id}`)}>
<Eye className="mr-2 h-4 w-4" />
{__('View Details')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => nav(`/orders/${row.id}/edit`)}>
<Edit className="mr-2 h-4 w-4" />
{__('Edit Order')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => {
setSelectedIds([row.id]);
setShowDeleteDialog(true);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
{__('Delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}