Files
flyer-crawler.projectium.com/src/pages/admin/components/StoreForm.tsx

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>
);
};