feat: expand badge system & fix location image display

- Add centralized BADGE_ICONS_MAP and BADGE_COLORS_MAP to options.ts
- Implement dynamic badge UI in SmartAddForm (Add Item)
- Update ItemDetailDialog (Edit Item) to use centralized config
- Update InventoryManager to support new badge icons
- Display location image in detail header
- Fix capacity badge missing icon/color
- Fix location edit not loading existing image
This commit is contained in:
Dương Cầm
2026-02-04 23:53:40 +07:00
parent 88ab5914c5
commit 420b18cebf
7 changed files with 2167 additions and 106 deletions

1686
public/database.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
import { useState, useMemo, useEffect } from "react";
import Link from "next/link";
import { QrCode, Package, Database, Search, LayoutDashboard, Tag, Wallet, Check, X, ArrowRightLeft, List, LayoutGrid, HelpCircle, Trash2, AlertTriangle, Shield, Clock, Image as ImageIcon } from "lucide-react";
import { QrCode, Package, Database, Search, LayoutDashboard, Tag, Wallet, Check, X, ArrowRightLeft, List, LayoutGrid, HelpCircle, Trash2, AlertTriangle, Shield, Image as ImageIcon } from "lucide-react";
import { Card, Button, Input, Select } from "@/components/ui/primitives";
import { Checkbox } from "@/components/ui/Checkbox";
import { ItemDetailDialog } from "./ItemDetailDialog";
@@ -17,6 +17,10 @@ import { bulkMoveItems, bulkDeleteItems } from "@/app/actions";
import { bulkLendItems, lendItem, returnItem } from "@/features/lending/actions";
import { checkOverdue, getUrgencyLevel, formatOverdueStatus } from "@/lib/utils/overdueChecker";
import { checkWarranty, getWarrantyUrgency, formatWarrantyStatus } from "@/lib/utils/warrantyChecker";
import { Tag as TagIcon } from "lucide-react";
import { BADGE_ICONS_MAP, BADGE_COLORS_MAP } from "@/lib/constants/options";
function getItemIconData(item: any) {
const type = item.category || item.type || 'Other';
@@ -570,16 +574,55 @@ 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">
{/* Check displayBadges array - if exists only show selected, else show all */}
{/* Display Badges (Dynamic) */}
{(() => {
const badges = specs.displayBadges || ['power', 'length', 'capacity']; // default show all
let activeBadges: any[] = specs.displayBadges || [];
// Migration for old string[] format
if (activeBadges.length > 0 && typeof activeBadges[0] === 'string') {
const defaultMap: any = {
power: { icon: 'zap', color: 'orange' },
length: { icon: 'tag', color: 'emerald' },
capacity: { icon: 'battery', color: 'purple' },
interface: { icon: 'tag', color: 'blue' },
bandwidth: { icon: 'zap', color: 'cyan' }
};
activeBadges = activeBadges.map((key: string) => ({
key,
icon: defaultMap[key]?.icon || 'tag',
color: defaultMap[key]?.color || 'blue'
}));
}
// If empty and not customized, show defaults if values exist (Legacy fallback)
if (activeBadges.length === 0 && !specs.displayBadges) {
if (specs.power) activeBadges.push({ key: 'power', icon: 'zap', color: 'orange' });
if (specs.length) activeBadges.push({ key: 'length', icon: 'tag', color: 'emerald' });
if (specs.capacity) activeBadges.push({ key: 'capacity', icon: 'battery', color: 'purple' });
}
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>}
{activeBadges.map((badge: any, idx: number) => {
const Icon = BADGE_ICONS_MAP[badge.icon] || TagIcon;
const colorClass = BADGE_COLORS_MAP[badge.color] || BADGE_COLORS_MAP['blue'];
const value = item[badge.key] || specs[badge.key];
if (!value) return null; // Don't show empty badges
// Format value specially for dates or known fields if needed
let displayValue = value;
if (badge.key === 'warrantyEnd' && value) {
displayValue = new Date(value).toLocaleDateString('vi-VN');
}
return (
<span key={idx} className={`${colorClass} px-1.5 py-0.5 rounded text-[10px] border font-medium whitespace-nowrap flex items-center gap-1`} title={badge.key}>
<Icon size={10} />
<span className="max-w-[80px] truncate">{displayValue}</span>
</span>
);
})}
</>
);
})()}

