feat: chế độ xem mặc định + custom spec badges trên card

This commit is contained in:
Dương Cầm
2026-02-04 19:57:13 +07:00
parent 19cf5dc8be
commit 95240d1c9f
4 changed files with 117 additions and 4 deletions

View File

@@ -712,6 +712,38 @@ export default function SettingsPage() {
</div>
</div>
{/* Default View Mode */}
<div className="space-y-4 pt-6 border-t border-gray-100">
<Label className="text-base font-semibold">Chế đ xem mặc đnh (Kho đ)</Label>
<p className="text-xs text-muted-foreground -mt-2">Chọn cách hiển thị thiết bị mặc đnh khi mở trang Kho đ</p>
<div className="grid grid-cols-3 gap-3">
{[
{ value: 'grid', label: 'Lưới', icon: '🔲', desc: 'Icon thiết bị' },
{ value: 'grid-thumb', label: 'Thumbnail', icon: '🖼️', desc: 'Ảnh thu nhỏ' },
{ value: 'list', label: 'Danh sách', icon: '📋', desc: 'Chi tiết hàng' },
].map(mode => {
const savedMode = typeof window !== 'undefined' ? localStorage.getItem('defaultViewMode') : 'grid';
const isActive = savedMode === mode.value || (!savedMode && mode.value === 'grid');
return (
<button
key={mode.value}
type="button"
onClick={() => {
localStorage.setItem('defaultViewMode', mode.value);
toast(`Đã đặt chế độ xem mặc định: ${mode.label}`, 'success');
}}
className={`p-4 rounded-xl border-2 transition-all text-center hover:shadow-md ${isActive ? 'border-primary-500 bg-primary-50 shadow-sm' : 'border-gray-200 bg-white hover:border-gray-300'}`}
>
<span className="text-2xl block mb-1">{mode.icon}</span>
<span className={`font-semibold text-sm ${isActive ? 'text-primary-700' : 'text-gray-700'}`}>{mode.label}</span>
<span className="text-[10px] text-gray-400 block">{mode.desc}</span>
</button>
);
})}
</div>
<p className="text-[10px] text-gray-400">💡 Tip: Khi F5 trang Kho đ sẽ tự đng hiển thị theo chế đ bạn chọn đây.</p>
</div>
<div className="flex justify-end pt-4 border-t border-gray-100">
<Button onClick={handleSaveTheme} disabled={loading} className="bg-gray-900 hover:bg-black text-white px-8 rounded-xl">
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}

View File

