Download src/components/MachineManager.tsx from phucsd/PrintCal: direct link, hf CLI and curl.
- Browser
- Download file 33 kB
-
https://huggingface.co/spaces/phucsd/PrintCal/resolve/main/src/components/MachineManager.tsx
- Command line
-
hf download hf://spaces/phucsd/PrintCal/src/components/MachineManager.tsx
-
curl -L -o MachineManager.tsx https://huggingface.co/spaces/phucsd/PrintCal/resolve/main/src/components/MachineManager.tsx
33 kB
| 'use client'; | |
| import React, { useState, useEffect } from 'react'; | |
| import { DigitalMachine, DigitalPrintGiaCong, DigitalPrintKemGiay, OffsetMachine } from '@/types'; | |
| import { Cpu, Printer, Edit2, Check, RefreshCw, Layers, FileText, Info, Save, X } from 'lucide-react'; | |
| export const MachineManager: React.FC = () => { | |
| const [activeTab, setActiveTab] = useState<'giacong' | 'kem_giay' | 'offset'>('giacong'); | |
| const [offsetMachines, setOffsetMachines] = useState<OffsetMachine[]>([]); | |
| const [digitalMachines, setDigitalMachines] = useState<DigitalMachine[]>([]); | |
| const [digitalPricingGiaCong, setDigitalPricingGiaCong] = useState<DigitalPrintGiaCong[]>([]); | |
| const [digitalPricingKemGiay, setDigitalPricingKemGiay] = useState<DigitalPrintKemGiay[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| // Editing state | |
| const [editingOffset, setEditingOffset] = useState<OffsetMachine | null>(null); | |
| const [editingGiaCong, setEditingGiaCong] = useState<DigitalPrintGiaCong | null>(null); | |
| const [editingKemGiay, setEditingKemGiay] = useState<DigitalPrintKemGiay | null>(null); | |
| // Filter for INTC paper | |
| const [paperFilter, setPaperFilter] = useState<'all' | 'C' | 'I' | 'F'>('all'); | |
| const fetchMachines = async () => { | |
| try { | |
| setLoading(true); | |
| const res = await fetch('/api/machines'); | |
| const data = await res.json(); | |
| if (data.offsetMachines) setOffsetMachines(data.offsetMachines); | |
| if (data.digitalMachines) setDigitalMachines(data.digitalMachines); | |
| if (data.digitalPricingGiaCong) setDigitalPricingGiaCong(data.digitalPricingGiaCong); | |
| if (data.digitalPricingKemGiay) setDigitalPricingKemGiay(data.digitalPricingKemGiay); | |
| } catch (err) { | |
| console.error(err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchMachines(); | |
| }, []); | |
| const handleSaveOffset = async (m: OffsetMachine) => { | |
| try { | |
| const res = await fetch('/api/machines', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ type: 'offset', ...m }), | |
| }); | |
| if (res.ok) { | |
| setEditingOffset(null); | |
| fetchMachines(); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| const handleSaveGiaCong = async (item: DigitalPrintGiaCong) => { | |
| try { | |
| const res = await fetch('/api/machines', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ type: 'digital_giacong', ...item }), | |
| }); | |
| if (res.ok) { | |
| setEditingGiaCong(null); | |
| fetchMachines(); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| const handleSaveKemGiay = async (item: DigitalPrintKemGiay) => { | |
| try { | |
| const res = await fetch('/api/machines', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ type: 'digital_kem_giay', ...item }), | |
| }); | |
| if (res.ok) { | |
| setEditingKemGiay(null); | |
| fetchMachines(); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| const filteredKemGiay = digitalPricingKemGiay.filter((item) => { | |
| if (paperFilter === 'all') return true; | |
| if (paperFilter === 'C') return item.paperCode.startsWith('C'); | |
| if (paperFilter === 'I') return item.paperCode.startsWith('I'); | |
| if (paperFilter === 'F') return item.paperCode.startsWith('F'); | |
| return true; | |
| }); | |
| return ( | |
| <div className="space-y-5"> | |
| {/* Top Header & Navigation Tabs */} | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-white p-4 rounded-2xl border border-slate-200 shadow-2xs"> | |
| <div> | |
| <h2 className="text-base font-bold text-slate-900 flex items-center gap-2"> | |
| <Printer className="w-5 h-5 text-blue-600" /> | |
| Quản Lý Bảng Giá In Ấn | |
| </h2> | |
| <p className="text-xs text-slate-500 mt-0.5"> | |
| Cập nhật biểu giá In nhanh gia công (Konica C12000), In nhanh kèm giấy (INTC) và Máy in Offset | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex bg-slate-100 p-1 rounded-xl text-xs font-semibold"> | |
| <button | |
| onClick={() => setActiveTab('giacong')} | |
| className={`px-3 py-1.5 rounded-lg transition-all ${ | |
| activeTab === 'giacong' | |
| ? 'bg-white shadow text-sky-700 font-bold' | |
| : 'text-slate-600 hover:text-slate-900' | |
| }`} | |
| > | |
| ⚙️ Konica C12000 (Gia Công) | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('kem_giay')} | |
| className={`px-3 py-1.5 rounded-lg transition-all ${ | |
| activeTab === 'kem_giay' | |
| ? 'bg-white shadow text-indigo-700 font-bold' | |
| : 'text-slate-600 hover:text-slate-900' | |
| }`} | |
| > | |
| 📦 INTC (Kèm Giấy) | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('offset')} | |
| className={`px-3 py-1.5 rounded-lg transition-all ${ | |
| activeTab === 'offset' | |
| ? 'bg-white shadow text-blue-700 font-bold' | |
| : 'text-slate-600 hover:text-slate-900' | |
| }`} | |
| > | |
| 🏭 Máy In Offset | |
| </button> | |
| </div> | |
| <button | |
| onClick={fetchMachines} | |
| disabled={loading} | |
| className="p-2 text-slate-500 hover:text-blue-600 rounded-lg hover:bg-slate-100 transition-colors" | |
| title="Làm mới dữ liệu" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* TAB 1: BẢNG GIÁ IN GIA CÔNG KONICA C12000 (TRANG 2 PDF 26.02.2026) */} | |
| {activeTab === 'giacong' && ( | |
| <div className="bg-white rounded-2xl border border-slate-200 shadow-2xs overflow-hidden space-y-4"> | |
| <div className="p-5 border-b border-slate-200 bg-gradient-to-r from-sky-50 to-blue-50/40"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> | |
| <div> | |
| <span className="px-2.5 py-0.5 bg-sky-600 text-white rounded-md text-[10px] font-bold uppercase tracking-wider"> | |
| Trang 2 PDF 26.02.2026 | |
| </span> | |
| <h3 className="text-base font-black text-slate-900 mt-1"> | |
| MÁY IN KONICA MINOLTA C12000 / C12010S 5 MÀU | |
| </h3> | |
| <p className="text-xs text-slate-600 mt-0.5"> | |
| Đơn giá in gia công (chưa gồm giấy) theo 5 kích thước tiêu chuẩn & 7 nhóm chất liệu. | |
| </p> | |
| </div> | |
| </div> | |
| {/* Quy tắc phụ phí & lưu ý */} | |
| <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2 text-xs"> | |
| <div className="bg-white/80 border border-sky-200 rounded-xl p-2.5 flex items-start gap-2"> | |
| <Info className="w-4 h-4 text-sky-600 shrink-0 mt-0.5" /> | |
| <div> | |
| <strong className="text-sky-900 block font-semibold">Phụ phí số lượng ít:</strong> | |
| <span className="text-slate-600 text-[11px]"> | |
| < 50 tờ: <strong>+30.000đ</strong> | < 100 tờ: <strong>+20.000đ</strong> | ≥ 100 tờ: <strong>Miễn phí</strong> | |
| </span> | |
| </div> | |
| </div> | |
| <div className="bg-white/80 border border-sky-200 rounded-xl p-2.5 flex items-start gap-2"> | |
| <Info className="w-4 h-4 text-indigo-600 shrink-0 mt-0.5" /> | |
| <div> | |
| <strong className="text-indigo-900 block font-semibold">Quy ước tính mặt in:</strong> | |
| <span className="text-slate-600 text-[11px]"> | |
| Đơn giá hiển thị dưới đây là đơn giá <strong>in 1 mặt</strong>. In 2 mặt được tính <strong>nhân đôi (×2)</strong>. | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-5 pt-0 overflow-x-auto"> | |
| <table className="w-full text-xs text-left border-collapse min-w-[760px]"> | |
| <thead> | |
| <tr className="bg-slate-100 text-slate-700 font-bold border-b border-slate-200"> | |
| <th className="p-3 text-center w-36">Khổ in</th> | |
| <th className="p-3 text-right">Giấy <249gsm</th> | |
| <th className="p-3 text-right">Giấy 250-349gsm</th> | |
| <th className="p-3 text-right">Giấy 350-450gsm</th> | |
| <th className="p-3 text-right">Decal (Giấy-nhựa)</th> | |
| <th className="p-3 text-right">Decal trong</th> | |
| <th className="p-3 text-right">Giấy nhựa <280gsm</th> | |
| <th className="p-3 text-right">Nhựa PVC</th> | |
| <th className="p-3 text-center w-24">Thao tác</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-slate-100"> | |
| {digitalPricingGiaCong.map((row) => { | |
| const isEditing = editingGiaCong?.id === row.id; | |
| const cur = isEditing ? editingGiaCong : row; | |
| return ( | |
| <tr key={row.id} className="hover:bg-slate-50/70 transition-colors"> | |
| <td className="p-3 text-center font-bold text-slate-800"> | |
| <span className="text-sm block">{row.sheetSize}</span> | |
| <span className="text-[10px] text-slate-500 font-normal"> | |
| {row.widthMm} × {row.heightMm} mm | |
| </span> | |
| </td> | |
| {/* Giấy <249gsm */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.paperLt249} | |
| onChange={(e) => setEditingGiaCong({ ...cur, paperLt249: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-slate-800">{row.paperLt249.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Giấy 250-349gsm */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.paper250To349} | |
| onChange={(e) => setEditingGiaCong({ ...cur, paper250To349: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-slate-800">{row.paper250To349.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Giấy 350-450gsm */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.paper350To450} | |
| onChange={(e) => setEditingGiaCong({ ...cur, paper350To450: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-slate-800">{row.paper350To450.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Decal giấy nhựa */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.decalPaperPlastic} | |
| onChange={(e) => setEditingGiaCong({ ...cur, decalPaperPlastic: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-700">{row.decalPaperPlastic.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Decal trong */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.decalClear} | |
| onChange={(e) => setEditingGiaCong({ ...cur, decalClear: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-700">{row.decalClear.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Giấy nhựa <280 */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.syntheticPaper} | |
| onChange={(e) => setEditingGiaCong({ ...cur, syntheticPaper: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-700">{row.syntheticPaper.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </td> | |
| {/* Nhựa PVC */} | |
| <td className="p-2 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-20 p-1 border rounded text-right font-bold text-blue-700 text-xs" | |
| value={cur.pvcPlastic} | |
| onChange={(e) => setEditingGiaCong({ ...cur, pvcPlastic: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-700"> | |
| {row.pvcPlastic > 0 ? `${row.pvcPlastic.toLocaleString('vi-VN')}đ` : '—'} | |
| </span> | |
| )} | |
| </td> | |
| {/* Nút hành động */} | |
| <td className="p-2 text-center"> | |
| {isEditing ? ( | |
| <div className="flex items-center justify-center gap-1"> | |
| <button | |
| onClick={() => handleSaveGiaCong(cur)} | |
| className="p-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded shadow-xs" | |
| title="Lưu thay đổi" | |
| > | |
| <Save className="w-3.5 h-3.5" /> | |
| </button> | |
| <button | |
| onClick={() => setEditingGiaCong(null)} | |
| className="p-1.5 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded" | |
| title="Hủy" | |
| > | |
| <X className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setEditingGiaCong({ ...row })} | |
| className="p-1.5 text-slate-400 hover:text-sky-600 rounded hover:bg-sky-50 transition-colors" | |
| title="Chỉnh sửa đơn giá hàng này" | |
| > | |
| <Edit2 className="w-3.5 h-3.5" /> | |
| </button> | |
| )} | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 2: BẢNG GIÁ IN NHANH KÈM GIẤY INTC (PDF 14.03.2026) */} | |
| {activeTab === 'kem_giay' && ( | |
| <div className="bg-white rounded-2xl border border-slate-200 shadow-2xs overflow-hidden space-y-4"> | |
| <div className="p-5 border-b border-slate-200 bg-gradient-to-r from-indigo-50 to-purple-50/40"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div> | |
| <span className="px-2.5 py-0.5 bg-indigo-600 text-white rounded-md text-[10px] font-bold uppercase tracking-wider"> | |
| Bảng Giá INTC 14.03.2026 | |
| </span> | |
| <h3 className="text-base font-black text-slate-900 mt-1"> | |
| BẢNG GIÁ IN NHANH KÈM GIẤY (TRỌN GÓI) | |
| </h3> | |
| <p className="text-xs text-slate-600 mt-0.5"> | |
| Đơn giá trọn gói đã bao gồm tiền giấy và công in. Khổ in 325 × 430 mm và 325 × 355 mm. | |
| </p> | |
| </div> | |
| {/* Bộ lọc loại giấy */} | |
| <div className="flex items-center gap-1 bg-white p-1 rounded-xl border border-indigo-200 text-xs font-semibold"> | |
| <button | |
| onClick={() => setPaperFilter('all')} | |
| className={`px-2.5 py-1 rounded-lg ${paperFilter === 'all' ? 'bg-indigo-600 text-white' : 'text-slate-600'}`} | |
| > | |
| Tất cả ({digitalPricingKemGiay.length}) | |
| </button> | |
| <button | |
| onClick={() => setPaperFilter('C')} | |
| className={`px-2.5 py-1 rounded-lg ${paperFilter === 'C' ? 'bg-indigo-600 text-white' : 'text-slate-600'}`} | |
| > | |
| Couche (6) | |
| </button> | |
| <button | |
| onClick={() => setPaperFilter('I')} | |
| className={`px-2.5 py-1 rounded-lg ${paperFilter === 'I' ? 'bg-indigo-600 text-white' : 'text-slate-600'}`} | |
| > | |
| Ivory (3) | |
| </button> | |
| <button | |
| onClick={() => setPaperFilter('F')} | |
| className={`px-2.5 py-1 rounded-lg ${paperFilter === 'F' ? 'bg-indigo-600 text-white' : 'text-slate-600'}`} | |
| > | |
| Fort (7) | |
| </button> | |
| </div> | |
| </div> | |
| {/* Phụ phí */} | |
| <div className="mt-3 bg-white/80 border border-indigo-200 rounded-xl p-2.5 flex items-start gap-2 text-xs"> | |
| <Info className="w-4 h-4 text-indigo-600 shrink-0 mt-0.5" /> | |
| <div> | |
| <strong className="text-indigo-900 block font-semibold">Quy định phụ phí số lượng ít:</strong> | |
| <span className="text-slate-600 text-[11px]"> | |
| Số lượng < 50 tờ: <strong>+30.000đ</strong> | < 100 tờ: <strong>+20.000đ</strong> | ≥ 100 tờ: <strong>Miễn phí phụ phí</strong> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-5 pt-0 overflow-x-auto"> | |
| <table className="w-full text-xs text-left border-collapse"> | |
| <thead> | |
| <tr className="bg-slate-100 text-slate-700 font-bold border-b border-slate-200"> | |
| <th className="p-3 text-center w-20">Mã</th> | |
| <th className="p-3 text-left">Chất liệu & Định lượng</th> | |
| <th className="p-3 text-center w-32">Khổ in</th> | |
| <th className="p-3 text-right w-36">In 1 mặt (+ Giấy)</th> | |
| <th className="p-3 text-right w-36">In 2 mặt (+ Giấy)</th> | |
| <th className="p-3 text-center w-24">Thao tác</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-slate-100"> | |
| {filteredKemGiay.map((row) => { | |
| const isEditing = editingKemGiay?.id === row.id; | |
| const cur = isEditing ? editingKemGiay : row; | |
| return ( | |
| <tr key={row.id} className="hover:bg-slate-50/70 transition-colors"> | |
| <td className="p-3 text-center font-bold text-indigo-700"> | |
| <span className="px-2 py-0.5 bg-indigo-50 border border-indigo-200 rounded font-mono"> | |
| {row.paperCode} | |
| </span> | |
| </td> | |
| <td className="p-3"> | |
| <strong className="text-slate-900 block text-xs">{row.paperName}</strong> | |
| <span className="text-[11px] text-slate-500 font-medium">Định lượng: {row.gsm} gsm</span> | |
| </td> | |
| <td className="p-3 text-center font-semibold text-slate-700"> | |
| <span className="px-2 py-0.5 bg-slate-100 rounded text-[11px]"> | |
| {row.sheetSize} mm | |
| </span> | |
| </td> | |
| {/* Đơn giá 1 mặt */} | |
| <td className="p-3 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-24 p-1.5 border rounded text-right font-bold text-indigo-700 text-xs" | |
| value={cur.price1Side} | |
| onChange={(e) => setEditingKemGiay({ ...cur, price1Side: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-slate-900 text-sm"> | |
| {row.price1Side.toLocaleString('vi-VN')}đ | |
| </span> | |
| )} | |
| </td> | |
| {/* Đơn giá 2 mặt */} | |
| <td className="p-3 text-right"> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-24 p-1.5 border rounded text-right font-bold text-purple-700 text-xs" | |
| value={cur.price2Side} | |
| onChange={(e) => setEditingKemGiay({ ...cur, price2Side: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-purple-700 text-sm"> | |
| {row.price2Side.toLocaleString('vi-VN')}đ | |
| </span> | |
| )} | |
| </td> | |
| {/* Nút thao tác */} | |
| <td className="p-3 text-center"> | |
| {isEditing ? ( | |
| <div className="flex items-center justify-center gap-1"> | |
| <button | |
| onClick={() => handleSaveKemGiay(cur)} | |
| className="p-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded shadow-xs" | |
| title="Lưu thay đổi" | |
| > | |
| <Save className="w-3.5 h-3.5" /> | |
| </button> | |
| <button | |
| onClick={() => setEditingKemGiay(null)} | |
| className="p-1.5 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded" | |
| title="Hủy" | |
| > | |
| <X className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setEditingKemGiay({ ...row })} | |
| className="p-1.5 text-slate-400 hover:text-indigo-600 rounded hover:bg-indigo-50 transition-colors" | |
| title="Sửa giá" | |
| > | |
| <Edit2 className="w-3.5 h-3.5" /> | |
| </button> | |
| )} | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 3: MÁY IN OFFSET */} | |
| {activeTab === 'offset' && ( | |
| <div className="bg-white rounded-2xl border border-slate-200 shadow-2xs overflow-hidden space-y-4"> | |
| <div className="p-5 border-b border-slate-200 bg-gradient-to-r from-blue-50 to-indigo-50/40"> | |
| <h3 className="font-bold text-slate-900 text-sm flex items-center gap-2"> | |
| <Cpu className="w-5 h-5 text-indigo-600" /> | |
| Cấu Hình Máy In Offset & Gói In Trọn Gói Gồm Kẽm | |
| </h3> | |
| <p className="text-xs text-slate-600 mt-0.5"> | |
| Khổ 65 × 86 cm (bắt nhíp chiều 86): 1.200.000đ | Khổ nhỏ dưới 65 × 43 cm: 900.000đ (Dưới 3.000 lượt in đã bao gồm 4 kẽm) | |
| </p> | |
| </div> | |
| <div className="p-5 pt-0 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {offsetMachines.map((m) => { | |
| const isEditing = editingOffset?.id === m.id; | |
| const current = isEditing ? editingOffset : m; | |
| return ( | |
| <div key={m.id} className="border border-slate-200 rounded-xl p-4 bg-slate-50/50 flex flex-col justify-between space-y-3"> | |
| <div className="flex items-center justify-between border-b border-slate-200 pb-2"> | |
| <strong className="text-xs font-bold text-slate-900">{current.name}</strong> | |
| <button | |
| onClick={() => setEditingOffset(isEditing ? null : { ...m })} | |
| className="p-1 text-slate-400 hover:text-indigo-600" | |
| > | |
| <Edit2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| <div className="space-y-2 text-xs"> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Khổ in max:</span> | |
| {isEditing ? ( | |
| <div className="flex gap-1 w-32"> | |
| <input | |
| type="number" | |
| className="w-1/2 p-1 border rounded text-right" | |
| value={current.maxWidthMm} | |
| onChange={(e) => setEditingOffset({ ...current, maxWidthMm: Number(e.target.value) })} | |
| /> | |
| <input | |
| type="number" | |
| className="w-1/2 p-1 border rounded text-right" | |
| value={current.maxHeightMm} | |
| onChange={(e) => setEditingOffset({ ...current, maxHeightMm: Number(e.target.value) })} | |
| /> | |
| </div> | |
| ) : ( | |
| <span className="font-medium text-slate-800">{current.maxWidthMm} × {current.maxHeightMm} mm</span> | |
| )} | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Giá kẽm CTP / lá:</span> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-28 p-1 border rounded text-right" | |
| value={current.platePrice} | |
| onChange={(e) => setEditingOffset({ ...current, platePrice: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-bold text-indigo-700">{current.platePrice.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Mở máy (≤{current.baseImpressions || 3000} lượt):</span> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-28 p-1 border rounded text-right" | |
| value={current.setupCost} | |
| onChange={(e) => setEditingOffset({ ...current, setupCost: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <div className="text-right"> | |
| <span className="font-bold text-slate-800">{current.setupCost.toLocaleString('vi-VN')}đ</span> | |
| {current.includesPlate && ( | |
| <span className="text-[10px] font-semibold text-emerald-600 block">Đã gồm kẽm</span> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Lũy tiến / 1.000 lượt:</span> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-28 p-1 border rounded text-right" | |
| value={current.stepCost} | |
| onChange={(e) => setEditingOffset({ ...current, stepCost: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-800">{current.stepCost.toLocaleString('vi-VN')}đ</span> | |
| )} | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Bù hao kẹp nhíp:</span> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-28 p-1 border rounded text-right" | |
| value={current.gripperMarginMm || 10} | |
| onChange={(e) => setEditingOffset({ ...current, gripperMarginMm: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-slate-800">{current.gripperMarginMm || 10} mm</span> | |
| )} | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-slate-500">Bù hao giấy in:</span> | |
| {isEditing ? ( | |
| <input | |
| type="number" | |
| className="w-28 p-1 border rounded text-right" | |
| value={current.defaultWasteSheets} | |
| onChange={(e) => setEditingOffset({ ...current, defaultWasteSheets: Number(e.target.value) })} | |
| /> | |
| ) : ( | |
| <span className="font-medium text-amber-700">{current.defaultWasteSheets} tờ</span> | |
| )} | |
| </div> | |
| </div> | |
| {isEditing && ( | |
| <div className="pt-2 border-t border-slate-200 flex justify-end gap-1.5"> | |
| <button | |
| onClick={() => setEditingOffset(null)} | |
| className="px-2 py-1 border rounded text-[11px] text-slate-600 hover:bg-slate-100" | |
| > | |
| Hủy | |
| </button> | |
| <button | |
| onClick={() => handleSaveOffset(current)} | |
| className="px-2.5 py-1 bg-indigo-600 text-white rounded text-[11px] font-semibold hover:bg-indigo-700" | |
| > | |
| Lưu | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |