Implement quantity-based inventory tracking

- Replace checkboxes with quantity inputs for refill, vakuum, and otvoreno
- Display actual quantities in admin table instead of checkmarks
- Auto-parse existing data formats (e.g., "3 vakuum", "Da") to numbers
- Maintain backwards compatibility with existing data
- Update price auto-fill logic to work with quantity values
- Update tests to reflect new quantity inputs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: DaX <noreply@anthropic.com>
This commit is contained in:
DaX
2025-06-27 01:40:18 +02:00
parent 57abb80072
commit fa59df4c3d
2 changed files with 118 additions and 73 deletions

View File

@@ -21,14 +21,17 @@ describe('UI Features Tests', () => {
expect(tableContent).toContain('color.hex'); expect(tableContent).toContain('color.hex');
}); });
it('should have checkboxes for boolean fields', () => { it('should have number inputs for quantity fields', () => {
const adminDashboardPath = join(process.cwd(), 'app', 'upadaj', 'dashboard', 'page.tsx'); const adminDashboardPath = join(process.cwd(), 'app', 'upadaj', 'dashboard', 'page.tsx');
const adminContent = readFileSync(adminDashboardPath, 'utf-8'); const adminContent = readFileSync(adminDashboardPath, 'utf-8');
// Check for checkbox inputs // Check for number inputs for quantities
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="refill"/); expect(adminContent).toMatch(/type="number"[\s\S]*?name="refill"/);
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="vakum"/); expect(adminContent).toMatch(/type="number"[\s\S]*?name="vakum"/);
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="otvoreno"/); expect(adminContent).toMatch(/type="number"[\s\S]*?name="otvoreno"/);
expect(adminContent).toContain('Refill količina');
expect(adminContent).toContain('Vakuum količina');
expect(adminContent).toContain('Otvoreno količina');
}); });
it('should have number input for quantity', () => { it('should have number input for quantity', () => {

View File

@@ -265,25 +265,33 @@ export default function AdminDashboard() {
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.refill?.toLowerCase() === 'da' ? ( {(() => {
<span className="text-green-600 dark:text-green-400 text-lg"></span> const refillCount = parseInt(filament.refill) || 0;
) : ( if (refillCount > 0) {
<span className="text-red-600 dark:text-red-400 text-lg"></span> return <span className="text-green-600 dark:text-green-400 font-semibold">{refillCount}</span>;
)} }
return <span className="text-gray-400 dark:text-gray-500">0</span>;
})()}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.vakum?.toLowerCase().includes('vakuum') ? ( {(() => {
<span className="text-green-600 dark:text-green-400 text-lg"></span> const match = filament.vakum?.match(/^(\d+)\s*vakuum/);
) : ( const vakuumCount = match ? parseInt(match[1]) : 0;
<span className="text-red-600 dark:text-red-400 text-lg"></span> if (vakuumCount > 0) {
)} return <span className="text-green-600 dark:text-green-400 font-semibold">{vakuumCount}</span>;
}
return <span className="text-gray-400 dark:text-gray-500">0</span>;
})()}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.otvoreno?.toLowerCase().includes('otvorena') ? ( {(() => {
<span className="text-green-600 dark:text-green-400 text-lg"></span> const match = filament.otvoreno?.match(/^(\d+)\s*otvorena/);
) : ( const otvorenCount = match ? parseInt(match[1]) : 0;
<span className="text-red-600 dark:text-red-400 text-lg"></span> if (otvorenCount > 0) {
)} return <span className="text-green-600 dark:text-green-400 font-semibold">{otvorenCount}</span>;
}
return <span className="text-gray-400 dark:text-gray-500">0</span>;
})()}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{filament.kolicina}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{filament.kolicina}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{filament.cena}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{filament.cena}</td>
@@ -392,28 +400,17 @@ function FilamentForm({
}, [filament]); }, [filament]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target; const { name, value } = e.target;
if (type === 'checkbox') { if (name === 'refill') {
const checked = (e.target as HTMLInputElement).checked; // Auto-set price based on refill quantity
if (name === 'vakum') { const refillCount = parseInt(value) || 0;
setFormData({ setFormData({
...formData, ...formData,
[name]: checked ? 'vakuum' : '' [name]: value,
}); // Auto-fill price based on refill status if this is a new filament
} else if (name === 'otvoreno') { ...(filament.id ? {} : { cena: refillCount > 0 ? '3499' : '3999' })
setFormData({ });
...formData,
[name]: checked ? 'otvorena' : ''
});
} else if (name === 'refill') {
setFormData({
...formData,
[name]: checked ? 'Da' : '',
// Auto-fill price based on refill status if this is a new filament
...(filament.id ? {} : { cena: checked ? '3499' : '3999' })
});
}
} else { } else {
setFormData({ setFormData({
...formData, ...formData,
@@ -593,40 +590,85 @@ function FilamentForm({
/> />
</div> </div>
{/* Checkboxes grouped together horizontally */} {/* Quantity inputs for refill, vakuum, and otvoreno */}
<div className="md:col-span-2 flex items-end gap-6"> <div>
<label className="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Refill količina</label>
<input <input
type="checkbox" type="number"
name="refill" name="refill"
checked={formData.refill.toLowerCase() === 'da'} value={(() => {
onChange={handleChange} if (!formData.refill) return 0;
className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" const num = parseInt(formData.refill);
/> return isNaN(num) ? (formData.refill.toLowerCase() === 'da' ? 1 : 0) : num;
<span>Refill</span> })()}
</label> onChange={(e) => {
const value = parseInt(e.target.value) || 0;
handleChange({
target: {
name: 'refill',
value: value > 0 ? value.toString() : 'Ne'
}
} as any);
}}
min="0"
step="1"
placeholder="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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<label className="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"> <div>
<input <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Vakuum količina</label>
type="checkbox" <input
name="vakum" type="number"
checked={formData.vakum.toLowerCase().includes('vakuum')} name="vakum"
onChange={handleChange} value={(() => {
className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" if (!formData.vakum) return 0;
/> const match = formData.vakum.match(/^(\d+)\s*vakuum/);
<span>Vakuum</span> if (match) return parseInt(match[1]);
</label> return formData.vakum.toLowerCase().includes('vakuum') ? 1 : 0;
})()}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
handleChange({
target: {
name: 'vakum',
value: value > 0 ? `${value} vakuum` : 'Ne'
}
} as any);
}}
min="0"
step="1"
placeholder="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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<label className="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"> <div>
<input <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Otvoreno količina</label>
type="checkbox" <input
name="otvoreno" type="number"
checked={formData.otvoreno.toLowerCase().includes('otvorena')} name="otvoreno"
onChange={handleChange} value={(() => {
className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" if (!formData.otvoreno) return 0;
/> const match = formData.otvoreno.match(/^(\d+)\s*otvorena/);
<span>Otvoreno</span> if (match) return parseInt(match[1]);
</label> return formData.otvoreno.toLowerCase().includes('otvorena') ? 1 : 0;
})()}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
handleChange({
target: {
name: 'otvoreno',
value: value > 0 ? `${value} otvorena` : 'Ne'
}
} as any);
}}
min="0"
step="1"
placeholder="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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div> </div>
<div className="md:col-span-2 flex justify-end gap-4 mt-4"> <div className="md:col-span-2 flex justify-end gap-4 mt-4">