@@ -74,6 +74,23 @@ export default function InventoryManager({ initialItems, locations }: { initialI
const searchParams = useSearchParams(); // Added useSearchParams
const { toast } = useToast();
// Load default view mode from localStorage
useEffect(() => {
const savedMode = localStorage.getItem('defaultViewMode');
if (savedMode) {
if (savedMode === 'grid') {
setViewMode('grid');
setShowThumbnails(false);
} else if (savedMode === 'grid-thumb') {
setViewMode('grid');
setShowThumbnails(true);
} else if (savedMode === 'list') {
setViewMode('list');
setShowThumbnails(false);
}
}
}, []);
// Auto-open Item Detail from URL
useEffect(() => {
const itemId = searchParams.get("item");
@@ -553,9 +570,19 @@ export default function InventoryManager({ initialItems, locations }: { initialI
<h3 className="font-bold text-sm text-gray-900 dark:text-gray-100 line-clamp-2 leading-tight mb-2" title={item.name}>{item.name}</h3>
<div className="flex flex-wrap gap-1">
{specs.power && <span className="bg-orange-50 px-1.5 py-0.5 rounded text-[10px] text-orange-700 border border-orange-100 font-medium whitespace-nowrap" title="Công suất"> {specs.power}</span>}
{specs.length && <span className="bg-emerald-50 px-1.5 py-0.5 rounded text-[10px] text-emerald-700 border border-emerald-100 font-medium whitespace-nowrap" title="Độ dài">📏 {specs.length}</span>}
{specs.capacity && <span className="bg-purple-50 px-1.5 py-0.5 rounded text-[10px] text-purple-700 border border-purple-100 font-medium whitespace-nowrap" title="Dung lượng">🔋 {specs.capacity}</span>}
{/* Check displayBadges array - if exists only show selected, else show all */}
{(() => {
const badges = specs.displayBadges || ['power', 'length', 'capacity']; // default show all
return (
<>
{badges.includes('power') && specs.power && <span className="bg-orange-50 px-1.5 py-0.5 rounded text-[10px] text-orange-700 border border-orange-100 font-medium whitespace-nowrap" title="Công suất"> {specs.power}</span>}
{badges.includes('length') && specs.length && <span className="bg-emerald-50 px-1.5 py-0.5 rounded text-[10px] text-emerald-700 border border-emerald-100 font-medium whitespace-nowrap" title="Độ dài">📏 {specs.length}</span>}
{badges.includes('capacity') && specs.capacity && <span className="bg-purple-50 px-1.5 py-0.5 rounded text-[10px] text-purple-700 border border-purple-100 font-medium whitespace-nowrap" title="Dung lượng">🔋 {specs.capacity}</span>}
{badges.includes('interface') && specs.interface && <span className="bg-blue-50 px-1.5 py-0.5 rounded text-[10px] text-blue-700 border border-blue-100 font-medium whitespace-nowrap" title="Kết nối">🔌 {specs.interface}</span>}
{badges.includes('bandwidth') && specs.bandwidth && <span className="bg-cyan-50 px-1.5 py-0.5 rounded text-[10px] text-cyan-700 border border-cyan-100 font-medium whitespace-nowrap" title="Tốc độ"> {specs.bandwidth}</span>}
</>
);
})()}
</div>
</div>

View File

@@ -713,7 +713,7 @@ function EditMode({ item, locations, onCancel, onClose }: { item: any, locations
{(() => {
const currentSpecs = form.watch("specs") || {};
// Filter out null/undefined values to avoid display issues
const entries = Object.entries(currentSpecs).filter(([_, v]) => v !== null && v !== undefined);
const entries = Object.entries(currentSpecs).filter(([k, v]) => v !== null && v !== undefined && k !== 'displayBadges');
const updateSpec = (key: string, val: string) => {
const newSpecs = { ...currentSpecs, [key]: val };
@@ -753,6 +753,60 @@ function EditMode({ item, locations, onCancel, onClose }: { item: any, locations
})()}
</div>
</div>
{/* Display Badges Selection */}
<div className="col-span-2 space-y-2">
<Label className="flex items-center gap-2">
Hiển thị trên Card
<span className="text-[10px] bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded-full font-medium">Badge</span>
</Label>
<p className="text-[10px] text-gray-500 -mt-1">Chọn thông số sẽ hiển thị dưới dạng badge trên card của thiết bị</p>
<div className="bg-gradient-to-r from-orange-50 to-emerald-50 rounded-lg p-3 border border-orange-200/50">
{(() => {
const currentSpecs = form.watch("specs") || {};
const displayBadges: string[] = currentSpecs.displayBadges || ['power', 'length', 'capacity'];
const allBadgeOptions = [
{ key: 'power', label: 'Công suất', icon: '⚡', color: 'bg-orange-100 border-orange-200 text-orange-700' },
{ key: 'length', label: 'Độ dài', icon: '📏', color: 'bg-emerald-100 border-emerald-200 text-emerald-700' },
{ key: 'capacity', label: 'Dung lượng', icon: '🔋', color: 'bg-purple-100 border-purple-200 text-purple-700' },
{ key: 'interface', label: 'Kết nối', icon: '🔌', color: 'bg-blue-100 border-blue-200 text-blue-700' },
{ key: 'bandwidth', label: 'Tốc độ', icon: '⚡', color: 'bg-cyan-100 border-cyan-200 text-cyan-700' },
];
const toggleBadge = (key: string) => {
let newBadges = [...displayBadges];
if (newBadges.includes(key)) {
newBadges = newBadges.filter(b => b !== key);
} else {
newBadges.push(key);
}
form.setValue("specs", { ...currentSpecs, displayBadges: newBadges }, { shouldDirty: true });
};
return (
<div className="flex flex-wrap gap-2">
{allBadgeOptions.map(opt => {
const isChecked = displayBadges.includes(opt.key);
const hasValue = currentSpecs[opt.key];
return (
<button
key={opt.key}
type="button"
onClick={() => toggleBadge(opt.key)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border transition-all text-xs font-medium ${isChecked ? opt.color + ' shadow-sm' : 'bg-white border-gray-200 text-gray-400 hover:border-gray-300'} ${!hasValue ? 'opacity-50' : ''}`}
title={hasValue ? `${opt.label}: ${currentSpecs[opt.key]}` : `Chưa có ${opt.label}`}
>
<span>{opt.icon}</span>
<span>{opt.label}</span>
{isChecked && <Check size={12} className="ml-1" />}
</button>
);
})}
</div>
);
})()}
</div>
</div>
</div>
</div>