Add sale management feature for admin panel
- Add database migration for sale fields (percentage, active, dates) - Update API to handle sale operations and bulk updates - Create SaleManager component for admin interface - Update FilamentTableV2 to display sale prices on frontend - Add sale column in admin dashboard - Implement sale price calculations with strikethrough styling
This commit is contained in:
@@ -245,20 +245,40 @@ const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
|
||||
spoolPrice = colorData?.cena_spulna || 3999;
|
||||
}
|
||||
|
||||
// Calculate sale prices if applicable
|
||||
const saleActive = filament.sale_active && filament.sale_percentage;
|
||||
const saleRefillPrice = saleActive ? Math.round(refillPrice * (1 - filament.sale_percentage! / 100)) : refillPrice;
|
||||
const saleSpoolPrice = saleActive ? Math.round(spoolPrice * (1 - filament.sale_percentage! / 100)) : spoolPrice;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasRefill && (
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
<span className={saleActive ? "line-through text-gray-500 dark:text-gray-400" : "text-green-600 dark:text-green-400"}>
|
||||
{refillPrice.toLocaleString('sr-RS')}
|
||||
</span>
|
||||
)}
|
||||
{hasRefill && saleActive && (
|
||||
<span className="text-green-600 dark:text-green-400 font-bold ml-1">
|
||||
{saleRefillPrice.toLocaleString('sr-RS')}
|
||||
</span>
|
||||
)}
|
||||
{hasRefill && hasSpool && <span className="mx-1">/</span>}
|
||||
{hasSpool && (
|
||||
<span className="text-blue-500 dark:text-blue-400">
|
||||
<span className={saleActive ? "line-through text-gray-500 dark:text-gray-400" : "text-blue-500 dark:text-blue-400"}>
|
||||
{spoolPrice.toLocaleString('sr-RS')}
|
||||
</span>
|
||||
)}
|
||||
{hasSpool && saleActive && (
|
||||
<span className="text-blue-500 dark:text-blue-400 font-bold ml-1">
|
||||
{saleSpoolPrice.toLocaleString('sr-RS')}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-1 text-gray-600 dark:text-gray-400">RSD</span>
|
||||
{saleActive && (
|
||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
-{filament.sale_percentage}%
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
177
src/components/SaleManager.tsx
Normal file
177
src/components/SaleManager.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { useState } from 'react';
|
||||
import { filamentService } from '@/src/services/api';
|
||||
import { Filament } from '@/src/types/filament';
|
||||
|
||||
interface SaleManagerProps {
|
||||
filaments: Filament[];
|
||||
selectedFilaments: Set<string>;
|
||||
onSaleUpdate: () => void;
|
||||
}
|
||||
|
||||
export function SaleManager({ filaments, selectedFilaments, onSaleUpdate }: SaleManagerProps) {
|
||||
const [showSaleModal, setShowSaleModal] = useState(false);
|
||||
const [salePercentage, setSalePercentage] = useState(10);
|
||||
const [saleStartDate, setSaleStartDate] = useState('');
|
||||
const [saleEndDate, setSaleEndDate] = useState('');
|
||||
const [enableSale, setEnableSale] = useState(true);
|
||||
const [applyToAll, setApplyToAll] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSaleUpdate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const filamentIds = applyToAll ? undefined : Array.from(selectedFilaments);
|
||||
|
||||
try {
|
||||
await filamentService.updateBulkSale({
|
||||
filamentIds,
|
||||
salePercentage,
|
||||
saleStartDate: saleStartDate || undefined,
|
||||
saleEndDate: saleEndDate || undefined,
|
||||
enableSale
|
||||
});
|
||||
} catch (error: any) {
|
||||
// If bulk endpoint fails, fall back to individual updates
|
||||
if (error.response?.status === 404) {
|
||||
alert('Bulk sale endpoint not available yet. Please update the API server with the latest code.');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
onSaleUpdate();
|
||||
setShowSaleModal(false);
|
||||
setSalePercentage(10);
|
||||
setSaleStartDate('');
|
||||
setSaleEndDate('');
|
||||
setEnableSale(true);
|
||||
setApplyToAll(false);
|
||||
} catch (error) {
|
||||
console.error('Error updating sale:', error);
|
||||
alert('Greška pri ažuriranju popusta');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get current date/time in local timezone for datetime-local input
|
||||
const getCurrentDateTime = () => {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
||||
return now.toISOString().slice(0, 16);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowSaleModal(true)}
|
||||
className="flex-1 sm:flex-initial px-3 sm:px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm sm:text-base"
|
||||
>
|
||||
Upravljaj popustima
|
||||
</button>
|
||||
|
||||
{showSaleModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
Upravljanje popustima
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={applyToAll}
|
||||
onChange={(e) => setApplyToAll(e.target.checked)}
|
||||
className="w-4 h-4 text-purple-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Primeni na sve filamente
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{!applyToAll && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Izabrano: {selectedFilaments.size} filament{selectedFilaments.size === 1 ? '' : 'a'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Procenat popusta (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={salePercentage}
|
||||
onChange={(e) => setSalePercentage(parseInt(e.target.value) || 0)}
|
||||
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-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Početak popusta (opciono)
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={saleStartDate}
|
||||
onChange={(e) => setSaleStartDate(e.target.value)}
|
||||
min={getCurrentDateTime()}
|
||||
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-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Kraj popusta (opciono)
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={saleEndDate}
|
||||
onChange={(e) => setSaleEndDate(e.target.value)}
|
||||
min={saleStartDate || getCurrentDateTime()}
|
||||
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-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableSale}
|
||||
onChange={(e) => setEnableSale(e.target.checked)}
|
||||
className="w-4 h-4 text-purple-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Aktiviraj popust
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<button
|
||||
onClick={() => setShowSaleModal(false)}
|
||||
className="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-200 rounded hover:bg-gray-400 dark:hover:bg-gray-500"
|
||||
disabled={loading}
|
||||
>
|
||||
Otkaži
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaleUpdate}
|
||||
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 disabled:opacity-50"
|
||||
disabled={loading || (!applyToAll && selectedFilaments.size === 0)}
|
||||
>
|
||||
{loading ? 'Ažuriranje...' : 'Primeni'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user