mình có đoạn code bên dưới. giờ muốn chạy thành app cho Iphone (hoặc có thể chạy trên nền wed bằng iphone). Mình không rành về phần code (phần code ng ta làm giúp thấy chạy trên wed laptop thì ok) có cao thủ nào giúp với.
Thank All!
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Crew Manager v1.5 - New Calculation Logic</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&display=swap');
body { font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; }
.tab-content { display: none; }
.tab-content.active { display: block; animation: fadeIn 0.2s ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body class="bg-slate-50 min-h-screen pb-24">
<div class="max-w-[1200px] mx-auto p-3 md:p-6">
<!-- HEADER -->
<div class="flex flex-col md:flex-row justify-between items-center mb-4 gap-4 bg-white p-4 rounded-2xl shadow-sm border border-slate-200">
<h1 class="text-lg font-black text-blue-600 uppercase italic">Crew Manager v1.5</h1>
<div class="flex space-x-1 bg-slate-100 p-1 rounded-xl w-full md:w-auto">
<button onclick="switchTab('calc')" id="btn-tab-calc" class="tab-btn flex-1 px-4 py-2 rounded-lg font-bold bg-white text-blue-600 shadow-sm text-[10px] uppercase">Calculator</button>
<button onclick="switchTab('staff')" id="btn-tab-staff" class="tab-btn flex-1 px-4 py-2 rounded-lg font-bold text-slate-600 text-[10px] uppercase">Staff</button>
<button onclick="switchTab('history')" id="btn-tab-history" class="tab-btn flex-1 px-4 py-2 rounded-lg font-bold text-slate-600 text-[10px] uppercase">History</button>
</div>
</div>
<!-- TAB 1: CALCULATOR -->
<div id="tab-calc" class="tab-content active space-y-4">
<div class="bg-white rounded-2xl shadow-sm p-4 border border-slate-200 space-y-3">
<input type="hidden" id="editing-id">
<input type="text" id="job-name" placeholder="TÊN JOB/SHOW" class="w-full p-3 bg-slate-50 border border-slate-200 rounded-xl text-sm font-bold uppercase outline-none">
<div class="grid grid-cols-2 gap-3">
<input type="datetime-local" id="onset-time" onchange="updateLivePreview()" class="w-full p-3 border border-slate-200 rounded-xl text-xs font-bold outline-none">
<input type="datetime-local" id="wrap-time" onchange="updateLivePreview()" class="w-full p-3 border border-slate-200 rounded-xl text-xs font-bold outline-none">
</div>
<div id="time-display" class="text-center text-[10px] font-bold text-slate-400 uppercase"></div>
</div>
<div id="staff-selection-grid" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-2 p-2"></div>
<div id="result-card" class="hidden space-y-3">
<div id="salary-table-body" class="space-y-2"></div>
<div class="flex gap-2">
<button id="cancel-edit-btn" onclick="resetCalculator()" class="hidden flex-1 bg-slate-200 text-slate-600 font-black py-4 rounded-xl text-[11px] uppercase">Hủy</button>
<button id="save-job-btn" onclick="calculateAndSave()" class="flex-[2] bg-blue-600 text-white font-black py-4 rounded-xl text-[11px] uppercase shadow-md active:scale-95 transition-transform">Xác nhận & Lưu Show</button>
</div>
</div>
</div>
<!-- TAB 2: STAFF & TAB 3: HISTORY (Giữ nguyên cấu trúc v1.4) -->
<div id="tab-staff" class="tab-content space-y-4">
<div class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm">
<textarea id="excel-import-area" placeholder="Dán từ Excel: Tên [Tab] Lương" class="w-full h-20 p-3 bg-slate-50 border border-slate-200 rounded-xl text-[11px] outline-none mb-2"></textarea>
<button onclick="handleSmartImport()" class="w-full bg-blue-600 text-white py-3 rounded-xl text-[10px] font-black uppercase">Import Nhân Sự</button>
</div>
<div id="staff-preview" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"></div>
</div>
<div id="tab-history" class="tab-content space-y-4">
<div class="bg-white p-4 rounded-2xl border border-slate-200 space-y-4">
<div class="flex gap-2">
<input type="month" id="month-filter" onchange="renderHistory()" class="text-[10px] font-black border rounded-xl p-3 flex-1 bg-slate-50 outline-none">
<button onclick="exportExcelV1()" class="bg-emerald-600 text-white font-black px-4 rounded-xl text-[10px] uppercase shadow-md italic">Xuất Excel v1.5</button>
</div>
<div id="history-list" class="space-y-3"></div>
</div>
</div>
</div>
<script>
let staffList = JSON.parse(localStorage.getItem('staff_data_v1')) || [];
let historyList = JSON.parse(localStorage.getItem('history_data_v1')) || [];
let selectedStaffData = {};
function switchTab(tabId) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('bg-white', 'text-blue-600', 'shadow-sm'));
document.getElementById('tab-' + tabId).classList.add('active');
document.getElementById('btn-tab-' + tabId).classList.add('bg-white', 'text-blue-600', 'shadow-sm');
if(tabId === 'calc') renderSelectionGrid();
if(tabId === 'staff') renderStaffSettings();
if(tabId === 'history') renderHistory();
}
// CÔNG THỨC MỚI: <15h = 1 công | Mỗi +3h = +0.5 công
function calculateBaseF(onset, wrap) {
const diffHours = (new Date(wrap) - new Date(onset)) / 3600000;
if (diffHours <= 0) return 0;
if (diffHours <= 15) return 1.0;
// Tính số block 3h sau mốc 15h ban đầu
const extraHours = diffHours - 15;
const extraF = Math.ceil(extraHours / 3) * 0.5;
return 1.0 + extraF;
}
function updateLivePreview() {
const onset = document.getElementById('onset-time').value;
const wrap = document.getElementById('wrap-time').value;
if(!onset || !wrap) return;
const diffHours = (new Date(wrap) - new Date(onset)) / 3600000;
document.getElementById('time-display').innerText = `Tổng thời gian: ${diffHours.toFixed(1)} giờ`;
const baseF = calculateBaseF(onset, wrap);
const names = Object.keys(selectedStaffData);
if(names.length > 0) {
document.getElementById('result-card').classList.remove('hidden');
document.getElementById('salary-table-body').innerHTML = names.map(name => {
const s = staffList.find(x => x.name === name) || { baseSalary: 800000 };
// Nếu đang không ở chế độ sửa hoặc nhân sự mới chọn, dùng baseF.
// Nếu đang sửa và có f tùy chỉnh, dùng f đó.
const currentF = (selectedStaffData[name].f !== undefined) ? selectedStaffData[name].f : baseF;
selectedStaffData[name].f = currentF;
return `<div class="bg-white p-3 rounded-xl border border-slate-200 space-y-2">
<div class="flex justify-between items-center text-[10px] font-black uppercase">
<span class="text-blue-700">${name}</span>
<div class="flex items-center gap-2">
<button onclick="changeOffset('${name}', -0.25)" class="w-7 h-7 bg-slate-100 rounded-lg text-red-500 font-bold">-</button>
<span class="w-10 text-center font-bold text-slate-600">${currentF}</span>
<button onclick="changeOffset('${name}', 0.25)" class="w-7 h-7 bg-slate-100 rounded-lg text-emerald-600 font-bold">+</button>
<span class="ml-2 text-slate-900">${new Intl.NumberFormat().format(s.baseSalary * currentF)}đ</span>
</div>
</div>
<input type="text" placeholder="Ghi chú..."
oninput="selectedStaffData['${name}'].note = this.value"
value="${selectedStaffData[name].note || ''}"
class="w-full p-2 bg-slate-50 border border-dashed border-slate-200 rounded-lg text-[9px] outline-none font-medium">
</div>`;
}).join('');
} else document.getElementById('result-card').classList.add('hidden');
}
function changeOffset(name, val) {
selectedStaffData[name].f = (selectedStaffData[name].f || 1) + val;
updateLivePreview();
}
function calculateAndSave() {
const onset = document.getElementById('onset-time').value, wrap = document.getElementById('wrap-time').value;
const editId = document.getElementById('editing-id').value;
if(!onset || !wrap || Object.keys(selectedStaffData).length === 0) return;
const jobData = {
id: editId ? parseInt(editId) : Date.now(),
job: document.getElementById('job-name').value.toUpperCase() || "SHOW",
onset: onset.replace('T', ' '),
wrap: wrap.replace('T', ' '),
month: onset.slice(0, 7),
details: Object.keys(selectedStaffData).map(name => {
const s = staffList.find(x => x.name === name) || { baseSalary: 800000 };
return {
name,
f: selectedStaffData[name].f,
total: s.baseSalary * selectedStaffData[name].f,
note: selectedStaffData[name].note || ""
};
})
};
if(editId) {
const index = historyList.findIndex(h => h.id === parseInt(editId));
if(index !== -1) historyList[index] = jobData;
} else {
historyList.unshift(jobData);
}
localStorage.setItem('history_data_v1', JSON.stringify(historyList));
resetCalculator();
switchTab('history');
}
function editHistory(id) {
const item = historyList.find(h => h.id === id);
if(!item) return;
switchTab('calc');
document.getElementById('editing-id').value = item.id;
document.getElementById('job-name').value = item.job;
document.getElementById('onset-time').value = item.onset.replace(' ', 'T');
document.getElementById('wrap-time').value = item.wrap.replace(' ', 'T');
selectedStaffData = {};
item.details.forEach(d => { selectedStaffData[d.name] = { f: d.f, note: d.note }; });
document.getElementById('save-job-btn').innerText = "Lưu thay đổi (Ghi đè)";
document.getElementById('cancel-edit-btn').classList.remove('hidden');
renderSelectionGrid();
updateLivePreview();
}
function resetCalculator() {
document.getElementById('editing-id').value = "";
document.getElementById('job-name').value = "";
document.getElementById('save-job-btn').innerText = "Xác nhận & Lưu Show";
document.getElementById('cancel-edit-btn').classList.add('hidden');
document.getElementById('time-display').innerText = "";
selectedStaffData = {};
renderSelectionGrid();
updateLivePreview();
}
function renderHistory() {
const filter = document.getElementById('month-filter').value;
const filtered = historyList.filter(h => h.month === filter);
document.getElementById('history-list').innerHTML = filtered.map(h => `
<div class="bg-white p-4 rounded-xl border text-[10px] font-bold shadow-sm relative">
<div class="flex justify-between items-start border-b pb-1 mb-2">
<div class="text-blue-600 uppercase font-black">${h.job}</div>
<div class="flex gap-3">
<button onclick="editHistory(${h.id})" class="text-amber-600 uppercase text-[9px]">Sửa</button>
<button onclick="deleteHistory(${h.id})" class="text-red-400 uppercase text-[9px]">Xóa</button>
</div>
</div>
<div class="text-[8px] text-slate-400 mb-2">${h.onset} → ${h.wrap}</div>
${h.details.map(d => `
<div class="py-1 border-b border-slate-50 last:border-0 flex justify-between">
<span>${d.name} (x${d.f})</span>
<span>${new Intl.NumberFormat().format(d.total)}đ</span>
</div>
${d.note ? `<div class="text-[8px] text-red-500 italic mb-1">● ${d.note}</div>` : ''}
`).join('')}
</div>`).join('');
}
function exportExcelV1() {
const filter = document.getElementById('month-filter').value;
if(!filter) return alert("Chọn tháng");
const filtered = historyList.filter(h => h.month === filter);
if(filtered.length === 0) return alert("Không có dữ liệu");
const activeDays = [...new Set(filtered.map(j => j.onset.split(' ')[0]))].sort();
const staffReport = {};
filtered.forEach(job => {
job.details.forEach(p => {
if(!staffReport[p.name]) staffReport[p.name] = { days: {}, notes: [], totalF: 0, totalMoney: 0 };
const d = job.onset.split(' ')[0];
staffReport[p.name].days[d] = (staffReport[p.name].days[d] || 0) + p.f;
staffReport[p.name].totalF += p.f;
staffReport[p.name].totalMoney += p.total;
if(p.note) staffReport[p.name].notes.push(`${d.split('-')[2]}: ${p.note}`);
});
});
const [year, month] = filter.split('-');
const row1 = ["BẢNG TỔNG KẾT CÔNG THÁNG " + month + "/" + year];
const row2 = ["STT", "HỌ TÊN", ...activeDays.map(d => ""), "TỔNG CÔNG", "THÀNH TIỀN", "GHI CHÚ"];
const row3 = ["", "", ...activeDays.map(d => d.split('-')[2]), "", "", ""];
const finalData = [row1, row2, row3];
Object.keys(staffReport).forEach((name, i) => {
const row = [i + 1, name, ...activeDays.map(day => staffReport[name].days[day] || ""), staffReport[name].totalF, staffReport[name].totalMoney, staffReport[name].notes.join("; ")];
finalData.push(row);
});
const ws = XLSX.utils.aoa_to_sheet(finalData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "BangCong");
XLSX.writeFile(wb, `CrewManager_v1_5_${month}.xlsx`);
}
function toggleStaffSelection(name) {
if(selectedStaffData[name]) delete selectedStaffData[name];
else selectedStaffData[name] = { f: undefined, note: "" };
updateLivePreview(); renderSelectionGrid();
}
function renderSelectionGrid() {
document.getElementById('staff-selection-grid').innerHTML = staffList.map(s => `
<div onclick="toggleStaffSelection('${s.name}')" class="p-3 border rounded-xl text-center cursor-pointer transition-all ${selectedStaffData[s.name] ? 'bg-blue-600 text-white border-blue-600 shadow-md scale-95' : 'bg-white border-slate-200'}">
<div class="text-[9px] font-black uppercase truncate">${s.name}</div>
</div>`).join('');
}
function handleSmartImport() {
const raw = document.getElementById('excel-import-area').value.trim();
if (!raw) return;
raw.split('\n').forEach(line => {
const parts = line.split(/\t| {2,}/);
if (parts.length >= 1) {
const name = parts[0].trim().toUpperCase();
const salary = (parts.length >= 2) ? parseInt(parts[1].replace(/[^0-9]/g, '')) : 800000;
if (!staffList.find(s => s.name === name)) staffList.push({ name, baseSalary: salary || 800000 });
}
});
saveStaff(); document.getElementById('excel-import-area').value = '';
}
function renderStaffSettings() {
document.getElementById('staff-preview').innerHTML = staffList.map((s, i) => `
<div class="bg-white p-4 rounded-2xl border border-slate-200 flex flex-col gap-3">
<div class="flex justify-between items-start">
<div class="text-[11px] font-black uppercase text-slate-800">${s.name}</div>
<button onclick="deleteStaff(${i})" class="text-red-400 text-[10px]">✕</button>
</div>
<div class="flex items-center gap-2">
<button onclick="adjustBaseSalary(${i}, -50000)" class="px-2 py-2 bg-slate-100 rounded-lg text-[9px] font-black">-50K</button>
<input type="number" value="${s.baseSalary}" onchange="updateStaffSalary(${i}, this.value)" class="flex-1 bg-slate-50 p-2 text-xs font-black text-center outline-none rounded-lg">
<button onclick="adjustBaseSalary(${i}, 50000)" class="px-2 py-2 bg-slate-100 rounded-lg text-[9px] font-black">+50K</button>
</div>
</div>`).join('');
}
function adjustBaseSalary(i, amt) { staffList[i].baseSalary = Math.max(0, (staffList[i].baseSalary || 0) + amt); saveStaff(); }
function updateStaffSalary(i, val) { staffList[i].baseSalary = parseInt(val) || 0; saveStaff(); }
function deleteStaff(i) { if(confirm('Xóa?')) { staffList.splice(i, 1); saveStaff(); } }
function saveStaff() { localStorage.setItem('staff_data_v1', JSON.stringify(staffList)); renderStaffSettings(); }
function deleteHistory(id) { if(confirm('Xóa show?')) { historyList = historyList.filter(x => x.id !== id); localStorage.setItem('history_data_v1', JSON.stringify(historyList)); renderHistory(); } }
window.onload = () => {
const now = new Date();
document.getElementById('month-filter').value = now.toISOString().slice(0, 7);
document.getElementById('onset-time').value = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
renderSelectionGrid();
};
</script>
</body>
</html>
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?