feat: Add car availability check feature

- Add daily scheduled check for Carmodoo car availability
- Add manual trigger button in admin settings
- Mark sold cars as soldout=True automatically
- Add settings for check time and enable/disable toggle
- Display check status and statistics in admin UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-01-05 08:01:40 +09:00
parent 1c45840c70
commit 4858965087
6 changed files with 474 additions and 2 deletions

View File

@@ -33,6 +33,23 @@ interface SystemSettings {
event_cc_validity_months: number;
withdrawal_enabled: boolean;
min_withdrawal_usd: number;
// Car availability check settings
car_availability_check_enabled: boolean;
car_availability_check_hour: number;
car_availability_last_check: string | null;
car_availability_last_result: string | null;
}
interface CarAvailabilityStatus {
check_enabled: boolean;
check_hour: number;
last_check: string | null;
last_result: string | null;
stats: {
total_cars: number;
available: number;
sold: number;
};
}
interface ExchangeRateWeights {
@@ -50,6 +67,8 @@ export default function SettingsPage() {
const [saving, setSaving] = useState(false);
const [savingExchangeRates, setSavingExchangeRates] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [carAvailabilityStatus, setCarAvailabilityStatus] = useState<CarAvailabilityStatus | null>(null);
const [runningCheck, setRunningCheck] = useState(false);
const [exchangeRateWeights, setExchangeRateWeights] = useState<ExchangeRateWeights>({
usd: 0,
mnt: 0,
@@ -84,11 +103,15 @@ export default function SettingsPage() {
event_cc_validity_months: 6,
withdrawal_enabled: true,
min_withdrawal_usd: 10.0,
// Car availability check
car_availability_check_enabled: true,
car_availability_check_hour: 6,
});
useEffect(() => {
fetchSettings();
fetchExchangeRateWeights();
fetchCarAvailabilityStatus();
}, []);
const fetchSettings = async () => {
@@ -123,6 +146,9 @@ export default function SettingsPage() {
event_cc_validity_months: data.event_cc_validity_months ?? 6,
withdrawal_enabled: data.withdrawal_enabled ?? true,
min_withdrawal_usd: data.min_withdrawal_usd ?? 10.0,
// Car availability check
car_availability_check_enabled: data.car_availability_check_enabled ?? true,
car_availability_check_hour: data.car_availability_check_hour ?? 6,
});
}
} catch (error) {
@@ -145,6 +171,55 @@ export default function SettingsPage() {
}
};
const fetchCarAvailabilityStatus = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/settings/car-availability-status`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setCarAvailabilityStatus(data);
}
} catch (error) {
console.error('Failed to fetch car availability status:', error);
}
};
const triggerCarAvailabilityCheck = async () => {
setRunningCheck(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/settings/car-availability-check`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
setMessage({ type: 'success', text: 'Car availability check started in background. Refresh to see results.' });
// Poll for status updates
setTimeout(() => {
fetchCarAvailabilityStatus();
setRunningCheck(false);
}, 5000);
} else {
const error = await response.json();
setMessage({ type: 'error', text: error.detail || 'Failed to start car availability check' });
setRunningCheck(false);
}
} catch (error) {
console.error('Failed to trigger car availability check:', error);
setMessage({ type: 'error', text: 'Failed to start car availability check' });
setRunningCheck(false);
}
};
const saveExchangeRateWeights = async () => {
setSavingExchangeRates(true);
setMessage(null);
@@ -704,6 +779,98 @@ export default function SettingsPage() {
</div>
</div>
{/* Car Availability Check Settings */}
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
<span>Car Availability Check</span>
</h2>
<div className="space-y-4">
{/* Enable/Disable Toggle */}
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={formData.car_availability_check_enabled}
onChange={(e) => setFormData(prev => ({ ...prev, car_availability_check_enabled: e.target.checked }))}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600"></div>
</label>
<div>
<span className="text-sm font-medium text-gray-700">Enable Daily Auto Check</span>
<p className="text-sm text-gray-500">Import된 Carmodoo </p>
</div>
</div>
{/* Check Hour Setting */}
<div className="max-w-xs">
<label className="block text-sm font-medium text-gray-700 mb-1">
Daily Check Time (Hour, 0-23)
</label>
<input
type="number"
min="0"
max="23"
value={formData.car_availability_check_hour}
onChange={(e) => setFormData(prev => ({ ...prev, car_availability_check_hour: parseInt(e.target.value) || 6 }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
<p className="mt-1 text-sm text-gray-500"> {formData.car_availability_check_hour}:00 </p>
</div>
{/* Manual Check Button */}
<div className="pt-4 border-t">
<button
type="button"
onClick={triggerCarAvailabilityCheck}
disabled={runningCheck}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{runningCheck && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
)}
{runningCheck ? 'Checking...' : 'Run Check Now'}
</button>
<p className="mt-2 text-sm text-gray-500"> ( )</p>
</div>
{/* Status Display */}
{carAvailabilityStatus && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<h3 className="font-medium text-gray-800 mb-3">Check Status</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="bg-white p-3 rounded-lg shadow-sm">
<p className="text-gray-500">Total Cars</p>
<p className="text-xl font-bold text-gray-800">{carAvailabilityStatus.stats.total_cars}</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm">
<p className="text-gray-500">Available</p>
<p className="text-xl font-bold text-green-600">{carAvailabilityStatus.stats.available}</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm">
<p className="text-gray-500">Sold</p>
<p className="text-xl font-bold text-red-600">{carAvailabilityStatus.stats.sold}</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm">
<p className="text-gray-500">Last Check</p>
<p className="text-sm font-medium text-gray-800">
{carAvailabilityStatus.last_check
? new Date(carAvailabilityStatus.last_check).toLocaleString('ko-KR')
: 'Never'}
</p>
</div>
</div>
{carAvailabilityStatus.last_result && (
<div className="mt-3 p-2 bg-blue-50 rounded text-sm text-blue-700">
Last Result: {carAvailabilityStatus.last_result}
</div>
)}
</div>
)}
</div>
</div>
{/* Submit Button */}
<div className="flex justify-end">
<button