feat(backoffice): implemented edit and delete company functionality

This commit is contained in:
Tiago Yamamoto 2025-12-26 01:23:01 -03:00
parent 43c0719664
commit 7b76b62490
5 changed files with 282 additions and 5 deletions

View file

@ -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)
}

View file

@ -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".

View file

@ -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
}

View file

@ -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<AdminCompany | null>(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,10 +529,109 @@ export default function AdminCompaniesPage() {
</div>
</div>
)}
<DialogFooter>
<DialogFooter className="flex w-full justify-between sm:justify-between">
{selectedCompany && (
<Button
variant="destructive"
onClick={() => handleDelete(selectedCompany)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
)}
<div className="flex gap-2">
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
Close
</Button>
{selectedCompany && (
<Button onClick={() => handleEditClick(selectedCompany)}>
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit company</DialogTitle>
<DialogDescription>Update company information</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">Company name</Label>
<Input
id="edit-name"
value={editFormData.name}
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-slug">Slug</Label>
<Input
id="edit-slug"
value={editFormData.slug}
onChange={(e) => setEditFormData({ ...editFormData, slug: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-email">Email</Label>
<Input
id="edit-email"
value={editFormData.email}
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-phone">Phone</Label>
<Input
id="edit-phone"
value={editFormData.phone}
onChange={(e) => setEditFormData({ ...editFormData, phone: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-website">Website</Label>
<Input
id="edit-website"
value={editFormData.website}
onChange={(e) => setEditFormData({ ...editFormData, website: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-document">Document</Label>
<Input
id="edit-document"
value={editFormData.document}
onChange={(e) => setEditFormData({ ...editFormData, document: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-address">Address</Label>
<Input
id="edit-address"
value={editFormData.address}
onChange={(e) => setEditFormData({ ...editFormData, address: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-description">Description</Label>
<Input
id="edit-description"
value={editFormData.description}
onChange={(e) => setEditFormData({ ...editFormData, description: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button>
<Button onClick={handleUpdate} disabled={updating}>
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -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<AdminCompany>) => {
logCrudAction("update", "admin/companies", { id, ...data });
return apiRequest<AdminCompany>(`/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<void>(`/api/v1/companies/${id}/status`, {