261 lines
8.7 KiB
TypeScript
261 lines
8.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import Input from '@/components/ui/Input';
|
|
import {
|
|
getDriverRepository,
|
|
getGetTeamJoinRequestsQuery,
|
|
getApproveTeamJoinRequestUseCase,
|
|
getRejectTeamJoinRequestUseCase,
|
|
getUpdateTeamUseCase,
|
|
} from '@/lib/di-container';
|
|
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
|
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
|
import type { Team, TeamJoinRequest } from '@gridpilot/racing';
|
|
|
|
interface TeamAdminProps {
|
|
team: Team;
|
|
onUpdate: () => void;
|
|
}
|
|
|
|
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
|
const [joinRequests, setJoinRequests] = useState<TeamJoinRequest[]>([]);
|
|
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverDTO>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [editedTeam, setEditedTeam] = useState({
|
|
name: team.name,
|
|
tag: team.tag,
|
|
description: team.description,
|
|
});
|
|
|
|
useEffect(() => {
|
|
void loadJoinRequests();
|
|
}, [team.id]);
|
|
|
|
const loadJoinRequests = async () => {
|
|
const query = getGetTeamJoinRequestsQuery();
|
|
const requests = await query.execute({ teamId: team.id });
|
|
setJoinRequests(requests);
|
|
|
|
const driverRepo = getDriverRepository();
|
|
const allDrivers = await driverRepo.findAll();
|
|
const driverMap: Record<string, DriverDTO> = {};
|
|
|
|
for (const request of requests) {
|
|
const driver = allDrivers.find(d => d.id === request.driverId);
|
|
if (driver) {
|
|
const dto = EntityMappers.toDriverDTO(driver);
|
|
if (dto) {
|
|
driverMap[request.driverId] = dto;
|
|
}
|
|
}
|
|
}
|
|
|
|
setRequestDrivers(driverMap);
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleApprove = async (requestId: string) => {
|
|
try {
|
|
const useCase = getApproveTeamJoinRequestUseCase();
|
|
await useCase.execute({ requestId });
|
|
await loadJoinRequests();
|
|
onUpdate();
|
|
} catch (error) {
|
|
alert(error instanceof Error ? error.message : 'Failed to approve request');
|
|
}
|
|
};
|
|
|
|
const handleReject = async (requestId: string) => {
|
|
try {
|
|
const useCase = getRejectTeamJoinRequestUseCase();
|
|
await useCase.execute({ requestId });
|
|
await loadJoinRequests();
|
|
} catch (error) {
|
|
alert(error instanceof Error ? error.message : 'Failed to reject request');
|
|
}
|
|
};
|
|
|
|
const handleSaveChanges = async () => {
|
|
try {
|
|
const useCase = getUpdateTeamUseCase();
|
|
await useCase.execute({
|
|
teamId: team.id,
|
|
updates: {
|
|
name: editedTeam.name,
|
|
tag: editedTeam.tag,
|
|
description: editedTeam.description,
|
|
},
|
|
updatedBy: team.ownerId,
|
|
});
|
|
setEditMode(false);
|
|
onUpdate();
|
|
} catch (error) {
|
|
alert(error instanceof Error ? error.message : 'Failed to update team');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-xl font-semibold text-white">Team Settings</h3>
|
|
{!editMode && (
|
|
<Button variant="secondary" onClick={() => setEditMode(true)}>
|
|
Edit Details
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{editMode ? (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-400 mb-2">
|
|
Team Name
|
|
</label>
|
|
<Input
|
|
type="text"
|
|
value={editedTeam.name}
|
|
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-400 mb-2">
|
|
Team Tag
|
|
</label>
|
|
<Input
|
|
type="text"
|
|
value={editedTeam.tag}
|
|
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
|
|
maxLength={4}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-400 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
|
|
rows={4}
|
|
value={editedTeam.description}
|
|
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button variant="primary" onClick={handleSaveChanges}>
|
|
Save Changes
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setEditMode(false);
|
|
setEditedTeam({
|
|
name: team.name,
|
|
tag: team.tag,
|
|
description: team.description,
|
|
});
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="text-sm text-gray-400">Team Name</div>
|
|
<div className="text-white font-medium">{team.name}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-400">Team Tag</div>
|
|
<div className="text-white font-medium">{team.tag}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-400">Description</div>
|
|
<div className="text-white">{team.description}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-xl font-semibold text-white mb-6">Join Requests</h3>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-8 text-gray-400">Loading requests...</div>
|
|
) : joinRequests.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{joinRequests.map((request) => {
|
|
const driver = requestDrivers[request.driverId];
|
|
if (!driver) return null;
|
|
|
|
return (
|
|
<div
|
|
key={request.id}
|
|
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
|
|
>
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
|
|
{driver.name.charAt(0)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h4 className="text-white font-medium">{driver.name}</h4>
|
|
<p className="text-sm text-gray-400">
|
|
{driver.country} • Requested {new Date(request.requestedAt).toLocaleDateString()}
|
|
</p>
|
|
{request.message && (
|
|
<p className="text-sm text-gray-300 mt-1 italic">
|
|
"{request.message}"
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => handleApprove(request.id)}
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
onClick={() => handleReject(request.id)}
|
|
>
|
|
Reject
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-400">
|
|
No pending join requests
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-xl font-semibold text-white mb-4">Danger Zone</h3>
|
|
<div className="space-y-4">
|
|
<div className="p-4 rounded-lg bg-danger-red/10 border border-danger-red/30">
|
|
<h4 className="text-white font-medium mb-2">Disband Team</h4>
|
|
<p className="text-sm text-gray-400 mb-4">
|
|
Permanently delete this team. This action cannot be undone.
|
|
</p>
|
|
<Button variant="danger" disabled>
|
|
Disband Team (Coming Soon)
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
} |