This commit is contained in:
2025-06-07 19:48:46 +09:00
parent d12d55020f
commit ed47add7bc

View File

@ -33,15 +33,30 @@ interface EnvVars {
[key: string]: string | undefined;
}
interface FeedRequest {
id: string;
url: string;
requestedBy?: string;
requestMessage?: string;
status: 'pending' | 'approved' | 'rejected';
createdAt: string;
reviewedAt?: string;
reviewedBy?: string;
adminNotes?: string;
}
function App() {
const [feeds, setFeeds] = useState<Feed[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [envVars, setEnvVars] = useState<EnvVars>({});
const [feedRequests, setFeedRequests] = useState<FeedRequest[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [newFeedUrl, setNewFeedUrl] = useState('');
const [activeTab, setActiveTab] = useState<'dashboard' | 'feeds' | 'env' | 'batch'>('dashboard');
const [requestFilter, setRequestFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all');
const [approvalNotes, setApprovalNotes] = useState<{[key: string]: string}>({});
const [activeTab, setActiveTab] = useState<'dashboard' | 'feeds' | 'env' | 'batch' | 'requests'>('dashboard');
useEffect(() => {
loadData();
@ -50,25 +65,28 @@ function App() {
const loadData = async () => {
setLoading(true);
try {
const [feedsRes, statsRes, envRes] = await Promise.all([
const [feedsRes, statsRes, envRes, requestsRes] = await Promise.all([
fetch('/api/admin/feeds'),
fetch('/api/admin/stats'),
fetch('/api/admin/env')
fetch('/api/admin/env'),
fetch('/api/admin/feed-requests')
]);
if (!feedsRes.ok || !statsRes.ok || !envRes.ok) {
if (!feedsRes.ok || !statsRes.ok || !envRes.ok || !requestsRes.ok) {
throw new Error('Failed to load data');
}
const [feedsData, statsData, envData] = await Promise.all([
const [feedsData, statsData, envData, requestsData] = await Promise.all([
feedsRes.json(),
statsRes.json(),
envRes.json()
envRes.json(),
requestsRes.json()
]);
setFeeds(feedsData);
setStats(statsData);
setEnvVars(envData);
setFeedRequests(requestsData);
setError(null);
} catch (err) {
setError('データの読み込みに失敗しました');
@ -218,6 +236,65 @@ function App() {
}
};
const approveFeedRequest = async (requestId: string, notes: string) => {
try {
const res = await fetch(`/api/admin/feed-requests/${requestId}/approve`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adminNotes: notes })
});
const data = await res.json();
if (res.ok) {
setSuccess(data.message || 'フィードリクエストを承認しました');
setApprovalNotes({ ...approvalNotes, [requestId]: '' });
loadData();
} else {
setError(data.error || 'フィードリクエストの承認に失敗しました');
}
} catch (err) {
setError('フィードリクエストの承認に失敗しました');
console.error('Error approving feed request:', err);
}
};
const rejectFeedRequest = async (requestId: string, notes: string) => {
if (!confirm('このフィードリクエストを拒否しますか?')) {
return;
}
try {
const res = await fetch(`/api/admin/feed-requests/${requestId}/reject`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adminNotes: notes })
});
const data = await res.json();
if (res.ok) {
setSuccess(data.message || 'フィードリクエストを拒否しました');
setApprovalNotes({ ...approvalNotes, [requestId]: '' });
loadData();
} else {
setError(data.error || 'フィードリクエストの拒否に失敗しました');
}
} catch (err) {
setError('フィードリクエストの拒否に失敗しました');
console.error('Error rejecting feed request:', err);
}
};
const updateApprovalNotes = (requestId: string, notes: string) => {
setApprovalNotes({ ...approvalNotes, [requestId]: notes });
};
const filteredRequests = feedRequests.filter(request => {
if (requestFilter === 'all') return true;
return request.status === requestFilter;
});
if (loading) {
return <div className="container"><div className="loading">...</div></div>;
}
@ -253,6 +330,12 @@ function App() {
>
</button>
<button
className={`btn ${activeTab === 'requests' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setActiveTab('requests')}
>
</button>
<button
className={`btn ${activeTab === 'env' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setActiveTab('env')}
@ -481,6 +564,118 @@ function App() {
</>
)}
{activeTab === 'requests' && (
<>
<h3></h3>
<p style={{ marginBottom: '20px', color: '#7f8c8d' }}>
</p>
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className={`btn ${requestFilter === 'all' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setRequestFilter('all')}
>
({feedRequests.length})
</button>
<button
className={`btn ${requestFilter === 'pending' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setRequestFilter('pending')}
>
({feedRequests.filter(r => r.status === 'pending').length})
</button>
<button
className={`btn ${requestFilter === 'approved' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setRequestFilter('approved')}
>
({feedRequests.filter(r => r.status === 'approved').length})
</button>
<button
className={`btn ${requestFilter === 'rejected' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setRequestFilter('rejected')}
>
({feedRequests.filter(r => r.status === 'rejected').length})
</button>
</div>
</div>
{filteredRequests.length === 0 ? (
<p style={{ color: '#7f8c8d', textAlign: 'center', padding: '20px' }}>
{requestFilter === 'all' ? 'フィードリクエストがありません' : `${requestFilter === 'pending' ? '保留中' : requestFilter === 'approved' ? '承認済み' : '拒否済み'}のリクエストがありません`}
</p>
) : (
<div className="requests-list">
{filteredRequests.map((request) => (
<div key={request.id} className="feed-item" style={{ marginBottom: '16px' }}>
<div className="feed-info">
<h4 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{request.url}</h4>
{request.requestMessage && (
<p style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#666' }}>
: {request.requestMessage}
</p>
)}
<div style={{ fontSize: '12px', color: '#999' }}>
<span>: {request.requestedBy || '匿名'}</span>
<span style={{ margin: '0 8px' }}>|</span>
<span>: {new Date(request.createdAt).toLocaleString('ja-JP')}</span>
{request.reviewedAt && (
<>
<span style={{ margin: '0 8px' }}>|</span>
<span>: {new Date(request.reviewedAt).toLocaleString('ja-JP')}</span>
</>
)}
</div>
<span className={`status ${request.status === 'approved' ? 'active' : request.status === 'rejected' ? 'inactive' : ''}`}>
{request.status === 'pending' ? '保留中' : request.status === 'approved' ? '承認済み' : '拒否済み'}
</span>
{request.adminNotes && (
<div style={{ marginTop: '8px', padding: '8px', background: '#f8f9fa', borderRadius: '4px', fontSize: '14px' }}>
<strong>:</strong> {request.adminNotes}
</div>
)}
</div>
{request.status === 'pending' && (
<div className="feed-actions" style={{ flexDirection: 'column', gap: '8px', minWidth: '200px' }}>
<textarea
placeholder="管理者メモ(任意)"
value={approvalNotes[request.id] || ''}
onChange={(e) => updateApprovalNotes(request.id, e.target.value)}
style={{
width: '100%',
minHeight: '60px',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '12px',
resize: 'vertical'
}}
/>
<div style={{ display: 'flex', gap: '4px' }}>
<button
className="btn btn-success"
onClick={() => approveFeedRequest(request.id, approvalNotes[request.id] || '')}
style={{ fontSize: '12px', padding: '6px 12px' }}
>
</button>
<button
className="btn btn-danger"
onClick={() => rejectFeedRequest(request.id, approvalNotes[request.id] || '')}
style={{ fontSize: '12px', padding: '6px 12px' }}
>
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</>
)}
{activeTab === 'env' && (
<>
<h3></h3>