View File

@@ -19,7 +19,7 @@ import { AutoCompleteInput } from "@/components/ui/AutoCompleteInput";
import { ColorPicker } from "@/components/ui/ColorPicker";
import { IconSelect } from "@/components/ui/IconSelect";
import { getColorHex } from "@/lib/utils/colors";
import { ITEM_TYPES, LOCATION_ICONS, ITEM_ICONS } from "@/lib/constants/options";
import { ITEM_TYPES, LOCATION_ICONS, ITEM_ICONS, BADGE_ICONS_MAP, BADGE_COLORS_MAP } from "@/lib/constants/options";
import { TECH_SUGGESTIONS } from "@/lib/constants";
import { optimizeImage, formatBytes, getBase64Size } from "@/lib/imageUtils";
@@ -337,12 +337,14 @@ function ViewMode({ item, setMode, onDelete }: { item: any, setMode: (m: "EDIT")
</div>
)}
{item.warrantyEnd && (
<div className="border-l-2 border-emerald-100 pl-3">
<p className="text-[10px] text-gray-500 uppercase font-semibold mb-0.5">Bảo hành</p>
<div className="border-l-2 border-emerald-100 pl-3">
<p className="text-[10px] text-gray-500 uppercase font-semibold mb-0.5">Bảo hành</p>
{item.warrantyEnd ? (
<p className="font-medium text-green-700 dark:text-green-400 text-sm">{formatDateVN(item.warrantyEnd)}</p>
</div>
)}
) : (
<p className="font-medium text-gray-400 text-sm italic">Không bảo hành</p>
)}
</div>
{item.purchaseLocation && (
<div className="border-l-2 border-emerald-100 pl-3">
@@ -666,33 +668,68 @@ function EditMode({ item, locations, onCancel, onClose }: { item: any, locations
<Label>Nơi mua</Label>
<Input {...form.register("purchaseLocation")} placeholder="Cửa hàng, Shopee..." />
</div>
<div>
<Label>Hết hạn bảo hành</Label>
<div className="flex gap-2">
<Input type="date" {...form.register("warrantyEnd")} className="flex-1" />
<div className="bg-gray-50 dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center mb-2">
<Label className="text-xs font-semibold text-gray-700 dark:text-gray-300">Hạn bảo hành</Label>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="edit-no-warranty"
checked={!form.watch("warrantyEnd") && warrantyMonths === ""}
onChange={(e) => {
if (e.target.checked) {
// Disable Warranty
setWarrantyMonths("");
form.setValue("warrantyEnd", null, { shouldDirty: true });
} else {
// Enable Warranty (Restore default)
setWarrantyMonths("12");
// Trigger update via effect or manual set if date exists
const pDate = form.getValues("purchaseDate");
if (pDate) {
const d = new Date(pDate);
d.setMonth(d.getMonth() + 12);
form.setValue("warrantyEnd", d.toISOString().split('T')[0], { shouldDirty: true });
}
}
}}
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 cursor-pointer"
/>
<label htmlFor="edit-no-warranty" className="text-xs font-medium text-gray-600 dark:text-gray-400 cursor-pointer select-none">Không bảo hành</label>
</div>
</div>
<div className="flex gap-2 mt-1 items-center">
<div className={`flex flex-col gap-2 transition-opacity duration-200 ${(!form.watch("warrantyEnd") && warrantyMonths === "") ? 'opacity-50 pointer-events-none' : ''}`}>
<Input
type="number"
placeholder="Tháng..."
className="h-8 w-20 text-xs"
value={warrantyMonths}
onChange={(e) => setWarrantyMonths(e.target.value)}
type="date"
{...form.register("warrantyEnd")}
className="h-9 bg-white dark:bg-gray-900"
disabled={!form.watch("warrantyEnd") && warrantyMonths === ""}
/>
<Select
className="h-8 flex-1 bg-white dark:bg-gray-800 text-xs"
onChange={(e) => {
if (e.target.value) setWarrantyMonths(e.target.value);
}}
value={""}
>
<option value="">+ Chọn nhanh...</option>
<option value="6">6 Tháng</option>
<option value="12">12 Tháng</option>
<option value="18">18 Tháng</option>
<option value="24">24 Tháng (2 Năm)</option>
<option value="36">36 Tháng (3 Năm)</option>
</Select>
<div className="flex gap-2 items-center">
<Input
type="number"
placeholder="Tháng..."
className="h-9 w-20 text-xs bg-white dark:bg-gray-900"
value={warrantyMonths}
onChange={(e) => setWarrantyMonths(e.target.value)}
disabled={!form.watch("warrantyEnd") && warrantyMonths === ""}
/>
<Select
className="h-9 flex-1 bg-white dark:bg-gray-900 text-xs"
onChange={(e) => {
if (e.target.value) setWarrantyMonths(e.target.value);
}}
value={""}
disabled={!form.watch("warrantyEnd") && warrantyMonths === ""}
>
<option value="">+ Chọn nhanh...</option>
<option value="6">6 Tháng</option>
<option value="12">12 Tháng</option>
<option value="18">18 Tháng</option>
<option value="24">24 Tháng (2 Năm)</option>
<option value="36">36 Tháng (3 Năm)</option>
</Select>
</div>
</div>
</div>
<div className="col-span-2">
@@ -754,58 +791,131 @@ 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>
);
})()}
{/* Display Badges Selection (Dynamic) */}
<div className="col-span-2 space-y-3 bg-gray-50/50 dark:bg-gray-800/50 p-4 rounded-xl border border-gray-200 dark:border-gray-700">
<div>
<Label className="flex items-center gap-2 text-base font-semibold">
Badge hiển thị
<span className="text-[10px] bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full font-bold">Custom</span>
</Label>
<p className="text-xs text-gray-500 mb-3">Chọn thông tin đ hiển thị nổi bật trên thẻ thiết bị.</p>
</div>
{(() => {
// Default or existing badges
const currentSpecs = form.watch("specs") || {};
let currentBadges: any[] = currentSpecs.displayBadges || [];
// Migration: If string[], convert to objects
if (currentBadges.length > 0 && typeof currentBadges[0] === 'string') {
currentBadges = currentBadges.map((key: string) => ({ key, icon: 'tag', color: 'blue' }));
}
const updateBadges = (newBadges: any[]) => {
const newSpecs = { ...currentSpecs, displayBadges: newBadges };
form.setValue("specs", newSpecs, { shouldDirty: true });
};
// Get all available keys
const specKeys = Object.keys(currentSpecs).filter(k => k !== 'displayBadges');
const standardKeys = ['brand', 'type', 'purchaseLocation', 'warrantyEnd'];
const allKeys = Array.from(new Set([...standardKeys, ...specKeys]));
return (
<div className="space-y-3">
{/* List of active badges */}
<div className="space-y-2">
{currentBadges.map((badge: any, idx: number) => (
<div key={idx} className="flex gap-2 items-center bg-white dark:bg-gray-900 p-2 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm animate-in slide-in-from-left-2 fade-in duration-200">
{/* Field Select */}
<div className="w-1/3 min-w-[120px]">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Trường thông tin</div>
<div className="font-medium text-sm truncate">{badge.key}</div>
</div>
{/* Icon Select (Dynamic) */}
<div className="flex-1">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Icon</div>
<Select
value={badge.icon || 'tag'}
onChange={(e) => {
const updated = [...currentBadges];
updated[idx] = { ...updated[idx], icon: e.target.value };
updateBadges(updated);
}}
className="h-8 text-xs w-full"
>
{Object.keys(BADGE_ICONS_MAP).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
{/* Color Select (Dynamic) */}
<div className="flex-1">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Màu</div>
<Select
value={badge.color || 'blue'}
onChange={(e) => {
const updated = [...currentBadges];
updated[idx] = { ...updated[idx], color: e.target.value };
updateBadges(updated);
}}
className="h-8 text-xs w-full"
>
{Object.keys(BADGE_COLORS_MAP).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
{/* Remove */}
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-red-500 hover:bg-red-50 hover:text-red-600 mt-4"
onClick={() => {
const updated = currentBadges.filter((_, i) => i !== idx);
updateBadges(updated);
}}
>
<Trash2 size={16} />
</Button>
</div>
))}
</div>
{/* Add New Badge */}
<div className="flex gap-2 items-end pt-2 border-t border-gray-200/50 dark:border-gray-700/50">
<div className="flex-1">
<Label className="text-xs mb-1 block">Thêm badge mới</Label>
<Select
id="new-badge-select"
className="h-9 text-sm"
onChange={(e) => {
if (e.target.value) {
const newBadge = {
key: e.target.value,
icon: 'tag',
color: 'blue'
};
updateBadges([...currentBadges, newBadge]);
e.target.value = ""; // Reset select
}
}}
value=""
>
<option value="">+ Chọn trường thông tin...</option>
{allKeys.filter(k => !currentBadges.some((b: any) => b.key === k)).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
</div>
</div>
);
})()}
</div>
</div>
</div>

View File

@@ -21,7 +21,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn, buildLocationTree } from "@/lib/utils";
import { LOCATION_ICONS, ITEM_ICONS } from "@/lib/constants/options";
import { LOCATION_ICONS, ITEM_ICONS, BADGE_ICONS_MAP, BADGE_COLORS_MAP } from "@/lib/constants/options";
import { optimizeImage, formatBytes, getBase64Size } from "@/lib/imageUtils";
@@ -798,6 +798,126 @@ export function SmartAddForm({ locations, onSuccess }: SmartAddFormProps) {
/>
</div>
</div>
{/* Display Badges Selection (Dynamic) */}
<div className="bg-white dark:bg-gray-900/50 p-4 rounded-xl border border-dashed border-gray-300 dark:border-gray-700">
<div>
<Label className="flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
<Tag className="h-4 w-4" /> Badge hiển thị trên Card
</Label>
<p className="text-[10px] text-gray-500 mb-3">Chọn thông tin đ hiển thị nổi bật.</p>
</div>
{(() => {
// Default or existing badges
const currentSpecs = form.watch("specs") || {};
let currentBadges: any[] = currentSpecs.displayBadges || [];
const updateBadges = (newBadges: any[]) => {
const newSpecs = { ...currentSpecs, displayBadges: newBadges };
form.setValue("specs", newSpecs, { shouldDirty: true });
};
// Get all available keys
const specKeys = Object.keys(currentSpecs).filter(k => k !== 'displayBadges');
const standardKeys = ['brand', 'type', 'purchaseLocation', 'warrantyEnd', 'purchasePrice'];
const allKeys = Array.from(new Set([...standardKeys, ...specKeys]));
return (
<div className="space-y-3">
{/* List of active badges */}
<div className="space-y-2">
{currentBadges.map((badge: any, idx: number) => (
<div key={idx} className="flex gap-2 items-center bg-gray-50 dark:bg-gray-800 p-2 rounded-lg border border-gray-200 dark:border-gray-700">
{/* Field Select */}
<div className="w-1/3 min-w-[120px]">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Trường thông tin</div>
<div className="font-medium text-sm truncate">{badge.key}</div>
</div>
{/* Icon Select */}
<div className="flex-1">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Icon</div>
<Select
value={badge.icon || 'tag'}
onChange={(e) => {
const updated = [...currentBadges];
updated[idx] = { ...updated[idx], icon: e.target.value };
updateBadges(updated);
}}
className="h-8 text-xs w-full bg-white dark:bg-gray-900"
>
{Object.keys(BADGE_ICONS_MAP).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
{/* Color Select */}
<div className="flex-1">
<div className="text-[10px] uppercase font-bold text-gray-400 mb-0.5">Màu</div>
<Select
value={badge.color || 'blue'}
onChange={(e) => {
const updated = [...currentBadges];
updated[idx] = { ...updated[idx], color: e.target.value };
updateBadges(updated);
}}
className="h-8 text-xs w-full bg-white dark:bg-gray-900"
>
{Object.keys(BADGE_COLORS_MAP).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
{/* Remove */}
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-gray-400 hover:text-red-500 mt-4"
onClick={() => {
const updated = currentBadges.filter((_, i) => i !== idx);
updateBadges(updated);
}}
>
<Trash2 size={16} />
</Button>
</div>
))}
</div>
{/* Add New Badge */}
<div className="flex gap-2 items-end pt-2">
<div className="flex-1">
<Select
id="new-badge-select"
className="h-9 text-sm bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
onChange={(e) => {
if (e.target.value) {
const newBadge = {
key: e.target.value,
icon: 'tag',
color: 'blue'
};
updateBadges([...currentBadges, newBadge]);
e.target.value = ""; // Reset select
}
}}
value=""
>
<option value="">+ Thêm Badge hiển thị...</option>
{allKeys.filter(k => !currentBadges.some((b: any) => b.key === k)).map(k => (
<option key={k} value={k}>{k}</option>
))}
</Select>
</div>
</div>
</div>
);
})()}
</div>
</div>

View File

@@ -19,6 +19,7 @@ interface LocationNode {
name: string;
type: string;
icon?: string | null;
image?: string | null;
parentId?: string | null;
children: LocationNode[];
_count?: { items: number };
@@ -393,28 +394,39 @@ export function LocationManager({ initialLocations }: { initialLocations: Locati
<div className="flex-1 flex flex-col h-full">
{/* Detail Header */}
<div className="px-6 py-4 border-b border-primary-100 dark:border-gray-800 bg-primary-50/30 dark:bg-gray-800/40 flex justify-between items-start">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
{(() => {
if (selectedLocation?.icon) {
const { icon: Icon, color } = LOCATION_ICONS[selectedLocation.icon] || LOCATION_ICONS['default'] || ITEM_ICONS['default'];
return <Icon className={cn("h-6 w-6", color)} />;
}
return selectedLocation?.type === 'Person' ? <User className="text-purple-500" /> : selectedLocation?.type === 'Container' ? <Box className="text-primary-500" /> : <Folder className="text-amber-500" />
})()}
{selectedLocation?.name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
<span className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-2 py-0.5 rounded text-xs">{selectedLocation?.type}</span>
<span></span>
<span>ID: {selectedLocation?.id.slice(0, 8)}</span>
</p>
<div className="flex items-start gap-4">
{selectedLocation?.image ? (
<div className="h-16 w-16 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden shadow-sm bg-white dark:bg-gray-800">
<img src={selectedLocation.image} alt={selectedLocation.name} className="h-full w-full object-cover" />
</div>
) : (
<div className="h-16 w-16 rounded-xl border border-primary-100 dark:border-gray-700 bg-white dark:bg-gray-800 flex items-center justify-center shadow-sm">
{(() => {
if (selectedLocation?.icon) {
const { icon: Icon, color } = LOCATION_ICONS[selectedLocation.icon] || LOCATION_ICONS['default'] || ITEM_ICONS['default'];
return <Icon className={cn("h-8 w-8", color)} />;
}
return selectedLocation?.type === 'Person' ? <User size={32} className="text-purple-500" /> : selectedLocation?.type === 'Container' ? <Box size={32} className="text-primary-500" /> : <Folder size={32} className="text-amber-500" />
})()}
</div>
)}
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
{selectedLocation?.name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
<span className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-2 py-0.5 rounded text-xs">{selectedLocation?.type}</span>
<span></span>
<span>ID: {selectedLocation?.id.slice(0, 8)}</span>
</p>
</div>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={async () => {
setNewLocName(selectedLocation?.name || "");
setNewLocType(selectedLocation?.type || "Fixed");
setNewLocIcon(selectedLocation?.icon || "");
setNewLocImage(selectedLocation?.image || null);
setNewLocParentId(selectedLocation?.parentId || null);
setIsEditing(true);
}} className="bg-white dark:bg-gray-800 border border-primary-200 dark:border-gray-700 text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-900/20 gap-1">

View File

@@ -17,6 +17,7 @@ import {
Container, Backpack, Luggage,
User, Users,
Table, Layers,
Zap, Clock, Info, DollarSign, Star, Award, ThumbsUp, Check, AlertTriangle, Settings, Search, Link2, Mail, Music, Video, Image,
type LucideIcon
} from "lucide-react";
@@ -230,3 +231,92 @@ export const LOCATION_ICON_GROUPS = [
items: ["Person", "Family"]
}
];
// Badge Configuration
export const BADGE_ICONS_MAP: Record<string, any> = {
// Hardware
'cpu': Cpu,
'circuit': CircuitBoard,
'ram': MemoryStick,
'fan': Fan,
'battery': Battery,
'wifi': Wifi,
'bluetooth': Bluetooth,
'server': Server,
'harddrive': HardDrive,
// Abstract
'zap': Zap,
'tag': Tag,
'box': Box,
'clock': Clock,
'user': User,
'info': Info,
'dollar': DollarSign,
'lock': Lock,
'shield': Shield,
'star': Star,
'award': Award,
'thumbs-up': ThumbsUp,
'check': Check,
'alert': AlertTriangle,
// Tools
'tools': Wrench,
'settings': Settings,
'search': Search,
'link': Link2,
// Office
'file': FileText,
'folder': Folder,
'archive': Archive,
'mail': Mail,
// Fun
'gamepad': Gamepad2,
'music': Music,
'video': Video,
'image': Image,
};
export const BADGE_COLORS_MAP: Record<string, string> = {
'blue': "bg-blue-50 text-blue-700 border-blue-100",
'red': "bg-red-50 text-red-700 border-red-100",
'green': "bg-green-50 text-green-700 border-green-100",
'emerald': "bg-emerald-50 text-emerald-700 border-emerald-100",
'orange': "bg-orange-50 text-orange-700 border-orange-100",
'amber': "bg-amber-50 text-amber-700 border-amber-100",
'yellow': "bg-yellow-50 text-yellow-700 border-yellow-100",
'purple': "bg-purple-50 text-purple-700 border-purple-100",
'fuchsia': "bg-fuchsia-50 text-fuchsia-700 border-fuchsia-100",
'pink': "bg-pink-50 text-pink-700 border-pink-100",
'rose': "bg-rose-50 text-rose-700 border-rose-100",
'cyan': "bg-cyan-50 text-cyan-700 border-cyan-100",
'sky': "bg-sky-50 text-sky-700 border-sky-100",
'indigo': "bg-indigo-50 text-indigo-700 border-indigo-100",
'violet': "bg-violet-50 text-violet-700 border-violet-100",
'slate': "bg-slate-50 text-slate-700 border-slate-100",
'gray': "bg-gray-50 text-gray-700 border-gray-100",
'stone': "bg-stone-50 text-stone-700 border-stone-100",
'zinc': "bg-zinc-50 text-zinc-700 border-zinc-100",
};
export const BADGE_ICON_GROUPS = [
{
label: "Phổ biến",
items: ['tag', 'zap', 'battery', 'wifi', 'cpu', 'box', 'clock', 'user', 'dollar']
},
{
label: "Phần cứng",
items: ['circuit', 'ram', 'fan', 'server', 'harddrive', 'gamepad', 'video', 'music', 'image']
},
{
label: "Hành chính",
items: ['info', 'lock', 'shield', 'file', 'folder', 'archive', 'mail', 'link']
},
{
label: "Trạng thái",
items: ['check', 'alert', 'star', 'award', 'thumbs-up', 'settings', 'search', 'tools']
}
];