feat: Implement Create Subscription page and Coffee management hooks

This commit is contained in:
seaznCode 2025-11-13 20:13:27 +01:00
parent 6f8573fe16
commit ea1cba42bd
4 changed files with 547 additions and 0 deletions

View File

@ -0,0 +1,180 @@
"use client";
import React, { useMemo, useState } from 'react';
import PageLayout from '../../../components/PageLayout';
import useCoffeeManagement from '../hooks/useCoffeeManagement';
import { PhotoIcon } from '@heroicons/react/24/solid';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function CreateSubscriptionPage() {
const { createProduct } = useCoffeeManagement();
const router = useRouter();
const [error, setError] = useState<string | null>(null);
// form state
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [quantity, setQuantity] = useState(1);
const [price, setPrice] = useState(0);
const [state, setState] = useState<'available'|'unavailable'>('available');
const [pictureFile, setPictureFile] = useState<File | undefined>(undefined);
const [currency, setCurrency] = useState('EUR');
const [taxRate, setTaxRate] = useState<number | undefined>(undefined);
const [isFeatured, setIsFeatured] = useState(false);
const [billingInterval, setBillingInterval] = useState<'day'|'week'|'month'|'year'|''>('');
const [intervalCount, setIntervalCount] = useState<number | undefined>(undefined);
const [sku, setSku] = useState<string>('');
const [slug, setSlug] = useState<string>('');
const onCreate = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
const normalizedIntervalCount = billingInterval ? (intervalCount && intervalCount > 0 ? intervalCount : 1) : undefined;
await createProduct({
title,
description,
quantity,
price,
currency,
tax_rate: taxRate,
is_featured: isFeatured,
billing_interval: billingInterval || undefined,
interval_count: normalizedIntervalCount,
sku: sku || undefined,
slug: slug || undefined,
state: state === 'available',
pictureFile
});
router.push('/admin/subscriptions');
} catch (e: any) {
setError(e.message || 'Failed to create');
}
};
return (
<PageLayout>
<div className="min-h-screen bg-gray-50">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Create Subscription</h1>
<p className="text-sm sm:text-base text-gray-700 mt-2">Add a new product or subscription plan.</p>
</div>
<Link href="/admin/subscriptions" className="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Back to list
</Link>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<form onSubmit={onCreate} className="space-y-8">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-900">Title</label>
<input id="title" name="title" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} />
</div>
<div>
<label htmlFor="quantity" className="block text-sm font-medium text-gray-900">Quantity</label>
<input id="quantity" name="quantity" required min={1} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Quantity" type="number" value={quantity} onChange={e => setQuantity(Number(e.target.value))} />
</div>
<div>
<label htmlFor="price" className="block text-sm font-medium text-gray-900">Price</label>
<input id="price" name="price" required min={0.01} step={0.01} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Price" type="number" value={price} onChange={e => setPrice(Number(e.target.value))} />
</div>
<div>
<label htmlFor="currency" className="block text-sm font-medium text-gray-900">Currency (e.g., EUR)</label>
<input id="currency" name="currency" required maxLength={3} pattern="[A-Za-z]{3}" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="EUR" value={currency} onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))} />
</div>
<div>
<label htmlFor="tax_rate" className="block text-sm font-medium text-gray-900">Tax rate (%)</label>
<input id="tax_rate" name="tax_rate" min={0} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="e.g. 19" type="number" step="0.01" value={taxRate ?? ''} onChange={e => setTaxRate(e.target.value === '' ? undefined : Number(e.target.value))} />
</div>
<div className="flex items-center gap-2 mt-6">
<input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} />
<label htmlFor="featured" className="text-sm font-medium text-gray-900">Featured</label>
</div>
<div>
<label htmlFor="billing_interval" className="block text-sm font-medium text-gray-900">Billing interval</label>
<select id="billing_interval" name="billing_interval" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black" value={billingInterval} onChange={e => {
const v = e.target.value as any;
setBillingInterval(v);
if (v) {
setIntervalCount(c => (c && c > 0 ? c : 1));
} else {
setIntervalCount(undefined);
}
}}>
<option value="">One-time or N/A</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</div>
<div>
<label htmlFor="interval_count" className="block text-sm font-medium text-gray-900">Interval count</label>
<input id="interval_count" name="interval_count" min={1} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400 disabled:bg-gray-100 disabled:text-gray-500" placeholder="e.g. 6 for 6 months (defaults to 1)" type="number" value={intervalCount ?? ''} onChange={e => setIntervalCount(e.target.value === '' ? undefined : Number(e.target.value))} disabled={!billingInterval} />
</div>
<div>
<label htmlFor="sku" className="block text-sm font-medium text-gray-900">SKU (optional)</label>
<input id="sku" name="sku" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="SKU" value={sku} onChange={e => setSku(e.target.value)} />
</div>
<div>
<label htmlFor="slug" className="block text-sm font-medium text-gray-900">Slug (optional)</label>
<input id="slug" name="slug" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="slug" value={slug} onChange={e => setSlug(e.target.value)} />
</div>
<div>
<label htmlFor="availability" className="block text-sm font-medium text-gray-900">Availability</label>
<select id="availability" name="availability" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black" value={state} onChange={e => setState(e.target.value as any)}>
<option value="available">Available</option>
<option value="unavailable">Unavailable</option>
</select>
</div>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-900">Description</label>
<textarea id="description" name="description" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" rows={3} placeholder="Describe the product" value={description} onChange={e => setDescription(e.target.value)} />
<p className="mt-1 text-xs text-gray-600">Shown to users in the shop and checkout.</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-900">Picture</label>
<div className="mt-2 flex max-w-xl justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 bg-gray-50">
<div className="text-center">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-500" />
<div className="mt-4 flex text-sm text-gray-700">
<label htmlFor="file-upload" className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500 px-2 py-1 ring-1 ring-gray-200">
<span>Upload a file</span>
<input id="file-upload" name="file-upload" type="file" accept="image/*" className="sr-only" onChange={e => setPictureFile(e.target.files?.[0])} />
</label>
<p className="pl-1">or drag and drop</p>
</div>
<p className="text-xs text-gray-600">PNG, JPG up to 10MB</p>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-x-3">
<Link href="/admin/subscriptions" className="text-sm font-medium text-gray-700 hover:text-gray-900">
Cancel
</Link>
<button type="submit" className="inline-flex justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Create
</button>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
</form>
</div>
</main>
</div>
</PageLayout>
);
}

