mirror of
https://github.com/duongcamcute/tech-gadget-manager.git
synced 2026-06-27 22:36:08 +00:00
feat: Cải thiện UI Mobile Dialog, thêm thời gian sử dụng và sửa lỗi ESLint
## UI/UX
- Sửa layout Dialog chi tiết trên Mobile (tránh rớt chữ Bluetooth/Receiver)
- Thêm logic tính toán thời gian sử dụng (⏱️ X năm Y tháng)
- Thêm break-words/break-all cho các trường text dài
## Code Quality
- Sửa lỗi ESLint và TypeScript trong src/app/actions.ts
- Loại bỏ các any type không an toàn
- Cải thiện xử lý lỗi (unknown type + instanceof Error)
This commit is contained in:
@@ -123,9 +123,9 @@ export async function createItem(data: ItemFormData) {
|
||||
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("CREATE ERROR:", error);
|
||||
return { success: false, error: "Lỗi lưu dữ liệu: " + (error.message || "") };
|
||||
return { success: false, error: "Lỗi lưu dữ liệu: " + (error instanceof Error ? error.message : "Unknown error") };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,9 +276,10 @@ export async function updateItem(id: string, data: ItemFormData) {
|
||||
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("UPDATE ERROR:", error);
|
||||
return { success: false, error: "Lỗi cập nhật: " + error.message };
|
||||
const errorMessage = error instanceof Error ? error.message : "Unkown error";
|
||||
return { success: false, error: "Lỗi cập nhật: " + errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +321,6 @@ export async function deleteItem(id: string) {
|
||||
details: "Đã xóa thiết bị vĩnh viễn"
|
||||
});
|
||||
await triggerWebhooks("item.deleted", { id, name: item?.name });
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, error: "Không thể xóa" };
|
||||
@@ -396,8 +396,9 @@ export async function bulkMoveItems(ids: string[], locationId: string | null) {
|
||||
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: "Lỗi chuyển kho hàng loạt: " + e.message };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: "Lỗi chuyển kho hàng loạt: " + msg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,8 +439,9 @@ export async function bulkDeleteItems(ids: string[]) {
|
||||
});
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: "Lỗi xóa hàng loạt: " + e.message };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: "Lỗi xóa hàng loạt: " + msg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,8 +570,9 @@ export async function updateUserProfile(id: string, newUsername: string, newPass
|
||||
avatar: updated.avatar ?? null
|
||||
}
|
||||
};
|
||||
} catch (e: any) {
|
||||
return { success: false, error: "Lỗi cập nhật: " + e.message };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: "Lỗi cập nhật: " + msg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +589,7 @@ export async function saveThemeSettings(id: string, theme: string, colors: strin
|
||||
data: { theme, colors }
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
return { success: false, error: "Lỗi lưu cài đặt" };
|
||||
}
|
||||
}
|
||||
@@ -602,7 +605,7 @@ export async function addBrandAction(name: string) {
|
||||
await prisma.brand.create({ data: { name } });
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
return { success: false, error: "Hãng đã tồn tại hoặc lỗi khác" };
|
||||
}
|
||||
}
|
||||
@@ -625,11 +628,11 @@ export async function createTemplate(data: TemplateData) {
|
||||
config: result.data.config
|
||||
}
|
||||
});
|
||||
revalidatePath("/");
|
||||
revalidatePath("/settings");
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: "Lỗi tạo mẫu: " + e.message };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: "Lỗi tạo mẫu: " + msg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,10 +649,9 @@ export async function deleteTemplate(id: string) {
|
||||
// -----------------------
|
||||
try {
|
||||
await prisma.template.delete({ where: { id } });
|
||||
revalidatePath("/");
|
||||
revalidatePath("/settings");
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
return { success: false, error: "Lỗi xóa mẫu" };
|
||||
}
|
||||
}
|
||||
@@ -661,6 +663,7 @@ export async function exportDatabase() {
|
||||
try {
|
||||
// Lấy users nhưng loại bỏ password để bảo mật
|
||||
const usersRaw = await prisma.user.findMany();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const usersSafe = usersRaw.map(({ password, ...rest }) => rest);
|
||||
|
||||
const data = {
|
||||
@@ -676,8 +679,9 @@ export async function exportDatabase() {
|
||||
exportedAt: new Date().toISOString()
|
||||
};
|
||||
return { success: true, data: JSON.stringify(data, null, 2) };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: "Lỗi xuất dữ liệu: " + e.message };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: "Lỗi xuất dữ liệu: " + msg };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,42 @@ const formatCurrency = (amount: number | null | undefined) => {
|
||||
return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(amount);
|
||||
};
|
||||
|
||||
const calculateUsageDuration = (startDate: string | Date | null | undefined) => {
|
||||
if (!startDate) return null;
|
||||
const start = new Date(startDate);
|
||||
const now = new Date();
|
||||
|
||||
// Nếu ngày mua > hiện tại (vô lý)
|
||||
if (start > now) return "Chưa sử dụng";
|
||||
|
||||
let years = now.getFullYear() - start.getFullYear();
|
||||
let months = now.getMonth() - start.getMonth();
|
||||
let days = now.getDate() - start.getDate();
|
||||
|
||||
if (days < 0) {
|
||||
months--;
|
||||
// Lấy số ngày của tháng trước đó
|
||||
const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
days += prevMonth.getDate();
|
||||
}
|
||||
if (months < 0) {
|
||||
years--;
|
||||
months += 12;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (years > 0) parts.push(`${years} năm`);
|
||||
if (months > 0) parts.push(`${months} tháng`);
|
||||
|
||||
// Nếu chưa đầy 1 tháng, hiển thị số ngày
|
||||
if (years === 0 && months === 0) {
|
||||
if (days === 0) return "Vừa mới mua";
|
||||
return `${days} ngày`;
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
// --- Local UI Components for speed ---
|
||||
const Badge = ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold ${className}`}>{children}</span>
|
||||
@@ -170,19 +206,33 @@ function ViewMode({ item, setMode, onDelete }: { item: any, setMode: (m: "EDIT")
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100 uppercase tracking-wider mb-3">Chi tiết kỹ thuật</h3>
|
||||
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-gray-900/50 p-4 rounded-xl border border-slate-100 dark:border-gray-700">
|
||||
{item.brand && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Hãng sản xuất</p><p className="font-medium text-gray-900 dark:text-gray-100">{item.brand}</p></div>}
|
||||
{item.model && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Model</p><p className="font-mono font-medium text-gray-900 dark:text-gray-100">{item.model}</p></div>}
|
||||
{/* Removed duplicate Model */}
|
||||
{item.brand && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Hãng sản xuất</p><p className="font-medium text-gray-900 dark:text-gray-100 break-words">{item.brand}</p></div>}
|
||||
{item.model && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Model</p><p className="font-mono font-medium text-gray-900 dark:text-gray-100 break-words">{item.model}</p></div>}
|
||||
|
||||
{item.color && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Màu sắc</p><div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full border border-gray-300" style={{ backgroundColor: getColorHex(item.color) }}></div><p className="font-medium text-gray-900 dark:text-gray-100">{item.color}</p></div></div>}
|
||||
{item.purchaseDate && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Ngày mua</p><p className="font-medium text-gray-900 dark:text-gray-100">{formatDateVN(item.purchaseDate)}</p></div>}
|
||||
|
||||
{item.purchaseDate && (
|
||||
<>
|
||||
<div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Ngày mua</p><p className="font-medium text-gray-900 dark:text-gray-100">{formatDateVN(item.purchaseDate)}</p></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Đã dùng</p>
|
||||
<p className="font-medium text-blue-600 dark:text-blue-400">
|
||||
⏱️ {calculateUsageDuration(item.purchaseDate)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{item.purchasePrice && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Giá mua</p><p className="font-medium text-gray-900 dark:text-gray-100">{formatCurrency(item.purchasePrice)}</p></div>}
|
||||
{item.warrantyEnd && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Bảo hành đến</p><p className="font-medium text-green-700 dark:text-green-400">{formatDateVN(item.warrantyEnd)}</p></div>}
|
||||
{item.purchaseLocation && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Nơi mua</p><p className="font-medium text-gray-900 dark:text-gray-100">{item.purchaseLocation}</p></div>}
|
||||
{item.purchaseUrl && <div className="col-span-2"><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Link mua hàng</p><a href={item.purchaseUrl} target="_blank" rel="noopener noreferrer" className="font-medium text-blue-600 dark:text-blue-400 hover:underline break-words block">{item.purchaseUrl}</a></div>}
|
||||
{item.purchaseLocation && <div><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Nơi mua</p><p className="font-medium text-gray-900 dark:text-gray-100 break-words">{item.purchaseLocation}</p></div>}
|
||||
|
||||
{/* Dynamic Specs */}
|
||||
{/* Force full width on mobile for URL to avoid layout break */}
|
||||
{item.purchaseUrl && <div className="col-span-2"><p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">Link mua hàng</p><a href={item.purchaseUrl} target="_blank" rel="noopener noreferrer" className="font-medium text-blue-600 dark:text-blue-400 hover:underline break-all block">{item.purchaseUrl}</a></div>}
|
||||
|
||||
{/* Dynamic Specs - Smart Layout */}
|
||||
{Object.entries(item.specs ? JSON.parse(item.specs as string) : {}).map(([k, v]: any) => (
|
||||
<div key={k} className="col-span-1">
|
||||
<div key={k} className={`${String(v).length > 20 ? 'col-span-2' : 'col-span-1'}`}>
|
||||
<p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase">{k}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100 break-words" title={v}>{v}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user