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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
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.
|
// Needs to be wired with Optional Auth to support both Public and Admin.
|
||||||
// I will create OptionalHeaderAuthGuard in middleware next.
|
// 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}/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?
|
// /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".
|
// User said "remove admin from ALL routes".
|
||||||
|
|
|
||||||
|
|
@ -611,3 +611,88 @@ func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*
|
||||||
}
|
}
|
||||||
return &c, nil
|
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,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Label } from "@/components/ui/label"
|
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 { adminCompaniesApi, type AdminCompany } from "@/lib/api"
|
||||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
@ -66,11 +66,23 @@ export default function AdminCompaniesPage() {
|
||||||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
||||||
const [selectedCompany, setSelectedCompany] = useState<AdminCompany | null>(null)
|
const [selectedCompany, setSelectedCompany] = useState<AdminCompany | null>(null)
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [updating, setUpdating] = useState(false)
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
email: "",
|
email: "",
|
||||||
})
|
})
|
||||||
|
const [editFormData, setEditFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
slug: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
website: "",
|
||||||
|
document: "",
|
||||||
|
address: "",
|
||||||
|
description: "",
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = getCurrentUser()
|
const user = getCurrentUser()
|
||||||
|
|
@ -148,6 +160,51 @@ export default function AdminCompaniesPage() {
|
||||||
.replace(/(^-|-$)/g, "")
|
.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(
|
const filteredCompanies = companies.filter(
|
||||||
(company) =>
|
(company) =>
|
||||||
company.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
company.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
|
@ -472,9 +529,108 @@ export default function AdminCompaniesPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<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>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button>
|
||||||
Close
|
<Button onClick={handleUpdate} disabled={updating}>
|
||||||
|
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export interface ApiUser {
|
||||||
name: string;
|
name: string;
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: "superadmin" | "admin" | "recruiter" | "candidate" | string;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
|
|
@ -285,6 +285,13 @@ export const adminCompaniesApi = {
|
||||||
body: JSON.stringify(data),
|
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 }) => {
|
updateStatus: (id: string, data: { active?: boolean; verified?: boolean }) => {
|
||||||
logCrudAction("update", "admin/companies", { id, ...data });
|
logCrudAction("update", "admin/companies", { id, ...data });
|
||||||
return apiRequest<void>(`/api/v1/companies/${id}/status`, {
|
return apiRequest<void>(`/api/v1/companies/${id}/status`, {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue