diff --git a/api-update-sale.tar.gz b/api-update-sale.tar.gz new file mode 100644 index 0000000..b99fd73 Binary files /dev/null and b/api-update-sale.tar.gz differ diff --git a/api/server.js b/api/server.js index f571424..04738c4 100644 --- a/api/server.js +++ b/api/server.js @@ -153,7 +153,7 @@ app.post('/api/filaments', authenticateToken, async (req, res) => { app.put('/api/filaments/:id', authenticateToken, async (req, res) => { const { id } = req.params; - const { tip, finish, boja, boja_hex, refill, spulna, cena } = req.body; + const { tip, finish, boja, boja_hex, refill, spulna, cena, sale_percentage, sale_active, sale_start_date, sale_end_date } = req.body; try { // Ensure refill and spulna are numbers @@ -165,9 +165,12 @@ app.put('/api/filaments/:id', authenticateToken, async (req, res) => { `UPDATE filaments SET tip = $1, finish = $2, boja = $3, boja_hex = $4, refill = $5, spulna = $6, kolicina = $7, cena = $8, + sale_percentage = $9, sale_active = $10, + sale_start_date = $11, sale_end_date = $12, updated_at = CURRENT_TIMESTAMP - WHERE id = $9 RETURNING *`, - [tip, finish, boja, boja_hex, refillNum, spulnaNum, kolicina, cena, id] + WHERE id = $13 RETURNING *`, + [tip, finish, boja, boja_hex, refillNum, spulnaNum, kolicina, cena, + sale_percentage || 0, sale_active || false, sale_start_date, sale_end_date, id] ); res.json(result.rows[0]); } catch (error) { @@ -188,6 +191,51 @@ app.delete('/api/filaments/:id', authenticateToken, async (req, res) => { } }); +// Bulk sale update endpoint +app.post('/api/filaments/sale/bulk', authenticateToken, async (req, res) => { + const { filamentIds, salePercentage, saleStartDate, saleEndDate, enableSale } = req.body; + + try { + let query; + let params; + + if (filamentIds && filamentIds.length > 0) { + // Update specific filaments + query = ` + UPDATE filaments + SET sale_percentage = $1, + sale_active = $2, + sale_start_date = $3, + sale_end_date = $4, + updated_at = CURRENT_TIMESTAMP + WHERE id = ANY($5) + RETURNING *`; + params = [salePercentage || 0, enableSale || false, saleStartDate, saleEndDate, filamentIds]; + } else { + // Update all filaments + query = ` + UPDATE filaments + SET sale_percentage = $1, + sale_active = $2, + sale_start_date = $3, + sale_end_date = $4, + updated_at = CURRENT_TIMESTAMP + RETURNING *`; + params = [salePercentage || 0, enableSale || false, saleStartDate, saleEndDate]; + } + + const result = await pool.query(query, params); + res.json({ + success: true, + updatedCount: result.rowCount, + updatedFilaments: result.rows + }); + } catch (error) { + console.error('Error updating sale:', error); + res.status(500).json({ error: 'Failed to update sale' }); + } +}); + app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); \ No newline at end of file diff --git a/app/upadaj/dashboard/page.tsx b/app/upadaj/dashboard/page.tsx index 99a4cae..e9f37f3 100644 --- a/app/upadaj/dashboard/page.tsx +++ b/app/upadaj/dashboard/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { filamentService, colorService } from '@/src/services/api'; import { Filament } from '@/src/types/filament'; import { trackEvent } from '@/src/components/MatomoAnalytics'; +import { SaleManager } from '@/src/components/SaleManager'; // Removed unused imports for Bambu Lab color categorization import '@/src/styles/select.css'; @@ -339,6 +340,11 @@ export default function AdminDashboard() { Obriši izabrane ({selectedFilaments.size}) )} + router.push('/')} className="flex-1 sm:flex-initial px-3 sm:px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm sm:text-base" @@ -474,6 +480,7 @@ export default function AdminDashboard() { handleSort('cena')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> Cena {sortField === 'cena' && (sortOrder === 'asc' ? '↑' : '↓')} + Popust Akcije @@ -565,6 +572,22 @@ export default function AdminDashboard() { ); })()} + + {filament.sale_active && filament.sale_percentage ? ( + + + -{filament.sale_percentage}% + + {filament.sale_end_date && ( + + do {new Date(filament.sale_end_date).toLocaleDateString('sr-RS')} + + )} + + ) : ( + - + )} + { diff --git a/database/migrations/014_add_sale_fields.sql b/database/migrations/014_add_sale_fields.sql new file mode 100644 index 0000000..ee92988 --- /dev/null +++ b/database/migrations/014_add_sale_fields.sql @@ -0,0 +1,10 @@ +-- Add sale fields to filaments table +ALTER TABLE filaments +ADD COLUMN IF NOT EXISTS sale_percentage INTEGER DEFAULT 0 CHECK (sale_percentage >= 0 AND sale_percentage <= 100), +ADD COLUMN IF NOT EXISTS sale_active BOOLEAN DEFAULT FALSE, +ADD COLUMN IF NOT EXISTS sale_start_date TIMESTAMP WITH TIME ZONE, +ADD COLUMN IF NOT EXISTS sale_end_date TIMESTAMP WITH TIME ZONE; + +-- Add indexes for better performance +CREATE INDEX IF NOT EXISTS idx_filaments_sale_active ON filaments(sale_active) WHERE sale_active = TRUE; +CREATE INDEX IF NOT EXISTS idx_filaments_sale_dates ON filaments(sale_start_date, sale_end_date) WHERE sale_active = TRUE; \ No newline at end of file diff --git a/scripts/deploy-api-update.sh b/scripts/deploy-api-update.sh new file mode 100755 index 0000000..acf711d --- /dev/null +++ b/scripts/deploy-api-update.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +echo "Deploying API update to EC2 instance..." + +# Get instance ID +INSTANCE_ID="i-03956ecf32292d7d9" + +# Create update script +cat > /tmp/update-api.sh << 'EOF' +#!/bin/bash +cd /home/ubuntu/filamenteka-api + +# Backup current server.js +cp server.js server.js.backup + +# Download the updated server.js from GitHub +curl -o server.js https://raw.githubusercontent.com/daxdax89/Filamenteka/sale/api/server.js + +# Restart the service +sudo systemctl restart node-api + +echo "API server updated and restarted" +EOF + +# Send command to EC2 instance +aws ssm send-command \ + --region eu-central-1 \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters "commands=[ + 'cd /home/ubuntu/filamenteka-api', + 'cp server.js server.js.backup', + 'curl -o server.js https://raw.githubusercontent.com/daxdax89/Filamenteka/sale/api/server.js', + 'sudo systemctl restart node-api', + 'sudo systemctl status node-api' + ]" \ + --output json + +echo "Command sent. Check AWS Systems Manager for execution status." \ No newline at end of file diff --git a/scripts/run-sale-migration.sh b/scripts/run-sale-migration.sh new file mode 100755 index 0000000..d1e5bc2 --- /dev/null +++ b/scripts/run-sale-migration.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo "Running sale fields migration..." + +# Get RDS endpoint from AWS +RDS_ENDPOINT=$(aws rds describe-db-instances --region eu-central-1 --db-instance-identifier filamenteka --query 'DBInstances[0].Endpoint.Address' --output text) + +# Get database credentials from Secrets Manager +DB_CREDS=$(aws secretsmanager get-secret-value --region eu-central-1 --secret-id filamenteka-db-credentials --query 'SecretString' --output text) +DB_USER=$(echo $DB_CREDS | jq -r '.username') +DB_PASS=$(echo $DB_CREDS | jq -r '.password') +DB_NAME=$(echo $DB_CREDS | jq -r '.database') + +# Run the migration +PGPASSWORD="$DB_PASS" psql -h $RDS_ENDPOINT -U $DB_USER -d $DB_NAME -f database/migrations/014_add_sale_fields.sql + +echo "Migration completed!" \ No newline at end of file diff --git a/src/components/FilamentTableV2.tsx b/src/components/FilamentTableV2.tsx index 807420d..8a18ac6 100644 --- a/src/components/FilamentTableV2.tsx +++ b/src/components/FilamentTableV2.tsx @@ -245,20 +245,40 @@ const FilamentTableV2: React.FC = ({ 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 && ( - + {refillPrice.toLocaleString('sr-RS')} )} + {hasRefill && saleActive && ( + + {saleRefillPrice.toLocaleString('sr-RS')} + + )} {hasRefill && hasSpool && /} {hasSpool && ( - + {spoolPrice.toLocaleString('sr-RS')} )} + {hasSpool && saleActive && ( + + {saleSpoolPrice.toLocaleString('sr-RS')} + + )} RSD + {saleActive && ( + + -{filament.sale_percentage}% + + )} > ); })()} diff --git a/src/components/SaleManager.tsx b/src/components/SaleManager.tsx new file mode 100644 index 0000000..246bab1 --- /dev/null +++ b/src/components/SaleManager.tsx @@ -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; + 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 ( + <> + 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 + + + {showSaleModal && ( + + + + Upravljanje popustima + + + + + + setApplyToAll(e.target.checked)} + className="w-4 h-4 text-purple-600" + /> + + Primeni na sve filamente + + + + {!applyToAll && ( + + Izabrano: {selectedFilaments.size} filament{selectedFilaments.size === 1 ? '' : 'a'} + + )} + + + + + Procenat popusta (%) + + 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" + /> + + + + + Početak popusta (opciono) + + 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" + /> + + + + + Kraj popusta (opciono) + + 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" + /> + + + + + setEnableSale(e.target.checked)} + className="w-4 h-4 text-purple-600" + /> + + Aktiviraj popust + + + + + + + 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 + + + {loading ? 'Ažuriranje...' : 'Primeni'} + + + + + )} + > + ); +} \ No newline at end of file diff --git a/src/services/api.ts b/src/services/api.ts index 73b2bd2..394859c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -87,6 +87,17 @@ export const filamentService = { const response = await api.delete(`/filaments/${id}`); return response.data; }, + + updateBulkSale: async (data: { + filamentIds?: string[]; + salePercentage: number; + saleStartDate?: string; + saleEndDate?: string; + enableSale: boolean; + }) => { + const response = await api.post('/filaments/sale/bulk', data); + return response.data; + }, }; export default api; \ No newline at end of file diff --git a/src/types/filament.ts b/src/types/filament.ts index 7e7d092..1b62ea6 100644 --- a/src/types/filament.ts +++ b/src/types/filament.ts @@ -11,4 +11,8 @@ export interface Filament { status?: string; created_at?: string; // Using snake_case to match database updated_at?: string; // Using snake_case to match database + sale_percentage?: number; + sale_active?: boolean; + sale_start_date?: string; + sale_end_date?: string; } \ No newline at end of file
+ Izabrano: {selectedFilaments.size} filament{selectedFilaments.size === 1 ? '' : 'a'} +