View File

@ -0,0 +1,169 @@
import { useCallback } from 'react';
import useAuthStore from '../../../store/authStore';
export type CoffeeItem = {
id: number;
title: string;
description: string;
quantity: number;
price: number;
currency?: string;
tax_rate?: number;
is_featured?: boolean;
billing_interval?: 'day'|'week'|'month'|'year'|null;
interval_count?: number|null;
sku?: string|null;
slug?: string|null;
object_storage_id?: string|null;
original_filename?: string|null;
state: boolean;
pictureUrl?: string | null;
created_at?: string;
updated_at?: string;
};
function isFormData(body: any): body is FormData {
return typeof FormData !== 'undefined' && body instanceof FormData;
}
export default function useCoffeeManagement() {
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
const getState = useAuthStore.getState;
const authorizedFetch = useCallback(
async <T = any>(
path: string,
init: RequestInit = {},
responseType: 'json' | 'text' | 'blob' = 'json'
): Promise<T> => {
let token = getState().accessToken;
if (!token) {
const ok = await getState().refreshAuthToken();
if (ok) token = getState().accessToken;
}
const headers: Record<string, string> = {
...(init.headers as Record<string, string> || {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
if (!isFormData(init.body) && init.method && init.method !== 'GET') {
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
}
const res = await fetch(`${base}${path}`, {
credentials: 'include',
...init,
headers,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(text || `HTTP ${res.status}`);
}
if (responseType === 'blob') return (await res.blob()) as unknown as T;
if (responseType === 'text') return (await res.text()) as unknown as T;
const text = await res.text();
try { return JSON.parse(text) as T; } catch { return {} as T; }
},
[base]
);
const listProducts = useCallback(async (): Promise<CoffeeItem[]> => {
const data = await authorizedFetch<any[]>('/api/admin/coffee', { method: 'GET' });
if (!Array.isArray(data)) return [];
// Normalize numeric fields in case API returns strings (e.g., MySQL DECIMAL)
return data.map((r: any) => ({
...r,
id: Number(r.id),
quantity: r.quantity != null ? Number(r.quantity) : 0,
price: r.price != null && r.price !== '' ? Number(r.price) : 0,
tax_rate: r.tax_rate != null && r.tax_rate !== '' ? Number(r.tax_rate) : undefined,
interval_count: r.interval_count != null && r.interval_count !== '' ? Number(r.interval_count) : null,
state: !!r.state,
})) as CoffeeItem[];
}, [authorizedFetch]);
const createProduct = useCallback(async (payload: {
title: string;
description: string;
quantity: number;
price: number;
currency?: string;
tax_rate?: number;
is_featured?: boolean;
billing_interval?: 'day'|'week'|'month'|'year';
interval_count?: number;
sku?: string;
slug?: string;
state?: boolean;
pictureFile?: File;
}): Promise<CoffeeItem> => {
const fd = new FormData();
fd.append('title', payload.title);
fd.append('description', payload.description);
fd.append('quantity', String(payload.quantity));
fd.append('price', String(payload.price));
if (payload.currency) fd.append('currency', payload.currency);
if (typeof payload.tax_rate === 'number') fd.append('tax_rate', String(payload.tax_rate));
if (typeof payload.is_featured === 'boolean') fd.append('is_featured', String(payload.is_featured));
if (payload.billing_interval) fd.append('billing_interval', payload.billing_interval);
if (typeof payload.interval_count === 'number') fd.append('interval_count', String(payload.interval_count));
if (payload.sku) fd.append('sku', payload.sku);
if (payload.slug) fd.append('slug', payload.slug);
if (typeof payload.state === 'boolean') fd.append('state', String(payload.state));
if (payload.pictureFile) fd.append('picture', payload.pictureFile);
return authorizedFetch<CoffeeItem>('/api/admin/coffee', { method: 'POST', body: fd });
}, [authorizedFetch]);
const updateProduct = useCallback(async (id: number, payload: Partial<{
title: string;
description: string;
quantity: number;
price: number;
currency: string;
tax_rate: number;
is_featured: boolean;
billing_interval: 'day'|'week'|'month'|'year';
interval_count: number;
sku: string;
slug: string;
state: boolean;
pictureFile: File;
}>): Promise<CoffeeItem> => {
const fd = new FormData();
if (payload.title !== undefined) fd.append('title', String(payload.title));
if (payload.description !== undefined) fd.append('description', String(payload.description));
if (payload.quantity !== undefined) fd.append('quantity', String(payload.quantity));
if (payload.price !== undefined) fd.append('price', String(payload.price));
if (payload.currency !== undefined) fd.append('currency', payload.currency);
if (payload.tax_rate !== undefined) fd.append('tax_rate', String(payload.tax_rate));
if (payload.is_featured !== undefined) fd.append('is_featured', String(payload.is_featured));
if (payload.billing_interval !== undefined) fd.append('billing_interval', payload.billing_interval);
if (payload.interval_count !== undefined) fd.append('interval_count', String(payload.interval_count));
if (payload.sku !== undefined) fd.append('sku', payload.sku);
if (payload.slug !== undefined) fd.append('slug', payload.slug);
if (payload.state !== undefined) fd.append('state', String(payload.state));
if (payload.pictureFile) fd.append('picture', payload.pictureFile);
return authorizedFetch<CoffeeItem>(`/api/admin/coffee/${id}`, { method: 'PUT', body: fd });
}, [authorizedFetch]);
const setProductState = useCallback(async (id: number, state: boolean): Promise<CoffeeItem> => {
return authorizedFetch<CoffeeItem>(`/api/admin/coffee/${id}/state`, {
method: 'PATCH',
body: JSON.stringify({ state })
});
}, [authorizedFetch]);
const deleteProduct = useCallback(async (id: number): Promise<{success?: boolean}> => {
return authorizedFetch<{success?: boolean}>(`/api/admin/coffee/${id}`, { method: 'DELETE' });
}, [authorizedFetch]);
return {
listProducts,
createProduct,
updateProduct,
setProductState,
deleteProduct,
};
}

View File

@ -0,0 +1,104 @@
"use client";
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import PageLayout from '../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement';
export default function AdminSubscriptionsPage() {
const { listProducts, setProductState, deleteProduct } = useCoffeeManagement();
const [items, setItems] = useState<CoffeeItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
try {
const data = await listProducts();
setItems(Array.isArray(data) ? data : []);
} catch (e: any) {
setError(e?.message ?? 'Failed to load products');
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const availabilityBadge = (avail: boolean) => (
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${avail ? 'bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-200' : 'bg-gray-100 text-gray-700 ring-1 ring-inset ring-gray-300'}`}>
{avail ? 'Available' : 'Unavailable'}
</span>
);
return (
<PageLayout>
<div className="min-h-screen bg-gray-50">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Subscription Products</h1>
<p className="text-sm sm:text-base text-gray-700 mt-2">Manage all products and subscription plans.</p>
</div>
<Link href="/admin/subscriptions/createSubscription" className="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Create Subscription
</Link>
</div>
{error && (
<div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 ring-1 ring-inset ring-red-200">{error}</div>
)}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{loading && (
<div className="col-span-full text-sm text-gray-700">Loading</div>
)}
{!loading && items.map(item => (
<div key={item.id} className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
<div className="flex items-start justify-between gap-3">
<h3 className="text-base font-semibold text-gray-900">{item.title}</h3>
{availabilityBadge(!!item.state)}
</div>
{item.pictureUrl && (
<img src={item.pictureUrl} alt={item.title} className="mt-3 w-full h-40 object-cover rounded-md ring-1 ring-gray-200" />
)}
<p className="mt-3 text-sm text-gray-800 line-clamp-4">{item.description}</p>
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<div>
<dt className="text-gray-500">Qty</dt>
<dd className="font-medium text-gray-900">{item.quantity}</dd>
</div>
<div>
<dt className="text-gray-500">Price</dt>
<dd className="font-medium text-gray-900">
{item.currency || 'EUR'}{' '}
{Number.isFinite(Number(item.price)) ? Number(item.price).toFixed(2) : String(item.price)}
{Number.isFinite(Number(item.tax_rate)) ? ` + ${Number(item.tax_rate)}%` : ''}
</dd>
</div>
{item.billing_interval && item.interval_count ? (
<div className="col-span-2 text-gray-600">
<span className="text-xs">Every {item.interval_count} {item.billing_interval}{item.interval_count > 1 ? 's' : ''}</span>
</div>
) : null}
</dl>
<div className="mt-4 flex gap-2">
<button className="inline-flex items-center rounded-md bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-200 hover:bg-amber-100" onClick={async () => { await setProductState(item.id, !item.state); await load(); }}>
{item.state ? 'Disable' : 'Enable'}
</button>
<button className="inline-flex items-center rounded-md bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100" onClick={async () => { await deleteProduct(item.id); await load(); }}>
Delete
</button>
</div>
</div>
))}
</div>
</main>
</div>
</PageLayout>
);
}

View File

@ -46,6 +46,12 @@ export const API_ENDPOINTS = {
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
// Coffee products (admin)
ADMIN_COFFEE_LIST: '/api/admin/coffee',
ADMIN_COFFEE_CREATE: '/api/admin/coffee',
ADMIN_COFFEE_UPDATE: '/api/admin/coffee/:id',
ADMIN_COFFEE_SET_STATE: '/api/admin/coffee/:id/state',
ADMIN_COFFEE_DELETE: '/api/admin/coffee/:id',
}
// API Helper Functions
@ -344,6 +350,94 @@ export class AdminAPI {
}
return response.json()
}
// Coffee products (admin)
static async listCoffee(token: string) {
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token)
if (!response.ok) throw new Error('Failed to list products')
return response.json()
}
static async createCoffee(token: string, data: { title: string; description: string; quantity: number; price: number; currency?: string; tax_rate?: number; is_featured?: boolean; billing_interval?: 'day'|'week'|'month'|'year'; interval_count?: number; sku?: string; slug?: string; state?: boolean; pictureFile?: File }) {
if (data.pictureFile) {
const fd = new FormData()
fd.append('title', data.title)
fd.append('description', data.description)
fd.append('quantity', String(data.quantity))
fd.append('price', String(data.price))
if (data.currency) fd.append('currency', data.currency)
if (data.tax_rate !== undefined) fd.append('tax_rate', String(data.tax_rate))
if (data.is_featured !== undefined) fd.append('is_featured', String(!!data.is_featured))
if (data.billing_interval) fd.append('billing_interval', data.billing_interval)
if (data.interval_count !== undefined) fd.append('interval_count', String(data.interval_count))
if (data.sku) fd.append('sku', data.sku)
if (data.slug) fd.append('slug', data.slug)
if (data.state !== undefined) fd.append('state', String(!!data.state))
fd.append('picture', data.pictureFile)
const resp = await ApiClient.postFormData(API_ENDPOINTS.ADMIN_COFFEE_CREATE, fd, token)
if (!resp.ok) throw new Error('Failed to create product')
return resp.json()
} else {
const resp = await ApiClient.post(API_ENDPOINTS.ADMIN_COFFEE_CREATE, {
title: data.title,
description: data.description,
quantity: data.quantity,
price: data.price,
currency: data.currency,
tax_rate: data.tax_rate,
is_featured: data.is_featured,
billing_interval: data.billing_interval,
interval_count: data.interval_count,
sku: data.sku,
slug: data.slug,
state: data.state,
}, token)
if (!resp.ok) throw new Error('Failed to create product')
return resp.json()
}
}
static async updateCoffee(token: string, id: number, data: { title?: string; description?: string; quantity?: number; price?: number; currency?: string; tax_rate?: number; is_featured?: boolean; billing_interval?: 'day'|'week'|'month'|'year'; interval_count?: number; sku?: string; slug?: string; state?: boolean; pictureFile?: File }) {
if (data.pictureFile) {
const fd = new FormData()
if (data.title !== undefined) fd.append('title', data.title)
if (data.description !== undefined) fd.append('description', data.description)
if (data.quantity !== undefined) fd.append('quantity', String(data.quantity))
if (data.price !== undefined) fd.append('price', String(data.price))
if (data.currency !== undefined) fd.append('currency', data.currency)
if (data.tax_rate !== undefined) fd.append('tax_rate', String(data.tax_rate))
if (data.is_featured !== undefined) fd.append('is_featured', String(!!data.is_featured))
if (data.billing_interval !== undefined) fd.append('billing_interval', data.billing_interval)
if (data.interval_count !== undefined) fd.append('interval_count', String(data.interval_count))
if (data.sku !== undefined) fd.append('sku', data.sku)
if (data.slug !== undefined) fd.append('slug', data.slug)
if (data.state !== undefined) fd.append('state', String(!!data.state))
fd.append('picture', data.pictureFile)
const endpoint = API_ENDPOINTS.ADMIN_COFFEE_UPDATE.replace(':id', String(id))
const resp = await ApiClient.postFormData(endpoint, fd, token)
if (!resp.ok) throw new Error('Failed to update product')
return resp.json()
} else {
const endpoint = API_ENDPOINTS.ADMIN_COFFEE_UPDATE.replace(':id', String(id))
const resp = await ApiClient.put(endpoint, data, token)
if (!resp.ok) throw new Error('Failed to update product')
return resp.json()
}
}
static async setCoffeeState(token: string, id: number, state: boolean) {
const endpoint = API_ENDPOINTS.ADMIN_COFFEE_SET_STATE.replace(':id', String(id))
const resp = await ApiClient.patch(endpoint, { state }, token)
if (!resp.ok) throw new Error('Failed to set state')
return resp.json()
}
static async deleteCoffee(token: string, id: number) {
const endpoint = API_ENDPOINTS.ADMIN_COFFEE_DELETE.replace(':id', String(id))
const resp = await ApiClient.delete(endpoint, token)
if (!resp.ok) throw new Error('Failed to delete product')
return true
}
}
// Response Types