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:
@@ -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', () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
|
||||||
} 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
|
// Auto-fill price based on refill status if this is a new filament
|
||||||
...(filament.id ? {} : { cena: checked ? '3499' : '3999' })
|
...(filament.id ? {} : { cena: refillCount > 0 ? '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;
|
||||||
|
})()}
|
||||||
|
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"
|
||||||
/>
|
/>
|
||||||
<span>Refill</span>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Vakuum količina</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="number"
|
||||||
name="vakum"
|
name="vakum"
|
||||||
checked={formData.vakum.toLowerCase().includes('vakuum')}
|
value={(() => {
|
||||||
onChange={handleChange}
|
if (!formData.vakum) 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 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"
|
||||||
/>
|
/>
|
||||||
<span>Vakuum</span>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Otvoreno količina</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="number"
|
||||||
name="otvoreno"
|
name="otvoreno"
|
||||||
checked={formData.otvoreno.toLowerCase().includes('otvorena')}
|
value={(() => {
|
||||||
onChange={handleChange}
|
if (!formData.otvoreno) 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 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"
|
||||||
/>
|
/>
|
||||||
<span>Otvoreno</span>
|
|
||||||
</label>
|
|
||||||
</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">
|
||||||
|
|||||||
Reference in New Issue
Block a user