← back to dothanhdat2012202__xe16cho

Function bodies 503 total

All specs Real LLM only Function bodies
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']}><PlanCo
AddressInput 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] fl
BookingsPage 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-betwee
CompaniesPage 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 ap
Generated 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(() => { fetchBoo
MyCompanyPage 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: compR
MyCustomersPage 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);
    setBooki
MyDriversPage 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-f
MyRatingsPage 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, setPaymen
MyVehiclesPage 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-center
BroadcastPage 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 borde
Repobility · 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(); } ca
PlanConfigPage 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-6
SubStatsPage 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="di
exportInvoicePDF 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 ›