Add CRUD logging and stabilize dashboard dates
This commit is contained in:
parent
fffe732776
commit
5e99115df6
5 changed files with 91 additions and 32 deletions
|
|
@ -30,6 +30,12 @@ import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
|||
import { toast } from "sonner"
|
||||
import { Archive, CheckCircle, Copy, PauseCircle, Plus, RefreshCw, XCircle } from "lucide-react"
|
||||
|
||||
const auditDateFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
const jobStatusBadge: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
|
||||
draft: { label: "Draft", variant: "outline" },
|
||||
review: { label: "Review", variant: "secondary" },
|
||||
|
|
@ -234,7 +240,7 @@ export default function BackofficePage() {
|
|||
<TableCell className="font-medium">{audit.identifier}</TableCell>
|
||||
<TableCell>{audit.roles}</TableCell>
|
||||
<TableCell>{audit.ipAddress || "-"}</TableCell>
|
||||
<TableCell>{new Date(audit.createdAt).toLocaleString()}</TableCell>
|
||||
<TableCell>{auditDateFormatter.format(new Date(audit.createdAt))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import { companiesApi, type ApiCompany } from "@/lib/api"
|
|||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const companyDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
export default function AdminCompaniesPage() {
|
||||
const router = useRouter()
|
||||
const [companies, setCompanies] = useState<ApiCompany[]>([])
|
||||
|
|
@ -252,7 +257,7 @@ export default function AdminCompaniesPage() {
|
|||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{company.created_at ? new Date(company.created_at).toLocaleDateString("en-US") : "-"}
|
||||
{company.created_at ? companyDateFormatter.format(new Date(company.created_at)) : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ import { usersApi, type ApiUser } from "@/lib/api"
|
|||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const userDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
const router = useRouter()
|
||||
const [users, setUsers] = useState<ApiUser[]>([])
|
||||
|
|
@ -288,7 +293,7 @@ export default function AdminUsersPage() {
|
|||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.created_at ? new Date(user.created_at).toLocaleDateString("en-US") : "-"}
|
||||
{user.created_at ? userDateFormatter.format(new Date(user.created_at)) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ export default function RootLayout({
|
|||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const shouldLoadAnalytics =
|
||||
process.env.VERCEL === "1" || Boolean(process.env.NEXT_PUBLIC_VERCEL_ANALYTICS_ID)
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable} antialiased`}>
|
||||
|
|
@ -40,7 +43,7 @@ export default function RootLayout({
|
|||
/>
|
||||
</NotificationProvider>
|
||||
</I18nProvider>
|
||||
{process.env.NODE_ENV === "production" && <Analytics />}
|
||||
{process.env.NODE_ENV === "production" && shouldLoadAnalytics && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,9 +69,17 @@ async function apiRequest<T>(
|
|||
return res.json();
|
||||
}
|
||||
|
||||
type CrudAction = "create" | "read" | "update" | "delete";
|
||||
|
||||
const logCrudAction = (action: CrudAction, resource: string, details?: unknown) => {
|
||||
const detailPayload = details ? { details } : undefined;
|
||||
console.log(`[CRUD:${action.toUpperCase()}] ${resource}`, detailPayload ?? "");
|
||||
};
|
||||
|
||||
// Users API
|
||||
export const usersApi = {
|
||||
list: (params?: { page?: number; limit?: number }) => {
|
||||
logCrudAction("read", "users", params);
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set("page", String(params.page));
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
|
|
@ -79,27 +87,36 @@ export const usersApi = {
|
|||
return apiRequest<PaginatedResponse<ApiUser>>(`/api/v1/users${queryStr ? `?${queryStr}` : ""}`);
|
||||
},
|
||||
|
||||
create: (data: { name: string; email: string; password: string; role: string }) =>
|
||||
apiRequest<ApiUser>("/api/v1/users", {
|
||||
create: (data: { name: string; email: string; password: string; role: string }) => {
|
||||
logCrudAction("create", "users", { name: data.name, email: data.email, role: data.role });
|
||||
return apiRequest<ApiUser>("/api/v1/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
delete: (id: string) =>
|
||||
apiRequest<void>(`/api/v1/users/${id}`, {
|
||||
delete: (id: string) => {
|
||||
logCrudAction("delete", "users", { id });
|
||||
return apiRequest<void>(`/api/v1/users/${id}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Companies API
|
||||
export const companiesApi = {
|
||||
list: () => apiRequest<ApiCompany[]>("/api/v1/companies"),
|
||||
list: () => {
|
||||
logCrudAction("read", "companies");
|
||||
return apiRequest<ApiCompany[]>("/api/v1/companies");
|
||||
},
|
||||
|
||||
create: (data: { name: string; slug: string; email?: string }) =>
|
||||
apiRequest<ApiCompany>("/api/v1/companies", {
|
||||
create: (data: { name: string; slug: string; email?: string }) => {
|
||||
logCrudAction("create", "companies", data);
|
||||
return apiRequest<ApiCompany>("/api/v1/companies", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Jobs API (public)
|
||||
|
|
@ -142,6 +159,7 @@ export interface PaginatedResponse<T> {
|
|||
|
||||
export const jobsApi = {
|
||||
list: (params?: { page?: number; limit?: number; companyId?: number }) => {
|
||||
logCrudAction("read", "jobs", params);
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set('page', String(params.page));
|
||||
if (params?.limit) query.set('limit', String(params.limit));
|
||||
|
|
@ -150,7 +168,10 @@ export const jobsApi = {
|
|||
return apiRequest<PaginatedResponse<ApiJob>>(`/jobs${queryStr ? `?${queryStr}` : ''}`);
|
||||
},
|
||||
|
||||
getById: (id: number) => apiRequest<ApiJob>(`/jobs/${id}`),
|
||||
getById: (id: number) => {
|
||||
logCrudAction("read", "jobs", { id });
|
||||
return apiRequest<ApiJob>(`/jobs/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
// Admin Backoffice API
|
||||
|
|
@ -184,27 +205,37 @@ export interface AdminTag {
|
|||
}
|
||||
|
||||
export const adminAccessApi = {
|
||||
listRoles: () => apiRequest<AdminRoleAccess[]>("/api/v1/admin/access/roles"),
|
||||
listRoles: () => {
|
||||
logCrudAction("read", "admin/access/roles");
|
||||
return apiRequest<AdminRoleAccess[]>("/api/v1/admin/access/roles");
|
||||
},
|
||||
};
|
||||
|
||||
export const adminAuditApi = {
|
||||
listLogins: (limit = 50) => apiRequest<AdminLoginAudit[]>(`/api/v1/admin/audit/logins?limit=${limit}`),
|
||||
listLogins: (limit = 50) => {
|
||||
logCrudAction("read", "admin/audit/logins", { limit });
|
||||
return apiRequest<AdminLoginAudit[]>(`/api/v1/admin/audit/logins?limit=${limit}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const adminCompaniesApi = {
|
||||
list: (verified?: boolean) => {
|
||||
logCrudAction("read", "admin/companies", typeof verified === "boolean" ? { verified } : undefined);
|
||||
const query = typeof verified === "boolean" ? `?verified=${verified}` : "";
|
||||
return apiRequest<AdminCompany[]>(`/api/v1/admin/companies${query}`);
|
||||
},
|
||||
updateStatus: (id: number, data: { active?: boolean; verified?: boolean }) =>
|
||||
apiRequest<AdminCompany>(`/api/v1/admin/companies/${id}`, {
|
||||
updateStatus: (id: number, data: { active?: boolean; verified?: boolean }) => {
|
||||
logCrudAction("update", "admin/companies", { id, ...data });
|
||||
return apiRequest<AdminCompany>(`/api/v1/admin/companies/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const adminJobsApi = {
|
||||
list: (params?: { page?: number; limit?: number; status?: string }) => {
|
||||
logCrudAction("read", "admin/jobs", params);
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set("page", String(params.page));
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
|
|
@ -212,32 +243,41 @@ export const adminJobsApi = {
|
|||
const queryStr = query.toString();
|
||||
return apiRequest<PaginatedResponse<AdminJob>>(`/api/v1/admin/jobs${queryStr ? `?${queryStr}` : ""}`);
|
||||
},
|
||||
updateStatus: (id: number, status: string) =>
|
||||
apiRequest<AdminJob>(`/api/v1/admin/jobs/${id}/status`, {
|
||||
updateStatus: (id: number, status: string) => {
|
||||
logCrudAction("update", "admin/jobs/status", { id, status });
|
||||
return apiRequest<AdminJob>(`/api/v1/admin/jobs/${id}/status`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status }),
|
||||
}),
|
||||
duplicate: (id: number) =>
|
||||
apiRequest<AdminJob>(`/api/v1/admin/jobs/${id}/duplicate`, {
|
||||
});
|
||||
},
|
||||
duplicate: (id: number) => {
|
||||
logCrudAction("create", "admin/jobs/duplicate", { id });
|
||||
return apiRequest<AdminJob>(`/api/v1/admin/jobs/${id}/duplicate`, {
|
||||
method: "POST",
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const adminTagsApi = {
|
||||
list: (category?: "area" | "level" | "stack") => {
|
||||
logCrudAction("read", "admin/tags", category ? { category } : undefined);
|
||||
const query = category ? `?category=${category}` : "";
|
||||
return apiRequest<AdminTag[]>(`/api/v1/admin/tags${query}`);
|
||||
},
|
||||
create: (data: { name: string; category: "area" | "level" | "stack" }) =>
|
||||
apiRequest<AdminTag>("/api/v1/admin/tags", {
|
||||
create: (data: { name: string; category: "area" | "level" | "stack" }) => {
|
||||
logCrudAction("create", "admin/tags", data);
|
||||
return apiRequest<AdminTag>("/api/v1/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
update: (id: number, data: { name?: string; active?: boolean }) =>
|
||||
apiRequest<AdminTag>(`/api/v1/admin/tags/${id}`, {
|
||||
});
|
||||
},
|
||||
update: (id: number, data: { name?: string; active?: boolean }) => {
|
||||
logCrudAction("update", "admin/tags", { id, ...data });
|
||||
return apiRequest<AdminTag>(`/api/v1/admin/tags/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Transform API job to frontend Job format
|
||||
|
|
|
|||
Loading…
Reference in a new issue