Function bodies 503 total
ProtectedRoute function · typescript · L33-L37 (5 LOC)admin/src/App.tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const token = localStorage.getItem('admin_token');
if (!token) return <Navigate to="/login" replace />;
return <>{children}</>;
}RoleRoute function · typescript · L39-L43 (5 LOC)admin/src/App.tsx
function RoleRoute({ children, roles }: { children: React.ReactNode; roles: string[] }) {
const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
if (!roles.includes(user.role)) return <Navigate to="/" replace />;
return <>{children}</>;
}GuestRoute function · typescript · L45-L49 (5 LOC)admin/src/App.tsx
function GuestRoute({ children }: { children: React.ReactNode }) {
const token = localStorage.getItem('admin_token');
if (token) return <Navigate to="/" replace />;
return <>{children}</>;
}RoleIndex function · typescript · L51-L55 (5 LOC)admin/src/App.tsx
function RoleIndex() {
const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
if (user.role === 'super_admin') return <SubStatsPage />;
return <DashboardPage />;
}App function · typescript · L57-L105 (49 LOC)admin/src/App.tsx
export default function App() {
return (
<BrowserRouter>
<UIProvider>
<Routes>
<Route path="/login" element={<GuestRoute><LoginPage /></GuestRoute>} />
<Route path="/register" element={<GuestRoute><RegisterPage /></GuestRoute>} />
<Route path="/forgot-password" element={<GuestRoute><ForgotPasswordPage /></GuestRoute>} />
<Route path="/booking" element={<PublicBookingPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<Route index element={<RoleIndex />} />
{/* Super Admin */}
<Route path="pending-subs" element={<RoleRoute roles={['super_admin']}><PendingSubsPage /></RoleRoute>} />
<Route path="manage-companies" element={<RoleRoute roles={['super_admin']}><ManageCompaniesPage /></RoleRoute>} />
<Route path="plan-config" element={<RoleRoute roles={['super_admin']}><PlanCoAddressInput function · typescript · L20-L109 (90 LOC)admin/src/components/AddressInput.tsx
export default function AddressInput({ value, onChange, placeholder = 'Nhập địa chỉ...', iconColor = 'text-slate-300', brandColor }: Props) {
const [query, setQuery] = useState(value);
const [results, setResults] = useState<Place[]>([]);
const [show, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const wrapRef = useRef<HTMLDivElement>(null);
const bc = brandColor || '#0D9488';
// Sync external value
useEffect(() => { setQuery(value); }, [value]);
// Close dropdown on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setShow(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const handleSearch = (q: string) => {
setQuery(q);
if (timerRef.current) clearTimeout(timerRef.current);
playNotificationSound function · typescript · L80-L107 (28 LOC)admin/src/components/Layout.tsx
function playNotificationSound() {
try {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 880;
osc.type = 'sine';
gain.gain.value = 0.3;
osc.start();
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
osc.stop(ctx.currentTime + 0.5);
// Second beep
setTimeout(() => {
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.frequency.value = 1100;
osc2.type = 'sine';
gain2.gain.value = 0.3;
osc2.start();
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
osc2.stop(ctx.currentTime + 0.5);
}, 200);
} catch {}
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
timeAgo function · typescript · L109-L117 (9 LOC)admin/src/components/Layout.tsx
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'Vừa xong';
if (mins < 60) return `${mins}p trước`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h trước`;
return `${Math.floor(hours / 24)}d trước`;
}Layout function · typescript · L119-L452 (334 LOC)admin/src/components/Layout.tsx
export default function Layout() {
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
const [showNotifPanel, setShowNotifPanel] = useState(false);
const { toast: showToast } = useUI();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const socketRef = useRef<Socket | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
// Hiện số thông báo trên tab trình duyệt (giống Facebook)
useEffect(() => {
const base = 'XE 16 CHỖ';
document.title = unreadCount > 0 ? `(${unreadCount > 99 ? '99+' : unreadCount}) ${base}` : base;
// Cập nhật favicon badge (optional)
const link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (link) link.href = unreadCount > 0 ? '/logo-40-badge.png' : '/logo-40.png';
}, [unreadCount]);
const storedUser = JSON.parse(localStorage.getItem('admin_user') || '{}');
const isOwner = storedUser.role ==SeatMap function · typescript · L58-L163 (106 LOC)admin/src/components/SeatMap.tsx
export default function SeatMap({ seats, totalSeats, selectable, selectedSeats = [], onSeatClick, compact }: SeatMapProps) {
const getSeat = (num: number) => seats.find((s) => s.seatNumber === num);
const isSelected = (num: number) => selectedSeats.includes(num);
const fmt = (n: number) => n > 0 ? (n / 1000) + 'k' : '';
const sz = compact ? 'w-9 h-9' : 'w-12 h-12';
const gap = compact ? 'gap-1.5' : 'gap-2';
const rows = 5;
const cols = 5;
// Build grid
const grid: (number | 'driver' | null)[][] = Array.from({ length: rows }, () => Array(cols).fill(null));
grid[0][0] = 'driver'; // Vị trí tài xế
for (const item of SEAT_LAYOUT) {
if (item.seatNumber <= totalSeats) {
grid[item.row][item.col] = item.seatNumber;
}
}
return (
<div className="flex flex-col items-center">
<div className={`bg-slate-100 border border-slate-200 rounded-2xl p-3 ${compact ? 'p-2' : 'p-4'}`}>
<div className={`space-y-1.5`}>
{grid.map((row, ri) =>UIProvider function · typescript · L46-L135 (90 LOC)admin/src/components/Toast.tsx
export function UIProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const [confirmState, setConfirmState] = useState<{
options: ConfirmOptions;
resolve: (v: boolean) => void;
} | null>(null);
const idRef = useRef(0);
const toast = useCallback((message: string, type: ToastType = 'info') => {
const id = ++idRef.current;
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 4000);
}, []);
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
return new Promise((resolve) => {
setConfirmState({ options, resolve });
});
}, []);
const handleConfirm = (value: boolean) => {
confirmState?.resolve(value);
setConfirmState(null);
};
return (
<UIContext.Provider value={{ toast, confirm }}>
{children}
{/* Toasts */}
<div className="fixed top-4 right-4 z-[200] flBookingsPage function · typescript · L23-L128 (106 LOC)admin/src/pages/BookingsPage.tsx
export default function BookingsPage() {
const [bookings, setBookings] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState('');
const [loading, setLoading] = useState(true);
const limit = 20;
useEffect(() => {
setLoading(true);
api.get('/admin/bookings', { params: { page, limit, status: statusFilter || undefined } })
.then((res) => { setBookings(res.data.items); setTotal(res.data.total); })
.finally(() => setLoading(false));
}, [page, statusFilter]);
const formatPrice = (p: number) => p ? `${Number(p).toLocaleString('vi-VN')}đ` : '-';
const formatDate = (d: string) => d ? new Date(d).toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-';
const totalPages = Math.ceil(total / limit);
return (
<div className="space-y-6">
<div className="flex items-center justify-betweeCompaniesPage function · typescript · L13-L89 (77 LOC)admin/src/pages/CompaniesPage.tsx
export default function CompaniesPage() {
const [companies, setCompanies] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get('/companies/list')
.then((res) => {
const data = res.data;
setCompanies(Array.isArray(data) ? data : data.items || []);
})
.finally(() => setLoading(false));
}, []);
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center shadow-sm">
<Building2 size={18} className="text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-slate-800">Nhà xe</h2>
<p className="text-xs text-slate-400">{companies.length} nhà xe đăng ký</p>
</div>
</div>
{loading ? (
<div className="flex justify-center py-20"><div className="animate-spin rounded-full h-6 DashboardPage function · typescript · L15-L358 (344 LOC)admin/src/pages/DashboardPage.tsx
export default function DashboardPage() {
const [data, setData] = useState<DashboardData | null>(null);
const [revenue, setRevenue] = useState<any>(null);
const [vehicleStats, setVehicleStats] = useState<any[]>([]);
const [subStats, setSubStats] = useState<any>(null);
const storedUser = JSON.parse(localStorage.getItem('admin_user') || '{}');
const isOwner = storedUser.role === 'owner';
const isSuperAdmin = storedUser.role === 'super_admin';
const fetchData = useCallback(async () => {
try {
if (isOwner) {
const [driversRes, vehiclesRes, bookingsRes, customersRes, vStatsRes] = await Promise.all([
api.get('/companies/me/drivers').catch(() => ({ data: [] })),
api.get('/vehicles/my').catch(() => ({ data: [] })),
api.get('/companies/me/bookings', { params: { limit: 999 } }).catch(() => ({ data: { items: [] } })),
api.get('/companies/me/customers').catch(() => ({ data: [] })),
api.get('/companies/me/vehicles/ForgotPasswordPage function · typescript · L7-L197 (191 LOC)admin/src/pages/ForgotPasswordPage.tsx
export default function ForgotPasswordPage() {
const { toast } = useUI();
const navigate = useNavigate();
const [step, setStep] = useState<'email' | 'otp' | 'reset'>('email');
const [email, setEmail] = useState('');
const [otpCode, setOtpCode] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [otpCountdown, setOtpCountdown] = useState(0);
useEffect(() => {
if (otpCountdown <= 0) return;
const t = setTimeout(() => setOtpCountdown(otpCountdown - 1), 1000);
return () => clearTimeout(t);
}, [otpCountdown]);
// Step 1: gửi OTP về email
const handleSendOtp = async (e: FormEvent) => {
e.preventDefault();
setError('');
if (!email) { setError('Vui lòng nhập email'); return; }
setLoading(true);
try {
await apGenerated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
LoginPage function · typescript · L14-L271 (258 LOC)admin/src/pages/LoginPage.tsx
export default function LoginPage() {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// OTP state
const [step, setStep] = useState<'credentials' | 'otp'>('credentials');
const [maskedEmail, setMaskedEmail] = useState('');
const [otpCode, setOtpCode] = useState('');
const [otpCountdown, setOtpCountdown] = useState(0);
useEffect(() => {
if (otpCountdown <= 0) return;
const t = setTimeout(() => setOtpCountdown(otpCountdown - 1), 1000);
return () => clearTimeout(t);
}, [otpCountdown]);
// Helper: get reCAPTCHA token (silent fail)
const getCaptcha = async (): Promise<string> => {
try {
if (window.grecaptcha) return await window.grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'login' });
} catch {}
return '';
};
MyBookingsPage function · typescript · L25-L318 (294 LOC)admin/src/pages/MyBookingsPage.tsx
export default function MyBookingsPage() {
const { toast, confirm } = useUI();
const storedUser = JSON.parse(localStorage.getItem('admin_user') || '{}');
const [bookings, setBookings] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState('');
const [loading, setLoading] = useState(true);
const [assignModal, setAssignModal] = useState<any>(null);
const [availableVehicles, setAvailableVehicles] = useState<any[]>([]);
const [assigningId, setAssigningId] = useState<number | null>(null);
const limit = 20;
const fetchBookings = () => {
setLoading(true);
api.get('/companies/me/bookings', { params: { page, limit, status: statusFilter || undefined } })
.then((res) => { setBookings(res.data.items || []); setTotal(res.data.total || 0); })
.catch(() => { setBookings([]); setTotal(0); })
.finally(() => setLoading(false));
};
useEffect(() => { fetchBooMyCompanyPage function · typescript · L6-L216 (211 LOC)admin/src/pages/MyCompanyPage.tsx
export default function MyCompanyPage() {
const { toast } = useUI();
const [company, setCompany] = useState<any>(null);
const [subscription, setSubscription] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ name: '', phone: '', email: '', address: '', slogan: '', description: '', primaryColor: '#0D9488' });
const [isNew, setIsNew] = useState(false);
const logoRef = useRef<HTMLInputElement>(null);
const bannerRef = useRef<HTMLInputElement>(null);
useEffect(() => {
Promise.all([
api.get('/companies/me').catch(() => ({ data: null })),
api.get('/companies/me/subscription').catch(() => ({ data: null })),
]).then(([compRes, subRes]) => {
if (compRes.data) {
setCompany(compRes.data);
setForm({ name: compRes.data.name, phone: compRes.data.phone || '', email: compRes.data.email || '', address: compRes.data.address || '', slogan: compRMyCustomersPage function · typescript · L44-L346 (303 LOC)admin/src/pages/MyCustomersPage.tsx
export default function MyCustomersPage() {
const { toast } = useUI();
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [showPw, setShowPw] = useState(false);
const [form, setForm] = useState({ fullName: '', phone: '', email: '', password: '123456' });
const [error, setError] = useState('');
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
const [bookings, setBookings] = useState<Booking[]>([]);
const [bookingsLoading, setBookingsLoading] = useState(false);
const fetchCustomers = () => {
api.get('/companies/me/customers')
.then((res) => setCustomers(res.data || []))
.finally(() => setLoading(false));
};
useEffect(() => { fetchCustomers(); }, []);
const openCustomerDetail = async (customer: Customer) => {
setSelectedCustomer(customer);
setBookiMyDriversPage function · typescript · L20-L280 (261 LOC)admin/src/pages/MyDriversPage.tsx
export default function MyDriversPage() {
const { toast, confirm } = useUI();
const [drivers, setDrivers] = useState<Driver[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [showPw, setShowPw] = useState(false);
const [form, setForm] = useState({ fullName: '', phone: '', password: '', email: '' });
const [error, setError] = useState('');
const [editDriver, setEditDriver] = useState<Driver | null>(null);
const [editForm, setEditForm] = useState({ fullName: '', phone: '', email: '' });
const [resetDriver, setResetDriver] = useState<Driver | null>(null);
const [newPassword, setNewPassword] = useState('123456');
const fetchDrivers = async () => {
try {
const res = await api.get('/companies/me/drivers');
setDrivers(res.data || []);
} catch { setDrivers([]); }
finally { setLoading(false); }
};
useEffect(() => { fetchDrivers(); }, [])MyPaymentsPage function · typescript · L39-L193 (155 LOC)admin/src/pages/MyPaymentsPage.tsx
export default function MyPaymentsPage() {
const [payments, setPayments] = useState<Payment[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [total, setTotal] = useState(0);
const [totalRevenue, setTotalRevenue] = useState(0);
const [statusFilter, setStatusFilter] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
const params = new URLSearchParams({ page: String(page), limit: '20' });
if (statusFilter) params.set('status', statusFilter);
api.get(`/companies/me/payments?${params}`)
.then((res) => {
const d = res.data;
setPayments(d.items || []);
setTotalPages(d.totalPages || 0);
setTotal(d.total || 0);
setTotalRevenue(d.totalRevenue || 0);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [page, statusFilter]);
const fmt = (n: number | string) => Number(n).toLocaleString('vi-VN');
MyQRCodesPage function · typescript · L16-L281 (266 LOC)admin/src/pages/MyQRCodesPage.tsx
export default function MyQRCodesPage() {
const { confirm } = useUI();
const [qrList, setQrList] = useState<QRItem[]>([]);
const [showForm, setShowForm] = useState(false);
const [label, setLabel] = useState('');
const [copied, setCopied] = useState('');
const [company, setCompany] = useState<any>(null);
const printRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Load saved QRs from localStorage
const saved = localStorage.getItem('qr_codes');
if (saved) setQrList(JSON.parse(saved));
api.get('/companies/me').then((res) => setCompany(res.data));
}, []);
const saveQrList = (list: QRItem[]) => {
setQrList(list);
localStorage.setItem('qr_codes', JSON.stringify(list));
};
const baseBookingUrl = window.location.origin + '/booking';
const handleCreate = () => {
if (!label.trim()) return;
const companyId = company?.id || '';
const companyName = company?.name || '';
// URL trỏ tới trang booking mobile với company pre-fMyRatingsPage function · typescript · L15-L142 (128 LOC)admin/src/pages/MyRatingsPage.tsx
export default function MyRatingsPage() {
const [ratings, setRatings] = useState<Rating[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [total, setTotal] = useState(0);
const [average, setAverage] = useState(0);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
api.get(`/companies/me/ratings?page=${page}&limit=20`)
.then((res) => {
const d = res.data;
setRatings(d.items || []);
setTotalPages(d.totalPages || 0);
setTotal(d.total || 0);
setAverage(d.average || 0);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [page]);
const renderStars = (count: number) =>
Array.from({ length: 5 }, (_, i) => (
<Star key={i} size={14} className={i < count ? 'text-amber-400 fill-amber-400' : 'text-slate-200'} />
));
return (
<div>
<h1 className="text-xl font-bold text-slate-800 mb-6">Đánh giá từAll rows above produced by Repobility · https://repobility.com
MyReportsPage function · typescript · L20-L274 (255 LOC)admin/src/pages/MyReportsPage.tsx
export default function MyReportsPage() {
const now = new Date();
const [startDate, setStartDate] = useState(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`);
const [endDate, setEndDate] = useState(now.toISOString().split('T')[0]);
const [data, setData] = useState<ReportData | null>(null);
const [loading, setLoading] = useState(true);
const [tab, setTab] = useState<'overview' | 'vehicle' | 'driver' | 'occupancy'>('overview');
const fetchReport = () => {
setLoading(true);
api.get(`/companies/me/reports?startDate=${startDate}&endDate=${endDate}`)
.then((r) => setData(r.data))
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => { fetchReport(); }, [startDate, endDate]);
const fmt = (n: number) => n.toLocaleString('vi-VN');
if (loading || !data) return <div className="py-20 text-center text-slate-400">Đang tải báo cáo...</div>;
const { summary, daily, byVehicle, byDriver, byShift, occupancy }MySubscriptionPage function · typescript · L13-L394 (382 LOC)admin/src/pages/MySubscriptionPage.tsx
export default function MySubscriptionPage() {
const { toast, confirm } = useUI();
const [company, setCompany] = useState<any>(null);
const [subscription, setSubscription] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [subscribing, setSubscribing] = useState('');
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [vehicleCounts, setVehicleCounts] = useState<Record<string, number>>({ basic: 5, pro: 10, enterprise: 20 });
const [planConfig, setPlanConfig] = useState<Record<string, { maxVehicles: number; price: number }>>({
free: { maxVehicles: 3, price: 0 },
basic: { maxVehicles: 10, price: 99000 },
pro: { maxVehicles: 30, price: 79000 },
enterprise: { maxVehicles: 9999, price: 59000 },
});
const [bankInfo, setBankInfo] = useState({ bank: 'MB Bank', bin: '970422', accountNumber: '20120283869999', accountName: 'DO THANH DAT', branch: 'Chi nhánh TP.HN' });
const [paymentModal, setPaymenMyVehiclesPage function · typescript · L17-L300 (284 LOC)admin/src/pages/MyVehiclesPage.tsx
export default function MyVehiclesPage() {
const { toast } = useUI();
const [vehicles, setVehicles] = useState<any[]>([]);
const [drivers, setDrivers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ licensePlate: '', brand: '', model: '', seats: '16', color: '', pricePerKm: '15000' });
const [error, setError] = useState('');
const [editVehicle, setEditVehicle] = useState<any>(null);
const [editForm, setEditForm] = useState({ licensePlate: '', brand: '', model: '', seats: '16', color: '', pricePerKm: '' });
const fetchData = async () => {
try {
const [vRes, dRes] = await Promise.all([
api.get('/vehicles/my'),
api.get('/companies/me/drivers'),
]);
setVehicles(vRes.data || []);
setDrivers(dRes.data || []);
} catch { }
finally { setLoading(false); }
};
useEffect((RegisterPage function · typescript · L14-L278 (265 LOC)admin/src/pages/RegisterPage.tsx
export default function RegisterPage() {
const navigate = useNavigate();
const [form, setForm] = useState({ fullName: '', phone: '', email: '', password: '', confirmPassword: '' });
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// OTP state
const [step, setStep] = useState<'form' | 'otp'>('form');
const [otpCode, setOtpCode] = useState('');
const [sendingOtp, setSendingOtp] = useState(false);
const [otpCountdown, setOtpCountdown] = useState(0);
// Countdown timer
useEffect(() => {
if (otpCountdown <= 0) return;
const t = setTimeout(() => setOtpCountdown(otpCountdown - 1), 1000);
return () => clearTimeout(t);
}, [otpCountdown]);
// Step 1: validate form + send OTP
const handleSendOtp = async (e: FormEvent) => {
e.preventDefault();
setError('');
if (!form.fullName || !form.phone || !form.email || !form.password) {
setError('Vui ActivityLogsPage function · typescript · L5-L63 (59 LOC)admin/src/pages/super/ActivityLogsPage.tsx
export default function ActivityLogsPage() {
const [logs, setLogs] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const fetch = (p = 1) => {
setLoading(true);
api.get('/admin/activity-logs', { params: { page: p, limit: 20 } })
.then((r) => { setLogs(r.data.items || []); setTotal(r.data.total || 0); setPage(p); })
.catch(() => {}).finally(() => setLoading(false));
};
useEffect(() => { fetch(); }, []);
const totalPages = Math.ceil(total / 20);
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center shadow-sm"><Activity size={18} className="text-white" /></div>
<div>
<h2 className="text-xl font-bold text-slate-800">Nhật ký hoạt động</h2>
<p className="text-xs text-slate-BankConfigPage function · typescript · L6-L61 (56 LOC)admin/src/pages/super/BankConfigPage.tsx
export default function BankConfigPage() {
const { toast } = useUI();
const [saving, setSaving] = useState(false);
const [bank, setBank] = useState({
bank_name: 'MB Bank', bank_bin: '970422', bank_account: '20120283869999',
bank_holder: 'DO THANH DAT', bank_branch: 'Chi nhánh TP.HN',
});
useEffect(() => {
api.get('/admin/settings').then((r) => {
const s = r.data || {};
if (s.bank_name) setBank(b => ({ ...b, ...Object.fromEntries(Object.keys(b).filter(k => s[k]).map(k => [k, s[k]])) }));
}).catch(() => {});
}, []);
const handleSave = async () => {
setSaving(true);
try { await api.post('/admin/settings', bank); toast('Lưu thành công', 'success'); }
catch { toast('Lỗi lưu', 'error'); }
finally { setSaving(false); }
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-centerBroadcastPage function · typescript · L6-L60 (55 LOC)admin/src/pages/super/BroadcastPage.tsx
export default function BroadcastPage() {
const { toast, confirm } = useUI();
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ title: '', message: '', target: 'owner' });
const handleSend = async () => {
if (!form.title || !form.message) { toast('Nhập tiêu đề và nội dung', 'warning'); return; }
const targetLabel = form.target === 'owner' ? 'chủ xe' : form.target === 'driver' ? 'tài xế' : 'khách hàng';
if (!await confirm({ title: 'Gửi thông báo', message: `Gửi đến tất cả ${targetLabel}?`, confirmText: 'Gửi' })) return;
setSaving(true);
try {
const res = await api.post('/admin/broadcast', form);
toast(`Đã gửi đến ${res.data.sent} người`, 'success');
setForm({ title: '', message: '', target: form.target });
} catch { toast('Lỗi gửi', 'error'); }
finally { setSaving(false); }
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 ManageCompaniesPage function · typescript · L9-L78 (70 LOC)admin/src/pages/super/ManageCompaniesPage.tsx
export default function ManageCompaniesPage() {
const { toast, confirm } = useUI();
const [companies, setCompanies] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const fetch = () => {
api.get('/companies/list').then((r) => setCompanies(Array.isArray(r.data) ? r.data : r.data?.items || [])).catch(() => setCompanies([])).finally(() => setLoading(false));
};
useEffect(() => { fetch(); }, []);
const handleToggle = async (c: any) => {
if (!await confirm({ title: c.isActive ? 'Khoá nhà xe' : 'Mở khoá nhà xe', message: `${c.isActive ? 'Khoá' : 'Mở khoá'} "${c.name}"?`, confirmText: c.isActive ? 'Khoá' : 'Mở khoá', danger: c.isActive })) return;
try { await api.patch(`/admin/companies/${c.id}/toggle-active`); toast(`Đã ${c.isActive ? 'khoá' : 'mở khoá'}`, 'success'); fetch(); } catch { toast('Lỗi', 'error'); }
};
if (loading) return <div className="flex justify-center py-20"><div className="animate-spin rounded-full h-6 w-6 border-2 bordeRepobility · code-quality intelligence · https://repobility.com
PendingSubsPage function · typescript · L8-L64 (57 LOC)admin/src/pages/super/PendingSubsPage.tsx
export default function PendingSubsPage() {
const { toast, confirm } = useUI();
const [subs, setSubs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const fetch = () => {
api.get('/admin/pending-subscriptions').then((r) => setSubs(r.data || [])).catch(() => setSubs([])).finally(() => setLoading(false));
};
useEffect(() => { fetch(); }, []);
const handleApprove = async (id: number) => {
if (!await confirm({ title: 'Duyệt thanh toán', message: 'Xác nhận đã nhận thanh toán và kích hoạt gói?', confirmText: 'Duyệt' })) return;
try { await api.patch(`/admin/subscriptions/${id}/approve`); toast('Đã duyệt', 'success'); fetch(); } catch { toast('Lỗi', 'error'); }
};
const handleReject = async (id: number) => {
if (!await confirm({ title: 'Từ chối', message: 'Từ chối yêu cầu này?', confirmText: 'Từ chối', danger: true })) return;
try { await api.patch(`/admin/subscriptions/${id}/reject`); toast('Đã từ chối', 'success'); fetch(); } caPlanConfigPage function · typescript · L6-L69 (64 LOC)admin/src/pages/super/PlanConfigPage.tsx
export default function PlanConfigPage() {
const { toast } = useUI();
const [saving, setSaving] = useState(false);
const [plans, setPlans] = useState({
free_max: '3', free_price: '0',
basic_max: '10', basic_price: '99000',
pro_max: '30', pro_price: '79000',
enterprise_max: '9999', enterprise_price: '59000',
});
useEffect(() => {
api.get('/admin/settings').then((r) => {
const s = r.data || {};
if (s.free_max) setPlans(p => ({ ...p, ...Object.fromEntries(Object.keys(p).filter(k => s[k]).map(k => [k, s[k]])) }));
}).catch(() => {});
}, []);
const handleSave = async () => {
setSaving(true);
try { await api.post('/admin/settings', plans); toast('Lưu cấu hình gói thành công', 'success'); }
catch { toast('Lỗi lưu', 'error'); }
finally { setSaving(false); }
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-teal-500 to-teal-6SubStatsPage function · typescript · L9-L96 (88 LOC)admin/src/pages/super/SubStatsPage.tsx
export default function SubStatsPage() {
const [subStats, setSubStats] = useState<any>(null);
const [systemStats, setSystemStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
api.get('/admin/subscription-stats').catch(() => ({ data: null })),
api.get('/admin/system-stats').catch(() => ({ data: null })),
]).then(([subRes, sysRes]) => {
setSubStats(subRes.data);
setSystemStats(sysRes.data);
}).finally(() => setLoading(false));
}, []);
if (loading) return <div className="flex justify-center py-20"><div className="animate-spin rounded-full h-6 w-6 border-2 border-teal-200 border-t-teal-600" /></div>;
const pieData = subStats?.byPlan ? Object.entries(subStats.byPlan).map(([key, val]: any) => ({ name: PLAN_LABELS[key] || key, value: val.count, color: PLAN_COLORS[key] || '#94a3b8' })) : [];
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
parseLine function · typescript · L18-L24 (7 LOC)admin/src/pages/super/SystemLogsPage.tsx
function parseLine(line: string) {
const match = line.match(/^\[(.+?)\]\s+(ERROR|WARN|INFO|DEBUG|VERBOSE)\s+\[(.+?)\]\s+(.+)$/);
if (match) {
return { time: match[1], level: match[2], context: match[3], message: match[4] };
}
return { time: '', level: '', context: '', message: line };
}SystemLogsPage function · typescript · L26-L176 (151 LOC)admin/src/pages/super/SystemLogsPage.tsx
export default function SystemLogsPage() {
const [files, setFiles] = useState<LogFile[]>([]);
const [lines, setLines] = useState<string[]>([]);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [logType, setLogType] = useState<'all' | 'error'>('all');
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
const fetchFiles = async () => {
try {
const res = await api.get('/admin/logs/files');
setFiles(res.data || []);
} catch { setFiles([]); }
};
const fetchLog = async (date?: string, type?: string) => {
setLoading(true);
try {
const res = await api.get('/admin/logs/read', { params: { date: date || selectedDate, type: type || logType, lines: 500 } });
setLines(res.data || []);
} catch { setLines([]); }
finally { setLoading(false); }
};
useEffect(() => { fetchFiles(); fetchLog(); }, []);
const handleDateChange = (date: string) => {
UsersPage function · typescript · L20-L196 (177 LOC)admin/src/pages/UsersPage.tsx
export default function UsersPage() {
const [users, setUsers] = useState<UserItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [roleFilter, setRoleFilter] = useState('');
const [loading, setLoading] = useState(true);
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
const limit = 100; // Load more to group properly
const fetchUsers = async () => {
setLoading(true);
const res = await api.get('/admin/users', { params: { page, limit, role: roleFilter || undefined } });
setUsers(res.data.items);
setTotal(res.data.total);
setLoading(false);
};
useEffect(() => { fetchUsers(); }, [page, roleFilter]);
const toggleActive = async (id: number) => {
await api.patch(`/admin/users/${id}/toggle-active`);
setUsers((prev) => prev.map((u) => u.id === id ? { ...u, isActive: !u.isActive } : u));
};
const toggleGroup = (key: string) => {
setCollapsedGroups((prev) => VehiclesPage function · typescript · L12-L77 (66 LOC)admin/src/pages/VehiclesPage.tsx
export default function VehiclesPage() {
const [vehicles, setVehicles] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get('/vehicles/available')
.then((res) => setVehicles(res.data))
.finally(() => setLoading(false));
}, []);
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl flex items-center justify-center shadow-sm">
<Bus size={18} className="text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-slate-800">Phương tiện</h2>
<p className="text-xs text-slate-400">{vehicles.length} xe</p>
</div>
</div>
{loading ? (
<div className="flex justify-center py-20"><div className="animate-spin rounded-full h-6 w-6 border-2 border-teal-200 border-t-teal-600" /></div>
) : vehicles.length === 0 ? (
<exportExcel function · typescript · L5-L10 (6 LOC)admin/src/utils/export.ts
export function exportExcel(data: Record<string, any>[], filename: string, sheetName = 'Sheet1') {
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, sheetName);
XLSX.writeFile(wb, `${filename}.xlsx`);
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
exportBookingsExcel function · typescript · L12-L28 (17 LOC)admin/src/utils/export.ts
export function exportBookingsExcel(bookings: any[], filename = 'bao-cao-chuyen-xe') {
const data = bookings.map((b, i) => ({
'STT': i + 1,
'Mã chuyến': b.bookingCode,
'Khách hàng': b.customer?.fullName || '-',
'SĐT': b.customer?.phone || '-',
'Điểm đón': b.pickupAddress,
'Điểm đến': b.dropoffAddress,
'Xe': b.vehicle?.licensePlate || 'Chưa có',
'Tài xế': b.vehicle?.driver?.fullName || '-',
'Giá (VND)': b.finalPrice || b.estimatedPrice || 0,
'Trạng thái': b.status,
'Thanh toán': b.isPaid ? 'Đã TT' : 'Chưa TT',
'Ngày tạo': b.createdAt ? new Date(b.createdAt).toLocaleString('vi-VN') : '',
}));
exportExcel(data, filename, 'Chuyến xe');
}htmlToPdf function · typescript · L31-L57 (27 LOC)admin/src/utils/export.ts
async function htmlToPdf(htmlContent: string, filename: string) {
const container = document.createElement('div');
container.innerHTML = htmlContent;
container.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;background:#fff;padding:40px;font-family:system-ui,-apple-system,sans-serif;';
document.body.appendChild(container);
try {
const canvas = await html2canvas(container, { scale: 2, useCORS: true });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const imgW = pageW;
const imgH = (canvas.height * imgW) / canvas.width;
let y = 0;
while (y < imgH) {
if (y > 0) pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, -y, imgW, imgH);
y += pageH;
}
pdf.save(filename);
} finally {
document.body.removeChild(container);
}
}exportRevenuePDF function · typescript · L59-L118 (60 LOC)admin/src/utils/export.ts
export function exportRevenuePDF(stats: {
companyName: string;
period: string;
totalBookings: number;
completedBookings: number;
revenue: number;
daily?: { date: string; amount: number }[];
}) {
const dailyRows = (stats.daily || []).map(d => `
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#475569">${d.date}</td>
<td style="padding:8px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#0f172a;text-align:right;font-weight:600">${d.amount.toLocaleString('vi-VN')}đ</td>
</tr>
`).join('');
const html = `
<div style="font-family:system-ui,-apple-system,sans-serif">
<div style="text-align:center;margin-bottom:30px">
<h1 style="font-size:24px;color:#0d9488;margin:0">BÁO CÁO DOANH THU</h1>
<p style="font-size:14px;color:#64748b;margin:6px 0 0">${stats.companyName}</p>
<p style="font-size:13px;color:#94a3b8;margin:4px 0 0">${stats.period}</p>
</div>
<div style="diexportInvoicePDF function · typescript · L120-L175 (56 LOC)admin/src/utils/export.ts
export function exportInvoicePDF(booking: any, companyName: string) {
const price = Number(booking.finalPrice || booking.estimatedPrice || 0);
const statusLabel: Record<string, string> = {
pending: 'Chờ xác nhận', confirmed: 'Đã xác nhận', picking_up: 'Đang đón',
in_progress: 'Đang chạy', completed: 'Hoàn thành', cancelled: 'Đã huỷ',
};
const infoRows = [
['Khách hàng', booking.customer?.fullName || '-'],
['Số điện thoại', booking.customer?.phone || '-'],
['Điểm đón', booking.pickupAddress || '-'],
['Điểm đến', booking.dropoffAddress || '-'],
['Thời gian đón', booking.pickupTime ? new Date(booking.pickupTime).toLocaleString('vi-VN') : '-'],
['Số khách', String(booking.passengerCount || '-')],
['Khoảng cách', booking.distanceKm ? `${booking.distanceKm} km` : '-'],
['Xe', booking.vehicle ? `${booking.vehicle.licensePlate} - ${booking.vehicle.brand} ${booking.vehicle.model}` : '-'],
['Tài xế', booking.vehicle?.driver?.fullName || '-'],
AppController class · typescript · L6-L14 (9 LOC)backend/src/app.controller.ts
export class AppController {
constructor(private readonly appService: AppService) {}
@Public()
@Get()
getHello(): string {
return this.appService.getHello();
}
}getHello method · typescript · L11-L13 (3 LOC)backend/src/app.controller.ts
getHello(): string {
return this.appService.getHello();
}AppService class · typescript · L4-L8 (5 LOC)backend/src/app.service.ts
export class AppService {
getHello(): string {
return 'Hello World!';
}
}getHello method · typescript · L5-L7 (3 LOC)backend/src/app.service.ts
getHello(): string {
return 'Hello World!';
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
RedisIoAdapter class · typescript · L8-L35 (28 LOC)backend/src/common/adapters/redis-io.adapter.ts
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter> | null = null;
constructor(
app: INestApplication,
private readonly configService: ConfigService,
) {
super(app);
}
async connectToRedis(): Promise<void> {
const host = this.configService.get('REDIS_HOST', 'localhost');
const port = this.configService.get('REDIS_PORT', 6379);
const pubClient = new Redis({ host, port });
const subClient = pubClient.duplicate();
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options?: ServerOptions) {
const server = super.createIOServer(port, options);
if (this.adapterConstructor) {
server.adapter(this.adapterConstructor);
}
return server;
}
}constructor method · typescript · L11-L16 (6 LOC)backend/src/common/adapters/redis-io.adapter.ts
constructor(
app: INestApplication,
private readonly configService: ConfigService,
) {
super(app);
}connectToRedis method · typescript · L18-L26 (9 LOC)backend/src/common/adapters/redis-io.adapter.ts
async connectToRedis(): Promise<void> {
const host = this.configService.get('REDIS_HOST', 'localhost');
const port = this.configService.get('REDIS_PORT', 6379);
const pubClient = new Redis({ host, port });
const subClient = pubClient.duplicate();
this.adapterConstructor = createAdapter(pubClient, subClient);
}page 1 / 11next ›