diff --git a/src/components/admin/CollectionForm.jsx b/src/components/admin/CollectionForm.jsx index afe2746..74dccd0 100644 --- a/src/components/admin/CollectionForm.jsx +++ b/src/components/admin/CollectionForm.jsx @@ -3,7 +3,7 @@ import { Card, CardContent } from '../ui/card' import { Input } from '../ui/input' import { Textarea } from '../ui/textarea' import { Button } from '../ui/button' -import { generateId, normalizeImageUrl } from '../../lib/utils' +import { generateId } from '../../lib/utils' import { adminApiRequest } from '../../lib/auth' import ImageUrlField from './ImageUrlField' import { @@ -102,10 +102,10 @@ export function CollectionForm({ collection, onSave, onCancel }) { return ( <>
-
+
-

{collection ? 'Edit Collection' : 'Create New Collection'}

-

Organize products into collections with names, descriptions, and hero imagery.

+

{collection ? 'Edit Collection' : 'Create New Collection'}

+

Organize products into collections with names, descriptions, and hero imagery.

{driveNotice && ( -

{driveNotice}

+

{driveNotice}

)}
@@ -139,7 +139,7 @@ export function CollectionForm({ collection, onSave, onCancel }) { checked={!!formData.archived} onCheckedChange={(v) => setFormData(prev => ({ ...prev, archived: v }))} /> - +
@@ -162,7 +162,7 @@ export function CollectionForm({ collection, onSave, onCancel }) { onPreview={(src) => setModalImage(src)} hideInput /> -

+

This image will be displayed as a banner on the collection page

@@ -172,18 +172,18 @@ export function CollectionForm({ collection, onSave, onCancel }) {
{modalImage && (
setModalImage(null)}> -
e.stopPropagation()}> +
e.stopPropagation()}> preview
diff --git a/src/components/admin/ProductWorkspace.jsx b/src/components/admin/ProductWorkspace.jsx index 63a7893..af9425e 100644 --- a/src/components/admin/ProductWorkspace.jsx +++ b/src/components/admin/ProductWorkspace.jsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from "react" +import { useNavigate, useParams } from "react-router-dom" import { Card, CardHeader, CardTitle, CardContent } from "../ui/card" import { Button } from "../ui/button" import { Select } from "../ui/select" @@ -6,6 +7,16 @@ import { Input } from "../ui/input" import { Textarea } from "../ui/textarea" import { Switch } from "../ui/switch" import { RefreshCcw, Plus, Save, Trash2, CheckCircle2, AlertTriangle, Package } from "lucide-react" +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from "../ui/alert-dialog" import ImageUrlField from "./ImageUrlField" import VariantImageSelector from "./VariantImageSelector" import { adminApiRequest } from "../../lib/auth" @@ -54,6 +65,8 @@ const mapProductToDraft = (product) => { const hasVariantGroup = (variants, style) => (Array.isArray(variants) && variants.length > 0) || Boolean(style) +const draftFingerprint = (draft) => JSON.stringify(draft || null) + const prepareVariantsForSave = (variants = []) => variants .filter((variant) => variant && (variant.name || variant.selectorImageUrl || variant.displayImageUrl)) @@ -109,7 +122,7 @@ function VariantGroupEditor({ Add option
-
+
Option name
Selector image
Display image
@@ -120,29 +133,41 @@ function VariantGroupEditor({ ) : (
{items.map((variant, index) => ( -
-
+
+
+ onVariantChange(index, "name", event.target.value)} placeholder="Variant name" />
-
+
+ onVariantChange(index, "selectorImageUrl", value)} onPreview={onPreview} />
-
+
+ onVariantChange(index, "displayImageUrl", value)} onPreview={onPreview} />
-
+
onVariantRemove(index)} + aria-label={`Remove ${variant.name || 'variant option'}`} > - X +
@@ -244,6 +270,8 @@ function ProductListItem({ product, isActive, onSelect, collectionName }) { } export function ProductWorkspace() { + const { productId } = useParams() + const navigate = useNavigate() const [products, setProducts] = useState([]) const [collections, setCollections] = useState([]) const [collectionFilter, setCollectionFilter] = useState('all') @@ -251,6 +279,7 @@ export function ProductWorkspace() { const [searchTerm, setSearchTerm] = useState('') const [selectedId, setSelectedId] = useState(null) const [draft, setDraft] = useState(null) + const [baselineDraft, setBaselineDraft] = useState(null) const [isCreating, setIsCreating] = useState(false) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) @@ -259,6 +288,11 @@ export function ProductWorkspace() { const [variants1Enabled, setVariants1Enabled] = useState(false) const [variants2Enabled, setVariants2Enabled] = useState(false) const [storeSettings, setStoreSettings] = useState(null) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [pendingSelection, setPendingSelection] = useState(null) + const [unsavedDialogOpen, setUnsavedDialogOpen] = useState(false) + + const isDirty = draftFingerprint(draft) !== draftFingerprint(baselineDraft) const collectionLookup = useMemo(() => { const map = new Map() @@ -308,9 +342,12 @@ export function ProductWorkspace() { const productData = await productsRes.json() setProducts(productData) - if (!isCreating) { + if (!isCreating && productId !== 'new') { if (productData.length > 0) { setSelectedId((prev) => { + if (productId && productData.some((product) => product.id === productId)) { + return productId + } if (prev && productData.some((product) => product.id === prev)) { return prev } @@ -319,6 +356,7 @@ export function ProductWorkspace() { } else { setSelectedId(null) setDraft(null) + setBaselineDraft(null) } } @@ -361,6 +399,28 @@ export function ProductWorkspace() { load() }, []) + useEffect(() => { + if (productId === 'new') { + if (!isCreating) { + const blank = createEmptyProduct() + setDraft(blank) + setBaselineDraft(blank) + setIsCreating(true) + setSelectedId(null) + setVariants1Enabled(false) + setVariants2Enabled(false) + setStatus(null) + } + return + } + + if (productId && products.some((product) => product.id === productId)) { + setIsCreating(false) + setSelectedId(productId) + setStatus(null) + } + }, [productId, products, isCreating]) + useEffect(() => { if (isCreating) return if (filteredProducts.length === 0) { @@ -369,24 +429,29 @@ export function ProductWorkspace() { } if (draft) { setDraft(null) + setBaselineDraft(null) } return } if (!selectedId || !filteredProducts.some((product) => product.id === selectedId)) { - setSelectedId(filteredProducts[0].id) + const nextId = filteredProducts[0].id + setSelectedId(nextId) + if (!productId) navigate(`/admin/products/${nextId}`, { replace: true }) } - }, [filteredProducts, isCreating, selectedId, draft]) + }, [filteredProducts, isCreating, selectedId, draft, productId, navigate]) useEffect(() => { if (isCreating) return if (!selectedId) { setDraft(null) + setBaselineDraft(null) return } const selectedProduct = products.find((product) => product.id === selectedId) if (selectedProduct) { const nextDraft = mapProductToDraft(selectedProduct) setDraft(nextDraft) + setBaselineDraft(nextDraft) setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle)) setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2)) } @@ -411,19 +476,55 @@ export function ProductWorkspace() { } const startCreate = () => { + if (isDirty) { + setPendingSelection('new') + setUnsavedDialogOpen(true) + return + } const blank = createEmptyProduct() setDraft(blank) + setBaselineDraft(blank) setIsCreating(true) setSelectedId(null) setVariants1Enabled(false) setVariants2Enabled(false) setStatus(null) + navigate('/admin/products/new') } const handleSelectProduct = (productId) => { + if (isDirty) { + setPendingSelection(productId) + setUnsavedDialogOpen(true) + return + } setIsCreating(false) setSelectedId(productId) setStatus(null) + navigate(`/admin/products/${productId}`) + } + + const confirmPendingSelection = () => { + const next = pendingSelection + setPendingSelection(null) + setUnsavedDialogOpen(false) + if (next === 'new') { + const blank = createEmptyProduct() + setDraft(blank) + setBaselineDraft(blank) + setIsCreating(true) + setSelectedId(null) + setVariants1Enabled(false) + setVariants2Enabled(false) + navigate('/admin/products/new') + return + } + if (next) { + setIsCreating(false) + setSelectedId(next) + setStatus(null) + navigate(`/admin/products/${next}`) + } } const handleDraftChange = (key, value) => { @@ -520,19 +621,24 @@ export function ProductWorkspace() { setIsCreating(false) if (filteredProducts.length > 0) { setSelectedId(filteredProducts[0].id) + navigate(`/admin/products/${filteredProducts[0].id}`) } else { setDraft(null) + setBaselineDraft(null) + navigate('/admin/products') } return } if (!selectedId) { setDraft(null) + setBaselineDraft(null) return } const original = products.find((product) => product.id === selectedId) if (original) { const nextDraft = mapProductToDraft(original) setDraft(nextDraft) + setBaselineDraft(nextDraft) setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle)) setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2)) } @@ -596,9 +702,11 @@ export function ProductWorkspace() { setSelectedId(savedProduct.id) const nextDraft = mapProductToDraft(savedProduct) setDraft(nextDraft) + setBaselineDraft(nextDraft) setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle)) setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2)) setStatus({ type: 'success', message: isNew ? 'Product created' : 'Product updated' }) + navigate(`/admin/products/${savedProduct.id}`, { replace: true }) // Refresh store settings after creating a product to update limit status if (isNew) { @@ -614,8 +722,7 @@ export function ProductWorkspace() { const handleDelete = async () => { if (!draft?.id || isCreating) return - const confirmed = window.confirm('Delete this product? This action cannot be undone.') - if (!confirmed) return + setDeleteDialogOpen(false) try { const response = await adminApiRequest(`/api/admin/products/${draft.id}`, { method: 'DELETE', @@ -625,9 +732,11 @@ export function ProductWorkspace() { } setProducts((prev) => prev.filter((product) => product.id !== draft.id)) setDraft(null) + setBaselineDraft(null) setSelectedId(null) setIsCreating(false) setStatus({ type: 'success', message: 'Product deleted' }) + navigate('/admin/products', { replace: true }) // Refresh store settings after deleting a product to update limit status await fetchStoreSettings() @@ -798,16 +907,18 @@ export function ProductWorkspace() { {draft ? (
- +
{draft.name || (isCreating ? 'New product draft' : 'Untitled product')}

- {isCreating - ? 'Draft will be created once you save changes.' - : 'Changes are saved to the live product.'} + {isDirty + ? 'Unsaved changes' + : isCreating + ? 'Draft is ready for edits.' + : 'All changes saved.'}

@@ -815,7 +926,7 @@ export function ProductWorkspace() { )} - -
)} + + + + + Delete product? + + {draft?.name || 'This product'} will be permanently removed from the catalog. This cannot be undone. + + + + Cancel + + Delete product + + + + + + + + + Discard unsaved changes? + + Switching products will discard edits that have not been saved. + + + + Keep editing + + Discard changes + + + +
) } diff --git a/src/components/admin/StoreSettingsEditor.jsx b/src/components/admin/StoreSettingsEditor.jsx index a5a872f..8377421 100644 --- a/src/components/admin/StoreSettingsEditor.jsx +++ b/src/components/admin/StoreSettingsEditor.jsx @@ -95,6 +95,7 @@ export function StoreSettingsEditor() { businessPostalCode: '', businessCountry: '' }) + const [settingsBaseline, setSettingsBaseline] = useState(null) const [saving, setSaving] = useState(false) const [modalImage, setModalImage] = useState(null) const [savedOpen, setSavedOpen] = useState(false) @@ -114,6 +115,10 @@ export function StoreSettingsEditor() { const previewTheme = useMemo(() => resolveStorefrontTheme(themeState), [themeState]) const previewStyles = useMemo(() => ({ ...previewTheme.cssVariables }), [previewTheme]) + const settingsDirty = useMemo( + () => JSON.stringify(settings) !== JSON.stringify(settingsBaseline || settings), + [settings, settingsBaseline] + ) const selectedFontOption = useMemo( () => FONT_OPTIONS.find((font) => font.id === themeState.typography.fontId) || FONT_OPTIONS[0], [themeState.typography.fontId] @@ -155,15 +160,19 @@ export function StoreSettingsEditor() { const response = await fetch('/api/store-settings') if (response.ok) { const data = await response.json() - setSettings(prev => ({ - ...prev, - ...data, - // Ensure about page fields have defaults if missing - aboutHeroImageUrl: data.aboutHeroImageUrl || '', - aboutHeroTitle: data.aboutHeroTitle || prev.aboutHeroTitle, - aboutHeroSubtitle: data.aboutHeroSubtitle || prev.aboutHeroSubtitle, - aboutContent: data.aboutContent || prev.aboutContent || '' - })) + setSettings(prev => { + const nextSettings = { + ...prev, + ...data, + // Ensure about page fields have defaults if missing + aboutHeroImageUrl: data.aboutHeroImageUrl || '', + aboutHeroTitle: data.aboutHeroTitle || prev.aboutHeroTitle, + aboutHeroSubtitle: data.aboutHeroSubtitle || prev.aboutHeroSubtitle, + aboutContent: data.aboutContent || prev.aboutContent || '' + } + setSettingsBaseline(nextSettings) + return nextSettings + }) } } catch (error) { console.error('Error fetching store settings:', error) @@ -341,6 +350,7 @@ export function StoreSettingsEditor() { if (response.ok) { const updatedSettings = await response.json() setSettings(updatedSettings) + setSettingsBaseline(updatedSettings) setSavedOpen(true) } else { const error = await response.json() @@ -519,7 +529,7 @@ export function StoreSettingsEditor() { onPreview={(src) => setModalImage(src)} hideInput /> - {driveNotice &&

{driveNotice}

} + {driveNotice &&

{driveNotice}

}

Recommended size: 200x50px

)} @@ -592,7 +602,7 @@ export function StoreSettingsEditor() { value={settings.aboutContent} onChange={handleChange} rows={8} - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent text-sm" + className="w-full px-3 py-2 border border-[var(--admin-border-primary)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--admin-accent)] focus:border-transparent text-sm bg-[var(--admin-bg-elevated)] text-[var(--admin-text-primary)]" />
@@ -624,15 +634,27 @@ export function StoreSettingsEditor() {
{(themeMessage || themeError) && ( -
+
{themeError || themeMessage}
)}
- {themeDirty ? 'Unsaved changes' : themeHasOverrides ? 'Custom theme saved' : 'Using default theme'} -
@@ -703,10 +725,10 @@ export function StoreSettingsEditor() { {/* Modals */} {modalImage && (
setModalImage(null)}> -
e.stopPropagation()}> +
e.stopPropagation()}> preview -
diff --git a/src/components/ui/alert-dialog.jsx b/src/components/ui/alert-dialog.jsx index c5eff55..ae36d99 100644 --- a/src/components/ui/alert-dialog.jsx +++ b/src/components/ui/alert-dialog.jsx @@ -1,4 +1,5 @@ import * as React from "react" +import { cn } from "../../lib/utils" // Minimal shadcn-style AlertDialog for this project (no portals for simplicity) @@ -27,13 +28,18 @@ export function AlertDialogTrigger({ asChild = false, children }) { : } -export function AlertDialogContent({ children }) { +export function AlertDialogContent({ children, className }) { const { open, setOpen } = React.useContext(AlertDialogContext) if (!open) return null return (
-
setOpen(false)} /> -
+
setOpen(false)} /> +
{children}
@@ -45,23 +51,27 @@ export function AlertDialogHeader({ children }) { } export function AlertDialogTitle({ children }) { - return

{children}

+ return

{children}

} export function AlertDialogDescription({ children }) { - return

{children}

+ return

{children}

} export function AlertDialogFooter({ children }) { return
{children}
} -export function AlertDialogCancel({ children, onClick }) { +export function AlertDialogCancel({ children, onClick, disabled, className }) { const { setOpen } = React.useContext(AlertDialogContext) return ( diff --git a/src/pages/admin/AdminDashboard.jsx b/src/pages/admin/AdminDashboard.jsx index 9de7b06..a4bd1fb 100644 --- a/src/pages/admin/AdminDashboard.jsx +++ b/src/pages/admin/AdminDashboard.jsx @@ -55,7 +55,9 @@ export function AdminDashboard() { }> } /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/admin/AdminLayout.jsx b/src/pages/admin/AdminLayout.jsx index 760aeda..2c6e8e3 100644 --- a/src/pages/admin/AdminLayout.jsx +++ b/src/pages/admin/AdminLayout.jsx @@ -8,13 +8,14 @@ import { ShoppingBag, Image as ImageIcon, LogOut, + Store, } from 'lucide-react' const menuItems = [ { path: '/admin', label: 'Dashboard', icon: Home, end: true }, { path: '/admin/products', label: 'Products', icon: Package }, { path: '/admin/collections', label: 'Collections', icon: FolderOpen }, - { path: '/admin/Fulfillment', label: 'Fulfillment', icon: ShoppingBag }, + { path: '/admin/fulfillment', label: 'Fulfillment', icon: ShoppingBag }, { path: '/admin/media', label: 'Media', icon: ImageIcon }, { path: '/admin/store-settings', label: 'Store Settings', icon: Settings }, ] @@ -30,19 +31,20 @@ export function AdminLayout({ onLogout }) { const location = useLocation() return ( -
-
+
+

OpenShop Admin

- ← Back to Store + + Back to Store
-
+
diff --git a/src/pages/admin/CollectionsPage.jsx b/src/pages/admin/CollectionsPage.jsx index d3d14c6..95dea2c 100644 --- a/src/pages/admin/CollectionsPage.jsx +++ b/src/pages/admin/CollectionsPage.jsx @@ -6,12 +6,23 @@ import { CollectionForm } from '../../components/admin/CollectionForm' import { adminApiRequest } from '../../lib/auth' import { FolderOpen, Plus, Edit, Trash2 } from 'lucide-react' import { Switch } from '../../components/ui/switch' +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from '../../components/ui/alert-dialog' export function CollectionsPage() { const [collections, setCollections] = useState([]) const [showForm, setShowForm] = useState(false) const [editingCollection, setEditingCollection] = useState(null) const [archivedFilter, setArchivedFilter] = useState('all') + const [collectionToDelete, setCollectionToDelete] = useState(null) useEffect(() => { fetchCollections() @@ -40,8 +51,6 @@ export function CollectionsPage() { } const handleDeleteCollection = async (collectionId) => { - if (!confirm('Are you sure you want to delete this collection?')) return - try { const response = await adminApiRequest(`/api/admin/collections/${collectionId}`, { method: 'DELETE', @@ -49,6 +58,7 @@ export function CollectionsPage() { if (response.ok) { setCollections(collections.filter((c) => c.id !== collectionId)) + setCollectionToDelete(null) } } catch (error) { console.error('Error deleting collection:', error) @@ -152,7 +162,7 @@ export function CollectionsPage() { variant="destructive" size="sm" className="h-8 px-2.5" - onClick={() => handleDeleteCollection(collection.id)} + onClick={() => setCollectionToDelete(collection)} > @@ -163,6 +173,26 @@ export function CollectionsPage() { ))}
)} + + !open && setCollectionToDelete(null)}> + + + Delete collection? + + {collectionToDelete?.name || 'This collection'} will be removed from admin and storefront organization. Products in it will remain in the catalog. + + + + Cancel + handleDeleteCollection(collectionToDelete.id)} + > + Delete collection + + + +
) } diff --git a/src/pages/admin/DashboardPage.jsx b/src/pages/admin/DashboardPage.jsx index 20affba..17ff94a 100644 --- a/src/pages/admin/DashboardPage.jsx +++ b/src/pages/admin/DashboardPage.jsx @@ -148,7 +148,7 @@ export function DashboardPage() { {formatCurrency(product.price, product.currency)}

- + + ))}
diff --git a/src/pages/admin/MediaLibraryPage.jsx b/src/pages/admin/MediaLibraryPage.jsx index b155e79..d39f3e6 100644 --- a/src/pages/admin/MediaLibraryPage.jsx +++ b/src/pages/admin/MediaLibraryPage.jsx @@ -3,13 +3,25 @@ import { Button } from '../../components/ui/button' import { Card, CardContent } from '../../components/ui/card' import { normalizeImageUrl } from '../../lib/utils' import { adminApiRequest } from '../../lib/auth' -import { Plus, Trash2, Image as ImageIcon, X } from 'lucide-react' +import { Plus, Trash2, Image as ImageIcon, X, Copy } from 'lucide-react' import AddMediaModal from '../../components/admin/AddMediaModal' +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from '../../components/ui/alert-dialog' export function MediaLibraryPage() { const [media, setMedia] = useState([]) const [selected, setSelected] = useState(null) const [addOpen, setAddOpen] = useState(false) + const [mediaToDelete, setMediaToDelete] = useState(null) + const [status, setStatus] = useState('') useEffect(() => { load() @@ -28,17 +40,28 @@ export function MediaLibraryPage() { } async function handleDelete(id) { - if (!confirm('Remove this media from the library?')) return try { const res = await adminApiRequest(`/api/admin/media/${id}`, { method: 'DELETE' }) if (res.ok) { setMedia(media.filter((m) => m.id !== id)) + if (selected?.id === id) setSelected(null) + setMediaToDelete(null) + setStatus('Media removed') } } catch (e) { console.error('Delete media failed', e) } } + async function copyUrl(url) { + try { + await navigator.clipboard.writeText(url) + setStatus('Media URL copied') + } catch { + setStatus('Unable to copy URL') + } + } + return (
@@ -54,6 +77,12 @@ export function MediaLibraryPage() {
+ {status && ( +
+ {status} +
+ )} + {media.length === 0 ? ( @@ -82,8 +111,11 @@ export function MediaLibraryPage() { className="w-full h-28 object-cover" /> -
- +
@@ -128,6 +160,20 @@ export function MediaLibraryPage() { > Open original + +
@@ -143,6 +189,23 @@ export function MediaLibraryPage() { setAddOpen(false) }} /> + + !open && setMediaToDelete(null)}> + + + Delete media? + + {mediaToDelete?.filename || 'This media item'} will be removed from the library. Existing product or page references may stop displaying if they use this URL. + + + + Cancel + handleDelete(mediaToDelete.id)}> + Delete media + + + +
) } diff --git a/src/routes/admin/analytics.js b/src/routes/admin/analytics.js index 64143a5..16380dc 100644 --- a/src/routes/admin/analytics.js +++ b/src/routes/admin/analytics.js @@ -23,6 +23,7 @@ router.get('/orders', asyncHandler(async (c) => { const direction = c.req.query('direction') || 'next' const cursor = c.req.query('cursor') || undefined const showFulfilled = c.req.query('showFulfilled') === 'true' + const fulfillmentStatus = c.req.query('status') || (showFulfilled ? 'fulfilled' : 'open') const stripeService = new StripeService(c.env.STRIPE_SECRET_KEY, c.env.SITE_URL) const analyticsService = new AnalyticsService(stripeService) @@ -33,6 +34,7 @@ router.get('/orders', asyncHandler(async (c) => { direction, cursor, showFulfilled, + fulfillmentStatus, kvNamespace }) diff --git a/src/services/AnalyticsService.js b/src/services/AnalyticsService.js index a1d9c92..6d75346 100644 --- a/src/services/AnalyticsService.js +++ b/src/services/AnalyticsService.js @@ -127,6 +127,7 @@ export class AnalyticsService { const direction = options.direction || 'next' const cursor = options.cursor const showFulfilled = options.showFulfilled === true + const fulfillmentFilter = options.fulfillmentStatus || (showFulfilled ? 'fulfilled' : 'open') const listParams = { limit } if (cursor) { @@ -165,7 +166,9 @@ export class AnalyticsService { const fulfillmentData = fulfillmentMap.get(s.id) const fulfillmentStatus = fulfillmentData ? JSON.parse(fulfillmentData) : { fulfilled: false } - if (showFulfilled) { + if (fulfillmentFilter === 'all') { + includeSession = true + } else if (fulfillmentFilter === 'fulfilled') { if (!fulfillmentStatus.fulfilled) { includeSession = false }