Added Serial Numbers, Fixed CSS, Added Dark Mode

# Changelog

## [0.05.2-beta] - 2024-03-05

### Added
- Multiple serial numbers support for warranties
  - Users can now add multiple serial numbers per warranty item
  - Dynamic form fields for adding/removing serial numbers
  - Database schema updated to support multiple serial numbers
  - Added settings menu
  - Added Darkmode

### Changed
- Enhanced warranty management interface
  - Improved form handling for serial numbers
  - Better organization of warranty details
- Optimized database queries with new indexes
  - Added index for serial numbers lookup
  - Added index for warranty ID relationships

### Technical
- Database schema improvements
  - New `serial_numbers` table with proper foreign key constraints
  - Added indexes for better query performance
  - Implemented cascading deletes for warranty-serial number relationships

### Fixed
- Form validation and handling for multiple serial numbers
- Database connection management and resource cleanup
This commit is contained in:
sassanix
2025-03-04 22:59:48 -04:00
parent 946c7b1a8f
commit b231317831
8 changed files with 505 additions and 20 deletions

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
## [0.05.2-beta] - 2024-03-05
### Added
- Multiple serial numbers support for warranties
- Users can now add multiple serial numbers per warranty item
- Dynamic form fields for adding/removing serial numbers
- Database schema updated to support multiple serial numbers
### Changed
- Enhanced warranty management interface
- Improved form handling for serial numbers
- Better organization of warranty details
- Optimized database queries with new indexes
- Added index for serial numbers lookup
- Added index for warranty ID relationships
### Technical
- Database schema improvements
- New `serial_numbers` table with proper foreign key constraints
- Added indexes for better query performance
- Implemented cascading deletes for warranty-serial number relationships
### Fixed
- Form validation and handling for multiple serial numbers
- Database connection management and resource cleanup
[0.05.2-beta]: https://github.com/username/warracker/releases/tag/v0.05.2-beta

View File

@@ -44,7 +44,7 @@ Warracker is a web-based application that provides a centralized system for mana
1. **Clone the Repository:**
```bash
git clone https://github.com/sassanix/warracker.git
git clone https://github.com/yourusername/warracker.git
cd warracker
```
2. **Start the Application:**
@@ -108,9 +108,6 @@ warracker/
* Mobile app.
* Settings page.
* Status page.
* Status page.
* Tags
* Serials
## 🛠️ Troubleshooting

View File

