Mission Refactor Big
This commit is contained in:
parent
55585db2f1
commit
0f275fc45d
157
app/api/missions/[missionId]/close/route.ts
Normal file
157
app/api/missions/[missionId]/close/route.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/app/api/auth/options";
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
/**
|
||||
* POST /api/missions/[missionId]/close
|
||||
*
|
||||
* Closes a mission by calling N8N webhook to close it in external services
|
||||
* and marking it as closed in the database.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
props: { params: Promise<{ missionId: string }> }
|
||||
) {
|
||||
const params = await props.params;
|
||||
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { missionId } = params;
|
||||
if (!missionId) {
|
||||
return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the mission with all details needed for N8N
|
||||
const mission = await prisma.mission.findUnique({
|
||||
where: { id: missionId },
|
||||
include: {
|
||||
missionUsers: {
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!mission) {
|
||||
return NextResponse.json({ error: 'Mission not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user has permission (creator or admin)
|
||||
const isCreator = mission.creatorId === session.user.id;
|
||||
const userRoles = Array.isArray(session.user.role) ? session.user.role : [];
|
||||
const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN');
|
||||
|
||||
if (!isCreator && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if already closed
|
||||
if ((mission as any).isClosed) {
|
||||
return NextResponse.json({ error: 'Mission is already closed' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract repo name from giteaRepositoryUrl if present
|
||||
let repoName = '';
|
||||
if (mission.giteaRepositoryUrl) {
|
||||
try {
|
||||
const url = new URL(mission.giteaRepositoryUrl);
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
repoName = pathParts[pathParts.length - 1] || '';
|
||||
logger.debug('Extracted repo name from URL', { repoName });
|
||||
} catch (error) {
|
||||
const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/);
|
||||
repoName = match ? match[1] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for N8N webhook (same format as deletion)
|
||||
const n8nCloseData = {
|
||||
missionId: mission.id,
|
||||
name: mission.name,
|
||||
repoName: repoName,
|
||||
leantimeProjectId: mission.leantimeProjectId || 0,
|
||||
documentationCollectionId: mission.outlineCollectionId || '',
|
||||
rocketchatChannelId: mission.rocketChatChannelId || '',
|
||||
giteaRepositoryUrl: mission.giteaRepositoryUrl,
|
||||
outlineCollectionId: mission.outlineCollectionId,
|
||||
rocketChatChannelId: mission.rocketChatChannelId,
|
||||
penpotProjectId: mission.penpotProjectId,
|
||||
action: 'close' // Indicate this is a close action, not delete
|
||||
};
|
||||
|
||||
logger.debug('Calling N8N NeahMissionClose webhook', {
|
||||
missionId: mission.id,
|
||||
missionName: mission.name,
|
||||
hasRepoName: !!repoName
|
||||
});
|
||||
|
||||
// Call N8N webhook
|
||||
const webhookUrl = process.env.N8N_CLOSE_MISSION_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/NeahMissionClose';
|
||||
const apiKey = process.env.N8N_API_KEY || '';
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey
|
||||
},
|
||||
body: JSON.stringify(n8nCloseData),
|
||||
});
|
||||
|
||||
logger.debug('N8N close webhook response', { status: response.status });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.error('N8N close webhook error', {
|
||||
status: response.status,
|
||||
error: errorText.substring(0, 200)
|
||||
});
|
||||
// Continue with closing even if N8N fails (non-blocking)
|
||||
logger.warn('Continuing with mission close despite N8N error');
|
||||
}
|
||||
|
||||
// Mark mission as closed in database
|
||||
// Using 'as any' until prisma generate is run
|
||||
const updatedMission = await (prisma.mission as any).update({
|
||||
where: { id: missionId },
|
||||
data: {
|
||||
isClosed: true,
|
||||
closedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('Mission closed successfully', {
|
||||
missionId: updatedMission.id,
|
||||
closedAt: updatedMission.closedAt
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Mission closed successfully',
|
||||
mission: {
|
||||
id: updatedMission.id,
|
||||
name: updatedMission.name,
|
||||
isClosed: updatedMission.isClosed,
|
||||
closedAt: updatedMission.closedAt
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error closing mission', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
missionId: params.missionId
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to close mission', details: error instanceof Error ? error.message : String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,9 @@ import {
|
||||
Sparkles,
|
||||
Save,
|
||||
Loader2,
|
||||
FileText
|
||||
FileText,
|
||||
Lock,
|
||||
CheckCircle
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
@ -54,6 +56,8 @@ interface Mission {
|
||||
attachments?: Attachment[];
|
||||
actionPlan?: string | null;
|
||||
actionPlanGeneratedAt?: string | null;
|
||||
isClosed?: boolean;
|
||||
closedAt?: string | null;
|
||||
createdAt: string;
|
||||
creator: User;
|
||||
missionUsers: any[];
|
||||
@ -63,6 +67,7 @@ export default function MissionDetailPage() {
|
||||
const [mission, setMission] = useState<Mission | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
const [generatingPlan, setGeneratingPlan] = useState(false);
|
||||
const [savingPlan, setSavingPlan] = useState(false);
|
||||
const [editedPlan, setEditedPlan] = useState<string>("");
|
||||
@ -215,6 +220,48 @@ export default function MissionDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle close mission
|
||||
const handleCloseMission = async () => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir clôturer cette mission ? Cette action fermera la mission dans tous les services externes.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setClosing(true);
|
||||
const response = await fetch(`/api/missions/${missionId}/close`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to close mission');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update local state
|
||||
setMission(prev => prev ? {
|
||||
...prev,
|
||||
isClosed: true,
|
||||
closedAt: data.mission.closedAt
|
||||
} : null);
|
||||
|
||||
toast({
|
||||
title: "Mission clôturée",
|
||||
description: "La mission a été clôturée avec succès dans tous les services",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error closing mission:', error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: error instanceof Error ? error.message : "Impossible de clôturer la mission",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setClosing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle generate action plan
|
||||
const handleGeneratePlan = async () => {
|
||||
try {
|
||||
@ -520,8 +567,42 @@ export default function MissionDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mission Status & Action Buttons */}
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Closed Status */}
|
||||
{mission.isClosed && (
|
||||
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-4 py-2 rounded-lg border border-amber-200">
|
||||
<Lock className="h-4 w-4" />
|
||||
<span className="font-medium">Mission clôturée</span>
|
||||
{mission.closedAt && (
|
||||
<span className="text-sm text-amber-500">
|
||||
le {formatDate(mission.closedAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!mission.isClosed && <div></div>}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
{/* Close Button - only show if not already closed */}
|
||||
{!mission.isClosed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 border-amber-600 text-amber-600 hover:bg-amber-50 bg-white"
|
||||
onClick={handleCloseMission}
|
||||
disabled={closing}
|
||||
>
|
||||
{closing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Lock className="h-4 w-4" />
|
||||
)}
|
||||
Clôturer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Delete Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 border-red-600 text-red-600 hover:bg-red-50 bg-white"
|
||||
@ -533,9 +614,10 @@ export default function MissionDetailPage() {
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
Supprimer la mission
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Plan d'actions Tab */}
|
||||
|
||||
@ -134,6 +134,8 @@ model Mission {
|
||||
profils String[] // Level / Profils
|
||||
actionPlan String? // Generated action plan from LLM (stored as text/markdown)
|
||||
actionPlanGeneratedAt DateTime? // When the action plan was generated
|
||||
isClosed Boolean @default(false) // Whether the mission is closed
|
||||
closedAt DateTime? // When the mission was closed
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user