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:
DaX
2025-07-05 14:48:31 +02:00
parent c0682e1969
commit 0df9d5d294
10 changed files with 354 additions and 5 deletions

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