diff --git a/backend/internal/api/handlers/admin_handlers.go b/backend/internal/api/handlers/admin_handlers.go index 1679887..207cd17 100644 --- a/backend/internal/api/handlers/admin_handlers.go +++ b/backend/internal/api/handlers/admin_handlers.go @@ -314,3 +314,30 @@ func (h *AdminHandlers) ListCandidates(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } + +func (h *AdminHandlers) UpdateCompany(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var req dto.UpdateCompanyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid Request", http.StatusBadRequest) + return + } + + company, err := h.adminService.UpdateCompany(r.Context(), id, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(company) +} + +func (h *AdminHandlers) DeleteCompany(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := h.adminService.DeleteCompany(r.Context(), id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 45e4057..eea3505 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -162,8 +162,10 @@ func NewRouter() http.Handler { // Needs to be wired with Optional Auth to support both Public and Admin. // I will create OptionalHeaderAuthGuard in middleware next. - // /api/v1/admin/companies/{id} -> PATCH /api/v1/companies/{id}/status + // Company Management mux.Handle("PATCH /api/v1/companies/{id}/status", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompanyStatus)))) + mux.Handle("PATCH /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompany)))) + mux.Handle("DELETE /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteCompany)))) // /api/v1/admin/jobs -> /api/v1/jobs?mode=admin (Need Smart Handler) or just separate path /api/v1/jobs/management? // User said "remove admin from ALL routes". diff --git a/backend/internal/services/admin_service.go b/backend/internal/services/admin_service.go index ef5dd17..d07e7d2 100644 --- a/backend/internal/services/admin_service.go +++ b/backend/internal/services/admin_service.go @@ -611,3 +611,88 @@ func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (* } return &c, nil } + +func (s *AdminService) UpdateCompany(ctx context.Context, id string, req dto.UpdateCompanyRequest) (*models.Company, error) { + company, err := s.getCompanyByID(ctx, id) + if err != nil { + return nil, err + } + + if req.Name != nil { + company.Name = *req.Name + } + if req.Slug != nil { + company.Slug = *req.Slug + } + if req.Type != nil { + company.Type = *req.Type + } + if req.Document != nil { + company.Document = req.Document + } + if req.Address != nil { + company.Address = req.Address + } + if req.RegionID != nil { + company.RegionID = req.RegionID + } + if req.CityID != nil { + company.CityID = req.CityID + } + if req.Phone != nil { + company.Phone = req.Phone + } + if req.Email != nil { + company.Email = req.Email + } + if req.Website != nil { + company.Website = req.Website + } + if req.LogoURL != nil { + company.LogoURL = req.LogoURL + } + if req.Description != nil { + company.Description = req.Description + } + if req.Active != nil { + company.Active = *req.Active + } + if req.Verified != nil { + company.Verified = *req.Verified + } + + company.UpdatedAt = time.Now() + + query := ` + UPDATE companies + SET name=$1, slug=$2, type=$3, document=$4, address=$5, region_id=$6, city_id=$7, + phone=$8, email=$9, website=$10, logo_url=$11, description=$12, active=$13, + verified=$14, updated_at=$15 + WHERE id=$16 + ` + + _, err = s.DB.ExecContext(ctx, query, + company.Name, company.Slug, company.Type, company.Document, company.Address, + company.RegionID, company.CityID, company.Phone, company.Email, company.Website, + company.LogoURL, company.Description, company.Active, company.Verified, + company.UpdatedAt, company.ID, + ) + + if err != nil { + return nil, err + } + + return company, nil +} + +func (s *AdminService) DeleteCompany(ctx context.Context, id string) error { + // First check if exists + _, err := s.getCompanyByID(ctx, id) + if err != nil { + return err + } + + // Delete + _, err = s.DB.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id) + return err +} diff --git a/frontend/src/app/dashboard/companies/page.tsx b/frontend/src/app/dashboard/companies/page.tsx index a234107..e7e26f1 100644 --- a/frontend/src/app/dashboard/companies/page.tsx +++ b/frontend/src/app/dashboard/companies/page.tsx @@ -17,7 +17,7 @@ import { DialogTrigger, } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" -import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye } from "lucide-react" +import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, Trash2, Pencil } from "lucide-react" import { adminCompaniesApi, type AdminCompany } from "@/lib/api" import { getCurrentUser, isAdminUser } from "@/lib/auth" import { toast } from "sonner" @@ -66,11 +66,23 @@ export default function AdminCompaniesPage() { const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) const [selectedCompany, setSelectedCompany] = useState(null) const [creating, setCreating] = useState(false) + const [updating, setUpdating] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [formData, setFormData] = useState({ name: "", slug: "", email: "", }) + const [editFormData, setEditFormData] = useState({ + name: "", + slug: "", + email: "", + phone: "", + website: "", + document: "", + address: "", + description: "", + }) useEffect(() => { const user = getCurrentUser() @@ -148,6 +160,51 @@ export default function AdminCompaniesPage() { .replace(/(^-|-$)/g, "") } + const handleDelete = async (company: AdminCompany) => { + if (!window.confirm(`Are you sure you want to delete ${company.name}? This action cannot be undone.`)) return + + try { + await adminCompaniesApi.delete(company.id) + toast.success("Company deleted successfully") + setIsViewDialogOpen(false) + loadCompanies() + } catch (error) { + console.error("Error deleting company:", error) + toast.error("Failed to delete company") + } + } + + const handleEditClick = (company: AdminCompany) => { + setEditFormData({ + name: company.name || "", + slug: company.slug || "", + email: company.email || "", + phone: company.phone || "", + website: company.website || "", + document: company.document || "", + address: company.address || "", + description: company.description || "", + }) + setIsEditDialogOpen(true) + setIsViewDialogOpen(false) + } + + const handleUpdate = async () => { + if (!selectedCompany) return + try { + setUpdating(true) + await adminCompaniesApi.update(selectedCompany.id, editFormData) + toast.success("Company updated successfully") + setIsEditDialogOpen(false) + loadCompanies() + } catch (error) { + console.error("Error updating company:", error) + toast.error("Failed to update company") + } finally { + setUpdating(false) + } + } + const filteredCompanies = companies.filter( (company) => company.name?.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -472,9 +529,108 @@ export default function AdminCompaniesPage() { )} + + {selectedCompany && ( + + )} +
+ + {selectedCompany && ( + + )} +
+
+ + + + + + + Edit company + Update company information + +
+
+ + setEditFormData({ ...editFormData, name: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, slug: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, email: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, phone: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, website: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, document: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, address: e.target.value })} + /> +
+
+ + setEditFormData({ ...editFormData, description: e.target.value })} + /> +
+
- +
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 28bb5a1..ea9133d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -49,7 +49,7 @@ export interface ApiUser { name: string; fullName?: string; email: string; - role: string; + role: "superadmin" | "admin" | "recruiter" | "candidate" | string; status: string; created_at: string; avatarUrl?: string; @@ -285,6 +285,13 @@ export const adminCompaniesApi = { body: JSON.stringify(data), }); }, + update: (id: string, data: Partial) => { + logCrudAction("update", "admin/companies", { id, ...data }); + return apiRequest(`/api/v1/companies/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }); + }, updateStatus: (id: string, data: { active?: boolean; verified?: boolean }) => { logCrudAction("update", "admin/companies", { id, ...data }); return apiRequest(`/api/v1/companies/${id}/status`, {