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');
});
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 adminContent = readFileSync(adminDashboardPath, 'utf-8');
// Check for checkbox inputs
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="refill"/);
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="vakum"/);
expect(adminContent).toMatch(/type="checkbox"[\s\S]*?name="otvoreno"/);
// Check for number inputs for quantities
expect(adminContent).toMatch(/type="number"[\s\S]*?name="refill"/);
expect(adminContent).toMatch(/type="number"[\s\S]*?name="vakum"/);
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', () => {

View File

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