Add staging environment documentation and minor fixes
- Document staging vs production DB separation (autonet_staging) - Add staging sync and deployment commands to CLAUDE.md - Update changelog with 2025-01-03 changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { vehicleRequestsApi, VehicleRequest, VehicleRequestWithVehicles } from '@/lib/api';
|
||||
import { vehicleRequestsApi, VehicleRequest, VehicleRequestWithVehicles, adminPdfApi } from '@/lib/api';
|
||||
|
||||
export default function AdminVehicleRequestsPage() {
|
||||
const [requests, setRequests] = useState<VehicleRequest[]>([]);
|
||||
@@ -10,6 +10,7 @@ export default function AdminVehicleRequestsPage() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [deletingVehicleId, setDeletingVehicleId] = useState<number | null>(null);
|
||||
const [regeneratingPdfId, setRegeneratingPdfId] = useState<number | null>(null);
|
||||
|
||||
// Load requests
|
||||
useEffect(() => {
|
||||
@@ -89,6 +90,23 @@ export default function AdminVehicleRequestsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Regenerate PDF for a car
|
||||
const regeneratePdf = async (carId: number) => {
|
||||
try {
|
||||
setRegeneratingPdfId(carId);
|
||||
await adminPdfApi.regenerateSingle(carId);
|
||||
// Reload request detail to refresh PDF status
|
||||
if (selectedRequest) {
|
||||
await loadRequestDetail(selectedRequest.request.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate PDF:', error);
|
||||
alert('Failed to regenerate PDF. Please try again.');
|
||||
} finally {
|
||||
setRegeneratingPdfId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('ko-KR', {
|
||||
@@ -304,42 +322,89 @@ export default function AdminVehicleRequestsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{selectedRequest.approved_vehicles.map((vehicle) => (
|
||||
<div key={vehicle.id} className="bg-gray-50 rounded-lg p-3 flex items-center gap-3">
|
||||
{vehicle.car_data.main_image && (
|
||||
<img
|
||||
src={vehicle.car_data.main_image}
|
||||
alt=""
|
||||
className="w-16 h-12 object-cover rounded"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm truncate">{vehicle.car_data.car_name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km
|
||||
</p>
|
||||
{selectedRequest.approved_vehicles.map((vehicle) => {
|
||||
const hasPdf = vehicle.car_data.has_pdf === true;
|
||||
const isSoldout = vehicle.car_data.soldout === true;
|
||||
const checkNum = vehicle.car_data.check_num;
|
||||
|
||||
return (
|
||||
<div key={vehicle.id} className={`bg-gray-50 rounded-lg p-3 flex items-center gap-3 ${isSoldout ? 'opacity-60' : ''}`}>
|
||||
{vehicle.car_data.main_image && (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={vehicle.car_data.main_image}
|
||||
alt=""
|
||||
className={`w-16 h-12 object-cover rounded ${isSoldout ? 'grayscale' : ''}`}
|
||||
/>
|
||||
{isSoldout && (
|
||||
<span className="absolute top-0 left-0 bg-red-500 text-white text-[8px] px-1 rounded">SOLD</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm truncate">{vehicle.car_data.car_name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km
|
||||
</p>
|
||||
{/* PDF Status */}
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{hasPdf ? (
|
||||
<span className="text-xs text-green-600 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
PDF OK
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-red-500 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
No PDF
|
||||
{vehicle.car_id && checkNum && (
|
||||
<button
|
||||
onClick={() => regeneratePdf(vehicle.car_id!)}
|
||||
disabled={regeneratingPdfId === vehicle.car_id}
|
||||
className="ml-1 text-blue-500 hover:text-blue-700 underline disabled:opacity-50"
|
||||
title="Retry PDF generation"
|
||||
>
|
||||
{regeneratingPdfId === vehicle.car_id ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-500"></div>
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
'Retry'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!checkNum && <span className="text-gray-400">(no check_num)</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex items-center gap-2">
|
||||
<p className="text-sm font-medium text-primary-600">
|
||||
{vehicle.car_data.final_price?.toLocaleString()}만원
|
||||
</p>
|
||||
<button
|
||||
onClick={() => deleteVehicleFromRequest(vehicle.id)}
|
||||
disabled={deletingVehicleId === vehicle.id}
|
||||
className="text-red-500 hover:text-red-700 p-1 disabled:opacity-50"
|
||||
title="Remove from list"
|
||||
>
|
||||
{deletingVehicleId === vehicle.id ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-500"></div>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex items-center gap-2">
|
||||
<p className="text-sm font-medium text-primary-600">
|
||||
{vehicle.car_data.final_price?.toLocaleString()}만원
|
||||
</p>
|
||||
<button
|
||||
onClick={() => deleteVehicleFromRequest(vehicle.id)}
|
||||
disabled={deletingVehicleId === vehicle.id}
|
||||
className="text-red-500 hover:text-red-700 p-1 disabled:opacity-50"
|
||||
title="Remove from list"
|
||||
>
|
||||
{deletingVehicleId === vehicle.id ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-500"></div>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user