295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
// src/pages/admin/components/StoreForm.tsx
|
|
import React, { useState } from 'react';
|
|
import toast from 'react-hot-toast';
|
|
import { createStore, updateStore, addStoreLocation } from '../../../services/apiClient';
|
|
import { StoreWithLocations } from '../../../types';
|
|
import { logger } from '../../../services/logger.client';
|
|
|
|
interface StoreFormProps {
|
|
store?: StoreWithLocations; // If provided, this is edit mode
|
|
onSuccess: () => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export const StoreForm: React.FC<StoreFormProps> = ({ store, onSuccess, onCancel }) => {
|
|
const isEditMode = !!store;
|
|
|
|
const [name, setName] = useState(store?.name || '');
|
|
const [logoUrl, setLogoUrl] = useState(store?.logo_url || '');
|
|
const [includeAddress, setIncludeAddress] = useState(!isEditMode); // Address optional in edit mode
|
|
const [addressLine1, setAddressLine1] = useState('');
|
|
const [city, setCity] = useState('');
|
|
const [provinceState, setProvinceState] = useState('ON');
|
|
const [postalCode, setPostalCode] = useState('');
|
|
const [country, setCountry] = useState('Canada');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!name.trim()) {
|
|
toast.error('Store name is required');
|
|
return;
|
|
}
|
|
|
|
if (
|
|
includeAddress &&
|
|
(!addressLine1.trim() || !city.trim() || !provinceState.trim() || !postalCode.trim())
|
|
) {
|
|
toast.error('All address fields are required when adding a location');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
const toastId = toast.loading(isEditMode ? 'Updating store...' : 'Creating store...');
|
|
|
|
try {
|
|
if (isEditMode && store) {
|
|
// Update existing store
|
|
const response = await updateStore(store.store_id, {
|
|
name: name.trim(),
|
|
logo_url: logoUrl.trim() || undefined,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text();
|
|
throw new Error(errorBody || `Update failed with status ${response.status}`);
|
|
}
|
|
|
|
// If adding a new location to existing store
|
|
if (includeAddress) {
|
|
const locationResponse = await addStoreLocation(store.store_id, {
|
|
address_line_1: addressLine1.trim(),
|
|
city: city.trim(),
|
|
province_state: provinceState.trim(),
|
|
postal_code: postalCode.trim(),
|
|
country: country.trim(),
|
|
});
|
|
|
|
if (!locationResponse.ok) {
|
|
const errorBody = await locationResponse.text();
|
|
throw new Error(`Location add failed: ${errorBody}`);
|
|
}
|
|
}
|
|
|
|
toast.success('Store updated successfully!', { id: toastId });
|
|
} else {
|
|
// Create new store
|
|
const storeData: {
|
|
name: string;
|
|
logo_url?: string;
|
|
address?: {
|
|
address_line_1: string;
|
|
city: string;
|
|
province_state: string;
|
|
postal_code: string;
|
|
country?: string;
|
|
};
|
|
} = {
|
|
name: name.trim(),
|
|
logo_url: logoUrl.trim() || undefined,
|
|
};
|
|
|
|
if (includeAddress) {
|
|
storeData.address = {
|
|
address_line_1: addressLine1.trim(),
|
|
city: city.trim(),
|
|
province_state: provinceState.trim(),
|
|
postal_code: postalCode.trim(),
|
|
country: country.trim(),
|
|
};
|
|
}
|
|
|
|
const response = await createStore(storeData);
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text();
|
|
throw new Error(errorBody || `Create failed with status ${response.status}`);
|
|
}
|
|
|
|
toast.success('Store created successfully!', { id: toastId });
|
|
}
|
|
|
|
onSuccess();
|
|
} catch (e) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
logger.error({ err: e }, '[StoreForm] Submission failed');
|
|
toast.error(`Failed: ${errorMessage}`, { id: toastId });
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label
|
|
htmlFor="name"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Store Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="e.g., Loblaws, Walmart, etc."
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="logoUrl"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Logo URL (optional)
|
|
</label>
|
|
<input
|
|
type="url"
|
|
id="logoUrl"
|
|
value={logoUrl}
|
|
onChange={(e) => setLogoUrl(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="https://example.com/logo.png"
|
|
/>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<div className="flex items-center mb-3">
|
|
<input
|
|
type="checkbox"
|
|
id="includeAddress"
|
|
checked={includeAddress}
|
|
onChange={(e) => setIncludeAddress(e.target.checked)}
|
|
className="h-4 w-4 text-brand-primary focus:ring-brand-primary border-gray-300 rounded"
|
|
/>
|
|
<label
|
|
htmlFor="includeAddress"
|
|
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
|
|
>
|
|
{isEditMode ? 'Add a new location' : 'Include store address'}
|
|
</label>
|
|
</div>
|
|
|
|
{includeAddress && (
|
|
<div className="space-y-4 pl-6 border-l-2 border-gray-200 dark:border-gray-600">
|
|
<div>
|
|
<label
|
|
htmlFor="addressLine1"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Address Line 1 *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="addressLine1"
|
|
value={addressLine1}
|
|
onChange={(e) => setAddressLine1(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="123 Main St"
|
|
required={includeAddress}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label
|
|
htmlFor="city"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
City *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="city"
|
|
value={city}
|
|
onChange={(e) => setCity(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="Toronto"
|
|
required={includeAddress}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="provinceState"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Province/State *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="provinceState"
|
|
value={provinceState}
|
|
onChange={(e) => setProvinceState(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="ON"
|
|
required={includeAddress}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label
|
|
htmlFor="postalCode"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Postal Code *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="postalCode"
|
|
value={postalCode}
|
|
onChange={(e) => setPostalCode(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="M5V 1A1"
|
|
required={includeAddress}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="country"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
>
|
|
Country
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="country"
|
|
value={country}
|
|
onChange={(e) => setCountry(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-primary focus:border-transparent"
|
|
placeholder="Canada"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={isSubmitting}
|
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="px-4 py-2 bg-brand-primary text-white rounded-md hover:bg-brand-dark disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isSubmitting ? 'Saving...' : isEditMode ? 'Update Store' : 'Create Store'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|