@@ -103,6 +103,21 @@ def init_db():
cur.execute('CREATE INDEX IF NOT EXISTS idx_expiration_date ON warranties(expiration_date)')
cur.execute('CREATE INDEX IF NOT EXISTS idx_product_name ON warranties(product_name)')
# Create serial numbers table
cur.execute('''
CREATE TABLE IF NOT EXISTS serial_numbers (
id SERIAL PRIMARY KEY,
warranty_id INTEGER NOT NULL,
serial_number VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
)
''')
# Add indexes for serial numbers
cur.execute('CREATE INDEX IF NOT EXISTS idx_warranty_id ON serial_numbers(warranty_id)')
cur.execute('CREATE INDEX IF NOT EXISTS idx_serial_number ON serial_numbers(serial_number)')
conn.commit()
logger.info("Database initialized successfully")
except Exception as e:
@@ -131,6 +146,13 @@ def get_warranties():
for key, value in warranty_dict.items():
if isinstance(value, (datetime, date)):
warranty_dict[key] = value.isoformat()
# Get serial numbers for this warranty
warranty_id = warranty_dict['id']
cur.execute('SELECT serial_number FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
serial_numbers = [row[0] for row in cur.fetchall()]
warranty_dict['serial_numbers'] = serial_numbers
warranties_list.append(warranty_dict)
return jsonify(warranties_list)
@@ -162,6 +184,7 @@ def add_warranty():
# Process the data
product_name = request.form['product_name']
purchase_date_str = request.form['purchase_date']
serial_numbers = request.form.getlist('serial_numbers')
try:
purchase_date = datetime.strptime(purchase_date_str, '%Y-%m-%d')
@@ -192,12 +215,23 @@ def add_warranty():
# Save to database
conn = get_db_connection()
with conn.cursor() as cur:
# Insert warranty
cur.execute('''
INSERT INTO warranties (product_name, purchase_date, warranty_years, expiration_date, invoice_path)
VALUES (%s, %s, %s, %s, %s)
RETURNING id
''', (product_name, purchase_date, warranty_years, expiration_date, db_invoice_path))
warranty_id = cur.fetchone()[0]
# Insert serial numbers
if serial_numbers:
for serial_number in serial_numbers:
if serial_number.strip(): # Only insert non-empty serial numbers
cur.execute('''
INSERT INTO serial_numbers (warranty_id, serial_number)
VALUES (%s, %s)
''', (warranty_id, serial_number.strip()))
conn.commit()
return jsonify({
@@ -272,6 +306,7 @@ def update_warranty(warranty_id):
# Process the data
product_name = request.form['product_name']
purchase_date_str = request.form['purchase_date']
serial_numbers = request.form.getlist('serial_numbers')
try:
purchase_date = datetime.strptime(purchase_date_str, '%Y-%m-%d')
@@ -321,6 +356,19 @@ def update_warranty(warranty_id):
''', (product_name, purchase_date, warranty_years, expiration_date,
db_invoice_path, warranty_id))
# Update serial numbers
# First, delete existing serial numbers for this warranty
cur.execute('DELETE FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
# Then insert the new serial numbers
if serial_numbers:
for serial_number in serial_numbers:
if serial_number.strip(): # Only insert non-empty serial numbers
cur.execute('''
INSERT INTO serial_numbers (warranty_id, serial_number)
VALUES (%s, %s)
''', (warranty_id, serial_number.strip()))
conn.commit()
return jsonify({"message": "Warranty updated successfully"}), 200

View File

@@ -11,4 +11,16 @@ CREATE TABLE warranties (
);
CREATE INDEX idx_expiration_date ON warranties(expiration_date);
CREATE INDEX idx_product_name ON warranties(product_name);
CREATE INDEX idx_product_name ON warranties(product_name);
-- Add serial numbers table
CREATE TABLE serial_numbers (
id SERIAL PRIMARY KEY,
warranty_id INTEGER NOT NULL,
serial_number VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
);
CREATE INDEX idx_warranty_id ON serial_numbers(warranty_id);
CREATE INDEX idx_serial_number ON serial_numbers(serial_number);

View File

@@ -0,0 +1,11 @@
-- Add serial numbers table
CREATE TABLE serial_numbers (
id SERIAL PRIMARY KEY,
warranty_id INTEGER NOT NULL,
serial_number VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
);
CREATE INDEX idx_warranty_id ON serial_numbers(warranty_id);
CREATE INDEX idx_serial_number ON serial_numbers(serial_number);

View File

@@ -16,6 +16,20 @@
<i class="fas fa-shield-alt"></i>
<h1>Warranty Tracker</h1>
</div>
<div class="settings-container">
<button id="settingsBtn" class="settings-btn" aria-label="Settings">
<i class="fas fa-cog"></i>
</button>
<div id="settingsMenu" class="settings-menu">
<div class="settings-item">
<span>Dark Mode</span>
<label class="toggle-switch" title="Toggle dark mode">
<input type="checkbox" id="darkModeToggle">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</header>
@@ -41,6 +55,18 @@
<input type="number" id="warrantyYears" name="warranty_years" class="form-control" min="1" max="100" required>
</div>
<div class="form-group">
<label>Serial Numbers</label>
<div id="serialNumbersContainer">
<div class="serial-number-input">
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
</div>
</div>
</div>
<div class="form-group">
<label>Invoice/Receipt</label>
<div class="file-input-wrapper">
@@ -107,6 +133,18 @@
<input type="number" id="editWarrantyYears" name="warranty_years" class="form-control" min="1" max="100" required>
</div>
<div class="form-group">
<label>Serial Numbers</label>
<div id="editSerialNumbersContainer">
<div class="serial-number-input">
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
</div>
</div>
</div>
<div class="form-group">
<label>Invoice/Receipt</label>
<div class="file-input-wrapper">

View File

@@ -1,5 +1,78 @@
// DOM Elements
const warrantyForm = document.getElementById('warrantyForm');
const settingsBtn = document.getElementById('settingsBtn');
const settingsMenu = document.getElementById('settingsMenu');
const darkModeToggle = document.getElementById('darkModeToggle');
// Theme Management
function setTheme(isDark) {
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
localStorage.setItem('darkMode', isDark);
darkModeToggle.checked = isDark;
}
// Initialize theme based on user preference or system preference
function initializeTheme() {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme !== null) {
// Use saved preference
setTheme(savedTheme === 'true');
} else {
// Check for system preference
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDarkMode);
}
}
// Initialize theme when page loads
initializeTheme();
// Settings menu toggle
settingsBtn.addEventListener('click', (e) => {
e.stopPropagation();
settingsMenu.classList.toggle('active');
});
// Close settings menu when clicking outside
document.addEventListener('click', (e) => {
if (!settingsMenu.contains(e.target) && !settingsBtn.contains(e.target)) {
settingsMenu.classList.remove('active');
}
});
// Dark mode toggle
darkModeToggle.addEventListener('change', (e) => {
setTheme(e.target.checked);
});
const serialNumbersContainer = document.getElementById('serialNumbersContainer');
// Add event listener for adding new serial number inputs
serialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput();
}
});
function addSerialNumberInput(container = serialNumbersContainer) {
const newInput = document.createElement('div');
newInput.className = 'serial-number-input';
newInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
<i class="fas fa-minus"></i> Remove
</button>
`;
// Add remove button functionality
newInput.querySelector('.remove-serial-number').addEventListener('click', function() {
this.parentElement.remove();
});
container.appendChild(newInput);
}
const warrantiesList = document.getElementById('warrantiesList');
const refreshBtn = document.getElementById('refreshBtn');
const searchInput = document.getElementById('searchWarranties');
@@ -180,6 +253,11 @@ function renderWarranties(filteredWarranties = null) {
statusText = `${daysRemaining} days remaining`;
}
// Make sure serial numbers array exists and is valid
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
const cardElement = document.createElement('div');
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : ''}`;
cardElement.innerHTML = `
@@ -199,6 +277,14 @@ function renderWarranties(filteredWarranties = null) {
<div>Warranty: <span>${warranty.warranty_years} ${warranty.warranty_years > 1 ? 'years' : 'year'}</span></div>
<div>Expires: <span>${formatDate(expirationDate)}</span></div>
<span class="warranty-status status-${statusClass}">${statusText}</span>
${validSerialNumbers.length > 0 ? `
<div class="serial-numbers">
<strong>Serial Numbers:</strong>
<ul>
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
</ul>
</div>
` : ''}
${warranty.invoice_path ? `
<div>
<a href="${warranty.invoice_path}" class="invoice-link" target="_blank">
@@ -253,6 +339,18 @@ async function addWarranty(event) {
const formData = new FormData(warrantyForm);
// Get all serial numbers and add them to formData
const serialNumbers = [];
document.querySelectorAll('input[name="serial_numbers[]"]').forEach(input => {
if (input.value.trim()) {
serialNumbers.push(input.value.trim());
}
});
formData.delete('serial_numbers[]'); // Remove the original array
if (serialNumbers.length > 0) {
serialNumbers.forEach(sn => formData.append('serial_numbers', sn));
}
try {
const response = await fetch(API_URL, {
method: 'POST',
@@ -271,6 +369,26 @@ async function addWarranty(event) {
warrantyForm.reset();
fileName.textContent = '';
// Completely reset serial number inputs
serialNumbersContainer.innerHTML = '';
// Create a fresh initial serial number input
const initialInput = document.createElement('div');
initialInput.className = 'serial-number-input';
initialInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
`;
// Add the event listener for the "Add Another" button
initialInput.querySelector('.add-serial-number').addEventListener('click', function() {
addSerialNumberInput();
});
serialNumbersContainer.appendChild(initialInput);
// Reload warranties
loadWarranties();
} catch (error) {
@@ -290,6 +408,57 @@ function openEditModal(warranty) {
document.getElementById('editPurchaseDate').value = new Date(warranty.purchase_date).toISOString().split('T')[0];
document.getElementById('editWarrantyYears').value = warranty.warranty_years;
// Clear existing serial number inputs
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
editSerialNumbersContainer.innerHTML = '';
// Make sure serial numbers array exists and is valid
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
// Add initial serial number input
const initialInput = document.createElement('div');
initialInput.className = 'serial-number-input';
initialInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
`;
editSerialNumbersContainer.appendChild(initialInput);
// Add existing serial numbers
if (validSerialNumbers.length > 0) {
validSerialNumbers.forEach((serialNumber, index) => {
if (index === 0) {
// Use the first input we already created
editSerialNumbersContainer.querySelector('input').value = serialNumber;
} else {
const newInput = document.createElement('div');
newInput.className = 'serial-number-input';
newInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${serialNumber}">
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
<i class="fas fa-minus"></i> Remove
</button>
`;
editSerialNumbersContainer.appendChild(newInput);
}
});
}
// Add event listeners for the serial number buttons
editSerialNumbersContainer.querySelectorAll('.add-serial-number').forEach(btn => {
btn.addEventListener('click', () => addSerialNumberInput(editSerialNumbersContainer));
});
editSerialNumbersContainer.querySelectorAll('.remove-serial-number').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.remove();
});
});
// Show current invoice if exists
const currentInvoiceElement = document.getElementById('currentInvoice');
if (warranty.invoice_path) {
@@ -329,6 +498,20 @@ async function updateWarranty() {
const formData = new FormData(editWarrantyForm);
// Get all serial numbers and add them to formData
const serialNumbers = [];
document.querySelectorAll('#editSerialNumbersContainer input[name="serial_numbers[]"]').forEach(input => {
if (input.value.trim()) {
serialNumbers.push(input.value.trim());
}
});
// Remove the original array and add clean values
formData.delete('serial_numbers[]');
if (serialNumbers.length > 0) {
serialNumbers.forEach(sn => formData.append('serial_numbers', sn));
}
try {
const response = await fetch(`${API_URL}/${currentWarrantyId}`, {
method: 'PUT',

View File

@@ -1,5 +1,13 @@
/* ===== GLOBAL STYLES ===== */
:root {
:root[data-theme="light"] {
--bg-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--border-color: #e0e0e0;
--modal-backdrop: rgba(0, 0, 0, 0.5);
--loading-backdrop: rgba(255, 255, 255, 0.8);
--shadow-color: rgba(0, 0, 0, 0.1);
--primary-color: #3498db;
--primary-dark: #2980b9;
--secondary-color: #2ecc71;
@@ -20,11 +28,29 @@
box-sizing: border-box;
}
:root[data-theme="dark"] {
--bg-color: #1a1a1a;
--card-bg: #2d2d2d;
--text-color: #e0e0e0;
--border-color: #404040;
--modal-backdrop: rgba(0, 0, 0, 0.7);
--loading-backdrop: rgba(0, 0, 0, 0.8);
--shadow-color: rgba(0, 0, 0, 0.3);
--primary-color: #4dabf7;
--primary-dark: #339af0;
--secondary-color: #40c057;
--danger-color: #fa5252;
--light-gray: #2d2d2d;
--medium-gray: #404040;
--dark-gray: #909090;
--white: #2d2d2d;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--light-gray);
background-color: var(--bg-color);
padding: 0;
margin: 0;
}
@@ -37,16 +63,21 @@ body {
/* ===== HEADER ===== */
header {
background-color: var(--white);
background-color: var(--card-bg);
box-shadow: var(--shadow);
padding: 20px 0;
margin-bottom: 30px;
}
header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.app-title {
display: flex;
align-items: center;
justify-content: center;
}
.app-title h1 {
@@ -76,7 +107,7 @@ header {
/* ===== FORM PANEL ===== */
.form-panel {
background-color: var(--white);
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 25px;
@@ -204,7 +235,7 @@ header {
/* ===== WARRANTIES LIST ===== */
.warranties-panel {
background-color: var(--white);
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 25px;
@@ -268,7 +299,7 @@ header {
}
.warranty-card {
background-color: var(--white);
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
padding: 15px;
@@ -391,7 +422,7 @@ header {
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
background-color: var(--modal-backdrop);
display: flex;
justify-content: center;
align-items: center;
@@ -407,7 +438,7 @@ header {
}
.modal {
background-color: var(--white);
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 90%;
@@ -555,18 +586,13 @@ header {
100% { transform: rotate(360deg); }
}
.loading-container {
position: fixed;
top: 0;
left: 0;
width
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
background-color: var(--loading-backdrop);
display: flex;
justify-content: center;
align-items: center;
@@ -622,6 +648,147 @@ header {
}
}
/* ===== SETTINGS MENU ===== */
.settings-container {
position: relative;
z-index: 100;
}
.settings-btn {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--primary-color);
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
}
.settings-btn:hover {
transform: rotate(30deg);
background-color: var(--primary-color);
color: var(--white);
}
.settings-menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 15px;
min-width: 220px;
z-index: 100;
display: none;
border: 1px solid var(--border-color);
}
.settings-menu.active {
display: block;
animation: slide-in-top 0.3s forwards;
}
@keyframes slide-in-top {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.settings-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.settings-item:last-child {
border-bottom: none;
}
.settings-item span {
font-weight: 500;
color: var(--text-color);
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-left: 10px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--medium-gray);
transition: var(--transition);
border-radius: 34px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: var(--white);
transition: var(--transition);
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
input:checked + .toggle-slider {
background-color: var(--primary-color);
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.toggle-slider:after {
content: '☀️';
position: absolute;
top: 3px;
left: 6px;
font-size: 12px;
color: var(--white);
opacity: 0.7;
transition: var(--transition);
}
input:checked + .toggle-slider:after {
content: '🌙';
left: auto;
right: 6px;
}
/* ===== UTILITIES ===== */
.text-danger {
color: var(--danger-color);