Add CRUD logging and stabilize dashboard dates

This commit is contained in:
Tiago Yamamoto 2025-12-22 18:55:43 -03:00
parent fffe732776
commit 5e99115df6
5 changed files with 91 additions and 32 deletions

View file

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

View file

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

View file

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

View file

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

View file

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