diff --git a/src/components/pages/LanguagesPage.tsx b/src/components/pages/LanguagesPage.tsx index da49e1b..0481597 100644 --- a/src/components/pages/LanguagesPage.tsx +++ b/src/components/pages/LanguagesPage.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react" -import { Languages, Plus } from "lucide-react" +import { Languages, Plus, Trash2 } from "lucide-react" import { toast } from "sonner" import { languagesAPI } from "@/services/api" +import type { LanguageResponse, LanguageStatsResponse } from "@/types" import { useLanguagesStore } from "@/stores/languagesStore" +import { useAuth } from "@/contexts/AuthContext" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -21,18 +23,26 @@ import { InfoTooltip } from "@/components/common/InfoTooltip" import { formatDate } from "@/utils/format" export default function LanguagesPage() { + const { user, isPlatformAdmin, isManager } = useAuth() const { languages, loading: storeLoading, lastFetched, fetch: fetchLanguages } = useLanguagesStore() const loading = storeLoading || (!lastFetched && languages.length === 0) const [dialogOpen, setDialogOpen] = useState(false) const [creating, setCreating] = useState(false) const [name, setName] = useState("") const [code, setCode] = useState("") + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleteStats, setDeleteStats] = useState(null) + const [deleting, setDeleting] = useState(false) useEffect(() => { fetchLanguages() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + function canDeactivate(lang: LanguageResponse) { + return isPlatformAdmin || (isManager && lang.created_by === user?.id) + } + function openDialog() { setName("") setCode("") @@ -60,6 +70,42 @@ export default function LanguagesPage() { } } + async function openDeleteDialog(lang: LanguageResponse) { + setDeleteTarget(lang) + setDeleteStats(null) + try { + const { data } = await languagesAPI.stats(lang.id) + setDeleteStats(data) + } catch { + setDeleteStats(null) + } + } + + const blockedByProjects = (deleteStats?.project_count ?? 0) > 0 + + async function handleDeactivate() { + if (!deleteTarget || deleteStats === null || blockedByProjects) return + setDeleting(true) + try { + await languagesAPI.delete(deleteTarget.id) + toast.success("Language deactivated") + setDeleteTarget(null) + useLanguagesStore.getState().invalidate() + await fetchLanguages() + } catch (err: unknown) { + const status = (err as { response?: { status?: number } })?.response?.status + if (status === 409) { + toast.error("This language is used by projects and cannot be deactivated") + } else if (status === 403) { + toast.error("You can only deactivate languages you created") + } else { + toast.error("Failed to deactivate language") + } + } finally { + setDeleting(false) + } + } + if (loading) { return } @@ -95,8 +141,19 @@ export default function LanguagesPage() { {languages.map((lang) => (
+ {canDeactivate(lang) && ( + + )}
{lang.code} @@ -167,6 +224,48 @@ export default function LanguagesPage() { + + { if (!open) setDeleteTarget(null) }}> + + + Deactivate Language + + {deleteTarget + ? deleteStats === null + ? `Checking which projects use "${deleteTarget.name}"...` + : blockedByProjects + ? `"${deleteTarget.name}" is used by ${deleteStats.project_count} project${deleteStats.project_count !== 1 ? "s" : ""} and cannot be deactivated. Reassign or remove it from these projects first.` + : `Deactivating "${deleteTarget.name}" hides it from new project creation. Existing projects are unaffected and it can be restored later.` + : ""} + + + {blockedByProjects && deleteStats && ( +
    + {deleteStats.projects.map((project) => ( +
  • + {project.name} +
  • + ))} +
+ )} + + + + +
+
) } diff --git a/src/services/api.ts b/src/services/api.ts index d0f3a8c..d48bfb0 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -16,6 +16,7 @@ import type { AppRoleResponse, LanguageResponse, LanguageCreate, + LanguageStatsResponse, OrganizationResponse, OrganizationCreate, OrganizationUpdate, @@ -174,6 +175,10 @@ export const languagesAPI = { api.get(`/languages/${languageId}`), getByCode: (code: string) => api.get(`/languages/code/${code}`), + stats: (languageId: string) => + api.get(`/languages/${languageId}/stats`), + delete: (languageId: string) => + api.delete(`/languages/${languageId}`), } export const orgsAPI = { diff --git a/src/types/index.ts b/src/types/index.ts index e343aa0..7411888 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,6 +25,8 @@ export type { export type { LanguageResponse, LanguageCreate, + LanguageProjectRef, + LanguageStatsResponse, } from "./language" export type { diff --git a/src/types/language.ts b/src/types/language.ts index fb5a8bd..daa9cdb 100644 --- a/src/types/language.ts +++ b/src/types/language.ts @@ -2,6 +2,7 @@ export interface LanguageResponse { id: string name: string code: string + created_by: string | null created_at: string } @@ -9,3 +10,14 @@ export interface LanguageCreate { name: string code: string } + +export interface LanguageProjectRef { + id: string + name: string +} + +export interface LanguageStatsResponse { + language_id: string + project_count: number + projects: LanguageProjectRef[] +}