feat(backoffice): implemented edit and delete company functionality
This commit is contained in:
parent
43c0719664
commit
7b76b62490
5 changed files with 282 additions and 5 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue