Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
97
oudelaa_dashboard/lib/api/admin-users.ts
Normal file
97
oudelaa_dashboard/lib/api/admin-users.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type {
|
||||
AdminCreatePayload,
|
||||
AdminUpdatePayload,
|
||||
ApiRole,
|
||||
ApiUser,
|
||||
PaginatedResponse,
|
||||
ProfileOverviewResponse,
|
||||
SuccessMessage,
|
||||
} from "@/types/api";
|
||||
|
||||
function normalizeUser(user: Partial<ApiUser> & { id?: string }) {
|
||||
return {
|
||||
...user,
|
||||
_id: user._id ?? user.id ?? "",
|
||||
} as ApiUser;
|
||||
}
|
||||
|
||||
function normalizeUsersResponse(response: PaginatedResponse<ApiUser>) {
|
||||
return {
|
||||
...response,
|
||||
items: Array.isArray(response.items) ? response.items.map((item) => normalizeUser(item)) : response.items,
|
||||
data: Array.isArray(response.data) ? response.data.map((item) => normalizeUser(item)) : response.data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listPlatformAdmins(page = 1, limit = 20) {
|
||||
const response = await fetchWithAuth<PaginatedResponse<ApiUser>>(
|
||||
apiEndpoints.users.admins({ page, limit }),
|
||||
);
|
||||
return normalizeUsersResponse(response);
|
||||
}
|
||||
|
||||
export async function getAdminUserById(userId: string) {
|
||||
const response = await fetchWithAuth<ApiUser>(apiEndpoints.users.byId(userId));
|
||||
return normalizeUser(response);
|
||||
}
|
||||
|
||||
export async function updateAdminUser(userId: string, payload: AdminUpdatePayload) {
|
||||
const response = await fetchWithAuth<ApiUser>(apiEndpoints.users.update(userId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return normalizeUser(response);
|
||||
}
|
||||
|
||||
export async function updatePlatformAdmin(userId: string, payload: AdminUpdatePayload) {
|
||||
const response = await fetchWithAuth<ApiUser>(apiEndpoints.users.updateAdmin(userId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return normalizeUser(response);
|
||||
}
|
||||
|
||||
export async function setAdminUserRole(userId: string, role: ApiRole) {
|
||||
const response = await fetchWithAuth<ApiUser>(apiEndpoints.users.setRole(userId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ role }),
|
||||
});
|
||||
return normalizeUser(response);
|
||||
}
|
||||
|
||||
export async function deleteAdminUser(userId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.users.remove(userId), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function deletePlatformAdmin(userId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.users.removeAdmin(userId), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function createAdminUser(payload: AdminCreatePayload) {
|
||||
const response = await fetchWithAuth<ApiUser>(apiEndpoints.users.createAdmin, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return normalizeUser(response);
|
||||
}
|
||||
|
||||
export async function searchAdminUsers(params: Record<string, string | number | boolean | null | undefined>) {
|
||||
const response = await fetchWithAuth<PaginatedResponse<ApiUser>>(apiEndpoints.users.all(params));
|
||||
return normalizeUsersResponse(response);
|
||||
}
|
||||
|
||||
export async function discoverAdminUsers(params: Record<string, string | number | boolean | null | undefined>) {
|
||||
const response = await fetchWithAuth<PaginatedResponse<ApiUser>>(apiEndpoints.users.discover(params));
|
||||
return normalizeUsersResponse(response);
|
||||
}
|
||||
|
||||
export async function searchPlatformAdmins(params: Record<string, string | number | boolean | null | undefined>) {
|
||||
const response = await fetchWithAuth<PaginatedResponse<ApiUser>>(apiEndpoints.users.admins(params));
|
||||
return normalizeUsersResponse(response);
|
||||
}
|
||||
|
||||
export async function getProfileOverviewForSuperAdmin(userId: string) {
|
||||
return fetchWithAuth<ProfileOverviewResponse>(apiEndpoints.users.profileOverview(userId));
|
||||
}
|
||||
7
oudelaa_dashboard/lib/api/audit.ts
Normal file
7
oudelaa_dashboard/lib/api/audit.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { AuditLogsResponse } from "@/types/api";
|
||||
|
||||
export async function listAuditLogs(params: Record<string, string | number | boolean | null | undefined> = {}) {
|
||||
return fetchWithAuth<AuditLogsResponse>(apiEndpoints.audit.logs(params));
|
||||
}
|
||||
34
oudelaa_dashboard/lib/api/auth.ts
Normal file
34
oudelaa_dashboard/lib/api/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { LoginResponse, SessionsResponse, SuccessMessage } from "@/types/api";
|
||||
|
||||
export async function loginDashboardUser(email: string, password: string) {
|
||||
return fetchWithAuth<LoginResponse>(apiEndpoints.auth.login, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshDashboardUser(refreshToken: string) {
|
||||
return fetchWithAuth<LoginResponse>(apiEndpoints.auth.refresh, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function logoutDashboardUser(refreshToken: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.auth.logout, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSuperAdminSessions() {
|
||||
return fetchWithAuth<SessionsResponse>(apiEndpoints.auth.superAdminSessions);
|
||||
}
|
||||
|
||||
export async function revokeSuperAdminSession(sessionId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.auth.revokeSuperAdminSession(sessionId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
15
oudelaa_dashboard/lib/api/comments.ts
Normal file
15
oudelaa_dashboard/lib/api/comments.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { CommentsResponse, SuccessMessage } from "@/types/api";
|
||||
|
||||
export async function listModerationComments(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<CommentsResponse>(apiEndpoints.comments.moderation(params));
|
||||
}
|
||||
|
||||
export async function deleteAdminComment(commentId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.comments.adminDelete(commentId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
36
oudelaa_dashboard/lib/api/core.ts
Normal file
36
oudelaa_dashboard/lib/api/core.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { ApiIdentifier, PaginatedResponse } from "@/types/api";
|
||||
|
||||
export function getEntityId(entity?: ApiIdentifier | null) {
|
||||
if (!entity) return "";
|
||||
return entity._id ?? entity.id ?? "";
|
||||
}
|
||||
|
||||
export function getItems<T>(payload: PaginatedResponse<T> | T[] | undefined | null) {
|
||||
if (!payload) return [] as T[];
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (Array.isArray(payload.items)) return payload.items;
|
||||
if (Array.isArray(payload.data)) return payload.data;
|
||||
return [] as T[];
|
||||
}
|
||||
|
||||
export function getTotal<T>(payload: PaginatedResponse<T> | undefined | null) {
|
||||
if (!payload) return 0;
|
||||
if (typeof payload.pagination?.total === "number") return payload.pagination.total;
|
||||
if (typeof payload.total === "number") return payload.total;
|
||||
return getItems(payload).length;
|
||||
}
|
||||
|
||||
export function getPagination<T>(payload: PaginatedResponse<T> | undefined | null) {
|
||||
return payload?.pagination ?? null;
|
||||
}
|
||||
|
||||
export function toQueryString(params: Record<string, string | number | boolean | null | undefined>) {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === "") return;
|
||||
search.set(key, String(value));
|
||||
});
|
||||
|
||||
return search.toString();
|
||||
}
|
||||
118
oudelaa_dashboard/lib/api/endpoints.ts
Normal file
118
oudelaa_dashboard/lib/api/endpoints.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { toQueryString } from "@/lib/api/core";
|
||||
|
||||
export const apiEndpoints = {
|
||||
health: "/",
|
||||
auth: {
|
||||
login: "/auth/login",
|
||||
refresh: "/auth/refresh",
|
||||
logout: "/auth/logout",
|
||||
superAdminLogin: "/auth/superadmin/login",
|
||||
superAdminRefresh: "/auth/superadmin/refresh",
|
||||
superAdminLogout: "/auth/superadmin/logout",
|
||||
superAdminSessions: "/auth/superadmin/sessions",
|
||||
revokeSuperAdminSession: (sessionId: string) => `/auth/superadmin/sessions/${sessionId}/revoke`,
|
||||
},
|
||||
users: {
|
||||
all: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/users/admin?${toQueryString(params)}`,
|
||||
byId: (userId: string) => `/users/admin/${userId}`,
|
||||
update: (userId: string) => `/users/admin/${userId}`,
|
||||
disable: (userId: string) => `/users/admin/${userId}/disable`,
|
||||
enable: (userId: string) => `/users/admin/${userId}/enable`,
|
||||
remove: (userId: string) => `/users/admin/${userId}`,
|
||||
setRole: (userId: string) => `/users/admin/${userId}/role`,
|
||||
admins: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/users/admin/admins?${toQueryString(params)}`,
|
||||
updateAdmin: (userId: string) => `/users/admin/admins/${userId}`,
|
||||
removeAdmin: (userId: string) => `/users/admin/admins/${userId}`,
|
||||
createAdmin: "/users/admin/create-admin",
|
||||
discover: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/users/admin/discover?${toQueryString(params)}`,
|
||||
profileOverview: (userId: string) => `/users/admin/${userId}/profile-overview`,
|
||||
},
|
||||
posts: {
|
||||
moderation: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/posts/admin/moderation?${toQueryString(params)}`,
|
||||
adminDelete: (postId: string) => `/posts/admin/${postId}`,
|
||||
},
|
||||
comments: {
|
||||
moderation: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/comments/admin?${toQueryString(params)}`,
|
||||
adminDelete: (commentId: string) => `/comments/admin/${commentId}`,
|
||||
},
|
||||
notifications: {
|
||||
superAdmin: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/notifications/superadmin?${toQueryString(params)}`,
|
||||
},
|
||||
reports: {
|
||||
superAdmin: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/reports/superadmin?${toQueryString(params)}`,
|
||||
updateStatus: (reportId: string) => `/reports/superadmin/${reportId}/status`,
|
||||
},
|
||||
marketplace: {
|
||||
home: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/home?${toQueryString(params)}`,
|
||||
shopByAdminId: (adminId: string) => `/marketplace/shops/${adminId}`,
|
||||
adminShopProfile: "/marketplace/admin/shop-profile",
|
||||
adminMyShopProfile: "/marketplace/admin/shop-profile/me",
|
||||
adminCreateRepairShop: "/marketplace/admin/repair-shops",
|
||||
adminUpdateRepairShop: (repairShopId: string) => `/marketplace/admin/repair-shops/${repairShopId}`,
|
||||
adminDeleteRepairShop: (repairShopId: string) => `/marketplace/admin/repair-shops/${repairShopId}`,
|
||||
adminMyRepairShops: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/admin/repair-shops/me?${toQueryString(params)}`,
|
||||
adminCreateInstrument: "/marketplace/admin/instruments",
|
||||
adminUpdateInstrument: (instrumentId: string) => `/marketplace/admin/instruments/${instrumentId}`,
|
||||
adminDeleteInstrument: (instrumentId: string) => `/marketplace/admin/instruments/${instrumentId}`,
|
||||
adminMyInstruments: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/admin/instruments/me?${toQueryString(params)}`,
|
||||
adminCreateListing: "/marketplace/admin/listings",
|
||||
adminUpdateListing: (listingId: string) => `/marketplace/admin/listings/${listingId}`,
|
||||
adminDeleteListing: (listingId: string) => `/marketplace/admin/listings/${listingId}`,
|
||||
adminMyListings: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/admin/listings/me?${toQueryString(params)}`,
|
||||
moderationListings: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/superadmin/listings?${toQueryString(params)}`,
|
||||
superAdminCreateListing: (adminId: string) => `/marketplace/superadmin/admins/${adminId}/listings`,
|
||||
superAdminCreateInstrument: (adminId: string) => `/marketplace/superadmin/admins/${adminId}/instruments`,
|
||||
superAdminCreateRepairShop: (adminId: string) => `/marketplace/superadmin/admins/${adminId}/repair-shops`,
|
||||
superAdminUpdateShopProfile: (adminId: string) => `/marketplace/superadmin/admins/${adminId}/shop-profile`,
|
||||
moderationUpdateListingStatus: (listingId: string) => `/marketplace/superadmin/listings/${listingId}/status`,
|
||||
moderationDeleteListing: (listingId: string) => `/marketplace/superadmin/listings/${listingId}`,
|
||||
moderationRepairShops: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/marketplace/superadmin/repair-shops?${toQueryString(params)}`,
|
||||
moderationUpdateRepairShopStatus: (repairShopId: string) =>
|
||||
`/marketplace/superadmin/repair-shops/${repairShopId}/status`,
|
||||
moderationDeleteRepairShop: (repairShopId: string) => `/marketplace/superadmin/repair-shops/${repairShopId}`,
|
||||
},
|
||||
audit: {
|
||||
logs: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/audit/superadmin/logs?${toQueryString(params)}`,
|
||||
},
|
||||
superadmin: {
|
||||
session: "/superadmin/session",
|
||||
overview: "/superadmin/overview",
|
||||
charts: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/superadmin/charts?${toQueryString(params)}`,
|
||||
recentActivity: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/superadmin/recent-activity?${toQueryString(params)}`,
|
||||
reports: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/superadmin/reports?${toQueryString(params)}`,
|
||||
ops: "/superadmin/ops",
|
||||
cases: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/superadmin/cases?${toQueryString(params)}`,
|
||||
createCase: "/superadmin/cases",
|
||||
caseById: (caseId: string) => `/superadmin/cases/${caseId}`,
|
||||
bulkActions: "/superadmin/bulk-actions",
|
||||
settings: "/superadmin/settings",
|
||||
settingsHistory: (params: Record<string, string | number | boolean | null | undefined> = {}) =>
|
||||
`/superadmin/settings/history?${toQueryString(params)}`,
|
||||
restoreSettingsHistory: (historyId: string) => `/superadmin/settings/history/${historyId}/restore`,
|
||||
updatePostStatus: (postId: string) => `/superadmin/posts/${postId}/status`,
|
||||
deletePost: (postId: string) => `/superadmin/posts/${postId}`,
|
||||
restorePost: (postId: string) => `/superadmin/posts/${postId}/restore`,
|
||||
updateCommentStatus: (commentId: string) => `/superadmin/comments/${commentId}/status`,
|
||||
deleteComment: (commentId: string) => `/superadmin/comments/${commentId}`,
|
||||
restoreComment: (commentId: string) => `/superadmin/comments/${commentId}/restore`,
|
||||
updateUserStatus: (userId: string) => `/superadmin/users/${userId}/status`,
|
||||
},
|
||||
} as const;
|
||||
6
oudelaa_dashboard/lib/api/health.ts
Normal file
6
oudelaa_dashboard/lib/api/health.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
|
||||
export async function getHealth() {
|
||||
return fetchWithAuth<string | Record<string, unknown>>(apiEndpoints.health);
|
||||
}
|
||||
236
oudelaa_dashboard/lib/api/marketplace.ts
Normal file
236
oudelaa_dashboard/lib/api/marketplace.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type {
|
||||
MarketplaceHomeResponse,
|
||||
MarketplaceListing,
|
||||
MarketplaceRepairShop,
|
||||
MarketplaceRepairShopResponse,
|
||||
MarketplaceResponse,
|
||||
MarketplaceShopProfile,
|
||||
SuccessMessage,
|
||||
} from "@/types/api";
|
||||
|
||||
type MarketplaceFormValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| File
|
||||
| null
|
||||
| undefined
|
||||
| Array<string | number | boolean | File>;
|
||||
|
||||
type MarketplaceFormPayload = Record<string, MarketplaceFormValue>;
|
||||
|
||||
function appendFormValue(formData: FormData, key: string, value: MarketplaceFormValue) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((entry) => appendFormValue(formData, key, entry));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof File) {
|
||||
formData.append(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
|
||||
function toMarketplaceFormData(payload: MarketplaceFormPayload) {
|
||||
const formData = new FormData();
|
||||
Object.entries(payload).forEach(([key, value]) => appendFormValue(formData, key, value));
|
||||
return formData;
|
||||
}
|
||||
|
||||
export async function getMarketplaceHome(params: Record<string, string | number | boolean | null | undefined> = {}) {
|
||||
return fetchWithAuth<MarketplaceHomeResponse>(apiEndpoints.marketplace.home(params));
|
||||
}
|
||||
|
||||
export async function getMarketplaceShopByAdminId(adminId: string) {
|
||||
return fetchWithAuth<MarketplaceShopProfile>(apiEndpoints.marketplace.shopByAdminId(adminId));
|
||||
}
|
||||
|
||||
export async function getAdminShopProfile() {
|
||||
return fetchWithAuth<MarketplaceShopProfile>(apiEndpoints.marketplace.adminMyShopProfile);
|
||||
}
|
||||
|
||||
export async function updateAdminShopProfile(payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceShopProfile>(apiEndpoints.marketplace.adminShopProfile, {
|
||||
method: "PATCH",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAdminRepairShop(payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceRepairShop>(apiEndpoints.marketplace.adminCreateRepairShop, {
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminRepairShop(repairShopId: string, payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceRepairShop>(apiEndpoints.marketplace.adminUpdateRepairShop(repairShopId), {
|
||||
method: "PATCH",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAdminRepairShop(repairShopId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.marketplace.adminDeleteRepairShop(repairShopId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAdminRepairShops(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceRepairShopResponse>(apiEndpoints.marketplace.adminMyRepairShops(params));
|
||||
}
|
||||
|
||||
export async function createAdminInstrument(payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.adminCreateInstrument, {
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminInstrument(instrumentId: string, payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.adminUpdateInstrument(instrumentId), {
|
||||
method: "PATCH",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAdminInstrument(instrumentId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.marketplace.adminDeleteInstrument(instrumentId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAdminInstruments(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceResponse>(apiEndpoints.marketplace.adminMyInstruments(params));
|
||||
}
|
||||
|
||||
export async function createAdminListing(payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.adminCreateListing, {
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminListing(listingId: string, payload: MarketplaceFormPayload) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.adminUpdateListing(listingId), {
|
||||
method: "PATCH",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAdminListing(listingId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.marketplace.adminDeleteListing(listingId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAdminListings(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceResponse>(apiEndpoints.marketplace.adminMyListings(params));
|
||||
}
|
||||
|
||||
export async function listModerationListings(params: Record<string, string | number | boolean | null | undefined> = {}) {
|
||||
return fetchWithAuth<MarketplaceResponse>(apiEndpoints.marketplace.moderationListings(params));
|
||||
}
|
||||
|
||||
export async function updateModerationListingStatus(
|
||||
listingId: string,
|
||||
isActive: boolean,
|
||||
reason?: string,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.moderationUpdateListingStatus(listingId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ isActive, reason }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteModerationListing(listingId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.marketplace.moderationDeleteListing(listingId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listModerationRepairShops(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceRepairShopResponse>(apiEndpoints.marketplace.moderationRepairShops(params));
|
||||
}
|
||||
|
||||
export async function updateModerationRepairShopStatus(
|
||||
repairShopId: string,
|
||||
isActive: boolean,
|
||||
reason?: string,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceRepairShop>(
|
||||
apiEndpoints.marketplace.moderationUpdateRepairShopStatus(repairShopId),
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ isActive, reason }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteModerationRepairShop(repairShopId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.marketplace.moderationDeleteRepairShop(repairShopId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMarketplaceListingForSuperAdmin(
|
||||
adminId: string,
|
||||
payload: MarketplaceFormPayload,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.superAdminCreateListing(adminId), {
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMarketplaceInstrumentForSuperAdmin(
|
||||
adminId: string,
|
||||
payload: MarketplaceFormPayload,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceListing>(apiEndpoints.marketplace.superAdminCreateInstrument(adminId), {
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMarketplaceRepairShopForSuperAdmin(
|
||||
adminId: string,
|
||||
payload: MarketplaceFormPayload,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceRepairShop>(
|
||||
apiEndpoints.marketplace.superAdminCreateRepairShop(adminId),
|
||||
{
|
||||
method: "POST",
|
||||
body: toMarketplaceFormData(payload),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateMarketplaceShopProfileForSuperAdmin(
|
||||
adminId: string,
|
||||
payload: MarketplaceFormPayload,
|
||||
) {
|
||||
return fetchWithAuth<MarketplaceShopProfile>(
|
||||
apiEndpoints.marketplace.superAdminUpdateShopProfile(adminId),
|
||||
{
|
||||
method: "PATCH",
|
||||
body: toMarketplaceFormData(payload),
|
||||
},
|
||||
);
|
||||
}
|
||||
9
oudelaa_dashboard/lib/api/notifications.ts
Normal file
9
oudelaa_dashboard/lib/api/notifications.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { NotificationsResponse } from "@/types/api";
|
||||
|
||||
export async function listPlatformNotifications(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<NotificationsResponse>(apiEndpoints.notifications.superAdmin(params));
|
||||
}
|
||||
13
oudelaa_dashboard/lib/api/posts.ts
Normal file
13
oudelaa_dashboard/lib/api/posts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { PostsResponse, SuccessMessage } from "@/types/api";
|
||||
|
||||
export async function listModerationPosts(params: Record<string, string | number | boolean | null | undefined> = {}) {
|
||||
return fetchWithAuth<PostsResponse>(apiEndpoints.posts.moderation(params));
|
||||
}
|
||||
|
||||
export async function deleteAdminPost(postId: string) {
|
||||
return fetchWithAuth<SuccessMessage>(apiEndpoints.posts.adminDelete(postId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
20
oudelaa_dashboard/lib/api/reports.ts
Normal file
20
oudelaa_dashboard/lib/api/reports.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type { PlatformReport, ReportsResponse, ReportStatus } from "@/types/api";
|
||||
|
||||
export async function listPlatformReports(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<ReportsResponse>(apiEndpoints.reports.superAdmin(params));
|
||||
}
|
||||
|
||||
export async function updatePlatformReportStatus(
|
||||
reportId: string,
|
||||
status: ReportStatus,
|
||||
resolutionNote?: string,
|
||||
) {
|
||||
return fetchWithAuth<PlatformReport>(apiEndpoints.reports.updateStatus(reportId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status, resolutionNote }),
|
||||
});
|
||||
}
|
||||
182
oudelaa_dashboard/lib/api/superadmin.ts
Normal file
182
oudelaa_dashboard/lib/api/superadmin.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import { fetchWithAuth } from "@/lib/auth/client";
|
||||
import type {
|
||||
ApiComment,
|
||||
ApiPost,
|
||||
ApiUser,
|
||||
CreateSuperAdminCasePayload,
|
||||
ModerationStatus,
|
||||
SuperAdminCase,
|
||||
SuperAdminCasesResponse,
|
||||
SuperAdminChartsResponse,
|
||||
SuperAdminOpsResponse,
|
||||
SuperAdminOverviewResponse,
|
||||
SuperAdminRecentActivityResponse,
|
||||
SuperAdminReportsResponse,
|
||||
SuperAdminSessionResponse,
|
||||
SuperAdminSettings,
|
||||
SuperAdminSettingsHistoryResponse,
|
||||
SuperAdminSettingsResponse,
|
||||
} from "@/types/api";
|
||||
|
||||
export async function getSuperAdminSession() {
|
||||
return fetchWithAuth<SuperAdminSessionResponse>(apiEndpoints.superadmin.session);
|
||||
}
|
||||
|
||||
export async function getSuperAdminOverview() {
|
||||
return fetchWithAuth<SuperAdminOverviewResponse>(apiEndpoints.superadmin.overview);
|
||||
}
|
||||
|
||||
export async function getSuperAdminCharts(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminChartsResponse>(apiEndpoints.superadmin.charts(params));
|
||||
}
|
||||
|
||||
export async function getSuperAdminRecentActivity(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminRecentActivityResponse>(apiEndpoints.superadmin.recentActivity(params));
|
||||
}
|
||||
|
||||
export async function getSuperAdminReports(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminReportsResponse>(apiEndpoints.superadmin.reports(params));
|
||||
}
|
||||
|
||||
export async function getSuperAdminOps() {
|
||||
return fetchWithAuth<SuperAdminOpsResponse>(apiEndpoints.superadmin.ops);
|
||||
}
|
||||
|
||||
export async function getSuperAdminCases(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminCasesResponse>(apiEndpoints.superadmin.cases(params));
|
||||
}
|
||||
|
||||
export async function createSuperAdminCase(payload: CreateSuperAdminCasePayload) {
|
||||
return fetchWithAuth<SuperAdminCase>(apiEndpoints.superadmin.createCase, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSuperAdminCase(
|
||||
caseId: string,
|
||||
payload: Partial<SuperAdminCase> & { note?: string },
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminCase>(apiEndpoints.superadmin.caseById(caseId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function performSuperAdminBulkAction(payload: {
|
||||
resourceType: string;
|
||||
targetIds: string[];
|
||||
action: string;
|
||||
reason?: string;
|
||||
priority?: string;
|
||||
assignToMe?: boolean;
|
||||
}) {
|
||||
return fetchWithAuth<Record<string, unknown>>(apiEndpoints.superadmin.bulkActions, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSuperAdminSettings() {
|
||||
return fetchWithAuth<SuperAdminSettingsResponse>(apiEndpoints.superadmin.settings);
|
||||
}
|
||||
|
||||
export async function getSuperAdminSettingsHistory(
|
||||
params: Record<string, string | number | boolean | null | undefined> = {},
|
||||
) {
|
||||
return fetchWithAuth<SuperAdminSettingsHistoryResponse>(
|
||||
apiEndpoints.superadmin.settingsHistory(params),
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateSuperAdminSettings(payload: Partial<SuperAdminSettings>) {
|
||||
return fetchWithAuth<SuperAdminSettingsResponse>(apiEndpoints.superadmin.settings, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function restoreSuperAdminSettingsHistory(historyId: string) {
|
||||
return fetchWithAuth<SuperAdminSettingsResponse>(
|
||||
apiEndpoints.superadmin.restoreSettingsHistory(historyId),
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateSuperAdminPostStatus(postId: string, status: ModerationStatus, reason?: string) {
|
||||
return fetchWithAuth<ApiPost>(apiEndpoints.superadmin.updatePostStatus(postId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status, reason }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSuperAdminPost(postId: string) {
|
||||
return fetchWithAuth<Record<string, unknown>>(apiEndpoints.superadmin.deletePost(postId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function restoreSuperAdminPost(postId: string) {
|
||||
return fetchWithAuth<ApiPost>(apiEndpoints.superadmin.restorePost(postId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSuperAdminCommentStatus(
|
||||
commentId: string,
|
||||
status: ModerationStatus,
|
||||
reason?: string,
|
||||
) {
|
||||
return fetchWithAuth<ApiComment>(apiEndpoints.superadmin.updateCommentStatus(commentId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status, reason }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSuperAdminComment(commentId: string) {
|
||||
return fetchWithAuth<Record<string, unknown>>(apiEndpoints.superadmin.deleteComment(commentId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function restoreSuperAdminComment(commentId: string) {
|
||||
return fetchWithAuth<ApiComment>(apiEndpoints.superadmin.restoreComment(commentId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSuperAdminUserStatus(userId: string, isDisabled: boolean, reason?: string) {
|
||||
try {
|
||||
return await fetchWithAuth<ApiUser>(apiEndpoints.superadmin.updateUserStatus(userId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ isDisabled, reason }),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = String(error);
|
||||
if (!message.includes("404")) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
return fetchWithAuth<ApiUser>(apiEndpoints.users.disable(userId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ reason: reason ?? "Disabled by SuperAdmin dashboard" }),
|
||||
});
|
||||
}
|
||||
|
||||
return fetchWithAuth<ApiUser>(apiEndpoints.users.enable(userId), {
|
||||
method: "PATCH",
|
||||
});
|
||||
}
|
||||
}
|
||||
108
oudelaa_dashboard/lib/auth/client.ts
Normal file
108
oudelaa_dashboard/lib/auth/client.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { apiEndpoints } from "@/lib/api/endpoints";
|
||||
import type { LoginResponse } from "@/types/api";
|
||||
|
||||
const API_PREFIX = "/api/proxy";
|
||||
|
||||
function isHardAuthFailure(error: unknown) {
|
||||
const message = String(error);
|
||||
return message.includes("400") || message.includes("401");
|
||||
}
|
||||
|
||||
function extractErrorMessage(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as { message?: unknown; error?: unknown };
|
||||
if (Array.isArray(candidate.message)) {
|
||||
return candidate.message.map((item) => String(item)).join(", ");
|
||||
}
|
||||
if (typeof candidate.message === "string" && candidate.message.trim()) {
|
||||
return candidate.message.trim();
|
||||
}
|
||||
if (typeof candidate.error === "string" && candidate.error.trim()) {
|
||||
return candidate.error.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const isFormDataRequest =
|
||||
typeof FormData !== "undefined" && init?.body instanceof FormData;
|
||||
|
||||
const res = await fetch(`${API_PREFIX}${path}`, {
|
||||
...init,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
...(isFormDataRequest ? {} : { "Content-Type": "application/json" }),
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
let detail = "";
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
const payload = (await res.json()) as unknown;
|
||||
const message = extractErrorMessage(payload);
|
||||
detail = message ? ` - ${message}` : ` - ${JSON.stringify(payload)}`;
|
||||
} else {
|
||||
const text = await res.text();
|
||||
detail = text ? ` - ${text}` : "";
|
||||
}
|
||||
|
||||
throw new Error(`Request failed: ${res.status}${detail}`);
|
||||
}
|
||||
|
||||
if (res.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function loginSuperAdmin(email: string, password: string): Promise<void> {
|
||||
await request<LoginResponse>(apiEndpoints.auth.superAdminLogin, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshSuperAdmin(): Promise<boolean> {
|
||||
try {
|
||||
await request<LoginResponse>(apiEndpoints.auth.superAdminRefresh, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isHardAuthFailure(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logoutSuperAdmin(): Promise<void> {
|
||||
await request<void>(apiEndpoints.auth.superAdminLogout, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchWithAuth<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const makeRequest = async () => request<T>(path, init);
|
||||
|
||||
try {
|
||||
return await makeRequest();
|
||||
} catch (error) {
|
||||
if (String(error).includes("401")) {
|
||||
const refreshed = await refreshSuperAdmin();
|
||||
if (refreshed) {
|
||||
return makeRequest();
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
23
oudelaa_dashboard/lib/auth/jwt.ts
Normal file
23
oudelaa_dashboard/lib/auth/jwt.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export function parseJwtPayload(token: string): Record<string, unknown> | null {
|
||||
const parts = token.split(".");
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
try {
|
||||
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
const decoded =
|
||||
typeof window !== "undefined"
|
||||
? window.atob(normalized)
|
||||
: Buffer.from(normalized, "base64").toString("utf8");
|
||||
return JSON.parse(decoded) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isJwtExpired(token: string, skewSeconds = 15): boolean {
|
||||
const payload = parseJwtPayload(token);
|
||||
const exp = typeof payload?.exp === "number" ? payload.exp : null;
|
||||
if (!exp) return false;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return exp <= now + skewSeconds;
|
||||
}
|
||||
14
oudelaa_dashboard/lib/auth/storage.ts
Normal file
14
oudelaa_dashboard/lib/auth/storage.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { AuthTokens } from "@/types/api";
|
||||
|
||||
export function loadTokens(): AuthTokens | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function saveTokens(tokens: AuthTokens) {
|
||||
void tokens;
|
||||
return;
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
return;
|
||||
}
|
||||
28
oudelaa_dashboard/lib/format.ts
Normal file
28
oudelaa_dashboard/lib/format.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function formatDateTime(value?: string | null) {
|
||||
if (!value) return "-";
|
||||
try {
|
||||
return new Intl.DateTimeFormat("ar-SA", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCurrency(value?: number | null, currency = "SAR") {
|
||||
if (typeof value !== "number") return "-";
|
||||
try {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
} catch {
|
||||
return `${value} ${currency}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCount(value?: number | null) {
|
||||
return typeof value === "number" ? new Intl.NumberFormat("en-US").format(value) : "0";
|
||||
}
|
||||
37
oudelaa_dashboard/lib/media-url.ts
Normal file
37
oudelaa_dashboard/lib/media-url.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? process.env.API_BASE_URL ?? "";
|
||||
|
||||
function getApiOrigin() {
|
||||
try {
|
||||
return new URL(API_BASE_URL).origin;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMediaUrl(value?: string | null) {
|
||||
const url = value?.trim() ?? "";
|
||||
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (
|
||||
/^[a-z][a-z0-9+.-]*:\/\//i.test(url) ||
|
||||
url.startsWith("//") ||
|
||||
url.startsWith("data:") ||
|
||||
url.startsWith("blob:")
|
||||
) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const origin = getApiOrigin();
|
||||
if (!origin) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url.startsWith("/")) {
|
||||
return `${origin}${url}`;
|
||||
}
|
||||
|
||||
return `${origin}/${url.replace(/^\/+/, "")}`;
|
||||
}
|
||||
34
oudelaa_dashboard/lib/mock/analytics.ts
Normal file
34
oudelaa_dashboard/lib/mock/analytics.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Insight, SettingsNotifications, SettingsProfile } from "@/types";
|
||||
|
||||
export const retentionCurve: Insight[] = [
|
||||
{ label: "الأسبوع 1", value: 100 },
|
||||
{ label: "الأسبوع 2", value: 84 },
|
||||
{ label: "الأسبوع 3", value: 73 },
|
||||
{ label: "الأسبوع 4", value: 64 },
|
||||
{ label: "الأسبوع 5", value: 58 },
|
||||
{ label: "الأسبوع 6", value: 53 },
|
||||
];
|
||||
|
||||
export const cityPerformance: Insight[] = [
|
||||
{ label: "الرياض", value: 34 },
|
||||
{ label: "جدة", value: 22 },
|
||||
{ label: "الدمام", value: 15 },
|
||||
{ label: "المدينة", value: 11 },
|
||||
{ label: "مكة", value: 9 },
|
||||
{ label: "أخرى", value: 9 },
|
||||
];
|
||||
|
||||
export const profileDefaults: SettingsProfile = {
|
||||
businessName: "Oudelaa Music Experience",
|
||||
supportEmail: "support@oudelaa.com",
|
||||
supportPhone: "+966 50 111 2233",
|
||||
city: "Riyadh",
|
||||
currency: "SAR",
|
||||
};
|
||||
|
||||
export const notificationDefaults: SettingsNotifications = {
|
||||
orders: true,
|
||||
newUsers: true,
|
||||
payouts: false,
|
||||
weeklyDigest: true,
|
||||
};
|
||||
9
oudelaa_dashboard/lib/mock/content.ts
Normal file
9
oudelaa_dashboard/lib/mock/content.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ContentItem } from "@/types";
|
||||
|
||||
export const contentLibrary: ContentItem[] = [
|
||||
{ id: "CNT-201", title: "موال السهرة", artist: "فرقة وتر الحجاز", genre: "طرب", duration: "06:12", status: "published", createdAt: "2026-03-10", listens: 12890 },
|
||||
{ id: "CNT-202", title: "ليالي العود", artist: "نواف الدوسري", genre: "Instrumental", duration: "04:48", status: "review", createdAt: "2026-03-26", listens: 7420 },
|
||||
{ id: "CNT-203", title: "نبض المرسى", artist: "سدن", genre: "Fusion", duration: "03:55", status: "draft", createdAt: "2026-04-01", listens: 0 },
|
||||
{ id: "CNT-204", title: "رحلة مقام", artist: "أمجاد", genre: "Classical", duration: "05:24", status: "published", createdAt: "2026-02-15", listens: 15430 },
|
||||
{ id: "CNT-205", title: "نخل الجنوب", artist: "علي الشهري", genre: "Folk", duration: "04:02", status: "published", createdAt: "2026-03-29", listens: 5120 },
|
||||
];
|
||||
36
oudelaa_dashboard/lib/mock/dashboard.ts
Normal file
36
oudelaa_dashboard/lib/mock/dashboard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { ChannelPoint, RevenuePoint, StatMetric } from "@/types";
|
||||
|
||||
export const dashboardStats: StatMetric[] = [
|
||||
{ id: "gmv", label: "إجمالي الإيراد", value: "1,284,920 ر.س", delta: "+18.6%", trend: "up", note: "مقارنة بـ مارس" },
|
||||
{ id: "orders", label: "الطلبات المكتملة", value: "4,912", delta: "+11.2%", trend: "up", note: "آخر 30 يوم" },
|
||||
{ id: "aov", label: "متوسط قيمة الطلب", value: "261 ر.س", delta: "-2.4%", trend: "down", note: "يحتاج تحسين سلة الشراء" },
|
||||
{ id: "retention", label: "الاحتفاظ بالعملاء", value: "82.1%", delta: "+3.1%", trend: "up", note: "العملاء العائدون" },
|
||||
];
|
||||
|
||||
export const revenueSeries: RevenuePoint[] = [
|
||||
{ month: "يناير", revenue: 780000, orders: 2900 },
|
||||
{ month: "فبراير", revenue: 842000, orders: 3180 },
|
||||
{ month: "مارس", revenue: 906000, orders: 3375 },
|
||||
{ month: "أبريل", revenue: 1002000, orders: 3710 },
|
||||
{ month: "مايو", revenue: 1093000, orders: 3980 },
|
||||
{ month: "يونيو", revenue: 1160000, orders: 4250 },
|
||||
{ month: "يوليو", revenue: 1284920, orders: 4912 },
|
||||
];
|
||||
|
||||
export const channelShare: ChannelPoint[] = [
|
||||
{ name: "التطبيق", value: 46 },
|
||||
{ name: "الموقع", value: 33 },
|
||||
{ name: "واتساب", value: 14 },
|
||||
{ name: "شركاء", value: 7 },
|
||||
];
|
||||
|
||||
export const heatmapHours = [
|
||||
{ slot: "09:00", score: 20 },
|
||||
{ slot: "11:00", score: 42 },
|
||||
{ slot: "13:00", score: 54 },
|
||||
{ slot: "15:00", score: 79 },
|
||||
{ slot: "17:00", score: 92 },
|
||||
{ slot: "19:00", score: 68 },
|
||||
{ slot: "21:00", score: 49 },
|
||||
{ slot: "23:00", score: 30 },
|
||||
];
|
||||
6
oudelaa_dashboard/lib/mock/index.ts
Normal file
6
oudelaa_dashboard/lib/mock/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./dashboard";
|
||||
export * from "./users";
|
||||
export * from "./content";
|
||||
export * from "./orders";
|
||||
export * from "./messages";
|
||||
export * from "./analytics";
|
||||
8
oudelaa_dashboard/lib/mock/messages.ts
Normal file
8
oudelaa_dashboard/lib/mock/messages.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { MessageThread } from "@/types";
|
||||
|
||||
export const messageThreads: MessageThread[] = [
|
||||
{ id: "MSG-881", customer: "رؤى السالم", subject: "تحديث الاشتراك", state: "unread", lastMessageAt: "2026-04-06 11:03", channel: "WhatsApp", preview: "أحتاج ترقية من Silver إلى Gold مع حفظ المكتبة الحالية.", priority: "high" },
|
||||
{ id: "MSG-882", customer: "حسين العمري", subject: "مشكلة في الدفع", state: "open", lastMessageAt: "2026-04-06 09:54", channel: "Email", preview: "تم خصم المبلغ ولم يظهر الطلب في لوحة الطلبات.", priority: "high" },
|
||||
{ id: "MSG-883", customer: "سارة البقمي", subject: "طلب تعاون فني", state: "open", lastMessageAt: "2026-04-05 23:15", channel: "In-App", preview: "هل يمكن إدراج أعمالي ضمن قسم المواهب الجديدة؟", priority: "normal" },
|
||||
{ id: "MSG-884", customer: "عبدالاله الدوسري", subject: "استفسار باقة Maestro", state: "closed", lastMessageAt: "2026-04-04 20:01", channel: "Email", preview: "تم الرد وإغلاق التذكرة مع مشاركة العرض السعري.", priority: "low" },
|
||||
];
|
||||
10
oudelaa_dashboard/lib/mock/orders.ts
Normal file
10
oudelaa_dashboard/lib/mock/orders.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Order } from "@/types";
|
||||
|
||||
export const orders: Order[] = [
|
||||
{ id: "ORD-7781", customer: "مشاعل الكندري", product: "باقة Oudelaa Gold", amount: 420, paymentMethod: "Card", status: "paid", createdAt: "2026-04-06 10:42", city: "الرياض" },
|
||||
{ id: "ORD-7780", customer: "فهد العلي", product: "جلسة Maestro Live", amount: 960, paymentMethod: "Apple Pay", status: "processing", createdAt: "2026-04-06 09:27", city: "جدة" },
|
||||
{ id: "ORD-7779", customer: "أروى الحازمي", product: "باقة Silver", amount: 190, paymentMethod: "Wallet", status: "paid", createdAt: "2026-04-06 08:15", city: "الخبر" },
|
||||
{ id: "ORD-7778", customer: "محمد اليامي", product: "إهداء مقطوعة", amount: 300, paymentMethod: "Card", status: "refunded", createdAt: "2026-04-05 22:50", city: "أبها" },
|
||||
{ id: "ORD-7777", customer: "غلا السبيعي", product: "اشتراك سنوي", amount: 1880, paymentMethod: "Card", status: "failed", createdAt: "2026-04-05 20:33", city: "الدمام" },
|
||||
{ id: "ORD-7776", customer: "بدر الجهني", product: "باقة Gold", amount: 520, paymentMethod: "Apple Pay", status: "processing", createdAt: "2026-04-05 18:02", city: "الرياض" },
|
||||
];
|
||||
10
oudelaa_dashboard/lib/mock/users.ts
Normal file
10
oudelaa_dashboard/lib/mock/users.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { User } from "@/types";
|
||||
|
||||
export const users: User[] = [
|
||||
{ id: "USR-1048", fullName: "ريم الحربي", email: "reem@oudelaa.sa", plan: "Maestro", status: "active", joinedAt: "2026-02-13", totalSpent: 12680, city: "الرياض" },
|
||||
{ id: "USR-1049", fullName: "سلمان العتيبي", email: "salman@oudelaa.sa", plan: "Gold", status: "active", joinedAt: "2026-01-29", totalSpent: 7480, city: "جدة" },
|
||||
{ id: "USR-1050", fullName: "نور البلوشي", email: "nour@oudelaa.sa", plan: "Silver", status: "pending", joinedAt: "2026-03-03", totalSpent: 980, city: "الدمام" },
|
||||
{ id: "USR-1051", fullName: "عبدالرحمن الزهراني", email: "rahman@oudelaa.sa", plan: "Gold", status: "suspended", joinedAt: "2025-11-17", totalSpent: 5320, city: "الخبر" },
|
||||
{ id: "USR-1052", fullName: "لولوة المطيري", email: "lolwah@oudelaa.sa", plan: "Maestro", status: "active", joinedAt: "2026-03-19", totalSpent: 15400, city: "المدينة" },
|
||||
{ id: "USR-1053", fullName: "زياد القحطاني", email: "ziad@oudelaa.sa", plan: "Silver", status: "active", joinedAt: "2026-02-22", totalSpent: 2280, city: "الطائف" },
|
||||
];
|
||||
107
oudelaa_dashboard/lib/navigation.ts
Normal file
107
oudelaa_dashboard/lib/navigation.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
Bell,
|
||||
ChartColumn,
|
||||
Flag,
|
||||
LayoutDashboard,
|
||||
MessageSquareMore,
|
||||
PackageSearch,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
ShoppingBag,
|
||||
SquareKanban,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { PermissionMatchMode } from "@/lib/permissions";
|
||||
import { SUPERADMIN_PERMISSIONS } from "@/lib/permissions";
|
||||
|
||||
export type DashboardNavItem = {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
permissionMode?: PermissionMatchMode;
|
||||
requiredPermissions: readonly string[];
|
||||
};
|
||||
|
||||
export const dashboardNav = [
|
||||
{
|
||||
href: "/dashboard",
|
||||
label: "Dashboard",
|
||||
icon: LayoutDashboard,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.OVERVIEW_READ],
|
||||
},
|
||||
{
|
||||
href: "/users",
|
||||
label: "Users",
|
||||
icon: Users,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.USERS_READ],
|
||||
},
|
||||
{
|
||||
href: "/analytics",
|
||||
label: "Analytics",
|
||||
icon: ChartColumn,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.ANALYTICS_READ],
|
||||
},
|
||||
{
|
||||
href: "/content",
|
||||
label: "Content",
|
||||
icon: SquareKanban,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.CONTENT_MODERATE],
|
||||
},
|
||||
{
|
||||
href: "/reports",
|
||||
label: "Reports",
|
||||
icon: Flag,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.CONTENT_MODERATE],
|
||||
},
|
||||
{
|
||||
href: "/marketplace",
|
||||
label: "Marketplace",
|
||||
icon: ShoppingBag,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.MARKETPLACE_MANAGE],
|
||||
},
|
||||
{
|
||||
href: "/notifications",
|
||||
label: "Notifications",
|
||||
icon: Bell,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.NOTIFICATIONS_READ],
|
||||
},
|
||||
{
|
||||
href: "/messages",
|
||||
label: "Engagement",
|
||||
icon: MessageSquareMore,
|
||||
permissionMode: "any",
|
||||
requiredPermissions: [
|
||||
SUPERADMIN_PERMISSIONS.NOTIFICATIONS_READ,
|
||||
SUPERADMIN_PERMISSIONS.CONTENT_MODERATE,
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/orders",
|
||||
label: "Operations",
|
||||
icon: PackageSearch,
|
||||
permissionMode: "any",
|
||||
requiredPermissions: [
|
||||
SUPERADMIN_PERMISSIONS.CASES_MANAGE,
|
||||
SUPERADMIN_PERMISSIONS.OPS_READ,
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/security",
|
||||
label: "Security",
|
||||
icon: ShieldCheck,
|
||||
permissionMode: "any",
|
||||
requiredPermissions: [
|
||||
SUPERADMIN_PERMISSIONS.SESSIONS_MANAGE,
|
||||
SUPERADMIN_PERMISSIONS.AUDIT_READ,
|
||||
SUPERADMIN_PERMISSIONS.OPS_READ,
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/settings",
|
||||
label: "Settings",
|
||||
icon: Settings,
|
||||
requiredPermissions: [SUPERADMIN_PERMISSIONS.SETTINGS_READ],
|
||||
},
|
||||
] satisfies readonly DashboardNavItem[];
|
||||
37
oudelaa_dashboard/lib/permissions.ts
Normal file
37
oudelaa_dashboard/lib/permissions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export const SUPERADMIN_PERMISSIONS = {
|
||||
OVERVIEW_READ: "overview.read",
|
||||
ANALYTICS_READ: "analytics.read",
|
||||
USERS_READ: "users.read",
|
||||
USERS_MANAGE: "users.manage",
|
||||
CONTENT_MODERATE: "content.moderate",
|
||||
MARKETPLACE_MANAGE: "marketplace.manage",
|
||||
NOTIFICATIONS_READ: "notifications.read",
|
||||
AUDIT_READ: "audit.read",
|
||||
SETTINGS_READ: "settings.read",
|
||||
SETTINGS_WRITE: "settings.write",
|
||||
SESSIONS_MANAGE: "sessions.manage",
|
||||
OPS_READ: "ops.read",
|
||||
CASES_MANAGE: "cases.manage",
|
||||
} as const;
|
||||
|
||||
export type PermissionMatchMode = "all" | "any";
|
||||
|
||||
export function hasPermission(permissions: string[] | undefined, permission: string) {
|
||||
return (permissions ?? []).includes(permission);
|
||||
}
|
||||
|
||||
export function matchesPermissions(
|
||||
permissions: string[] | undefined,
|
||||
requiredPermissions: readonly string[],
|
||||
mode: PermissionMatchMode = "all",
|
||||
) {
|
||||
if (!requiredPermissions.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode === "any") {
|
||||
return requiredPermissions.some((permission) => hasPermission(permissions, permission));
|
||||
}
|
||||
|
||||
return requiredPermissions.every((permission) => hasPermission(permissions, permission));
|
||||
}
|
||||
73
oudelaa_dashboard/lib/post-utils.ts
Normal file
73
oudelaa_dashboard/lib/post-utils.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { resolveMediaUrl } from "@/lib/media-url";
|
||||
import type { ApiComment, ApiPost, ApiUser } from "@/types/api";
|
||||
|
||||
function asUser(value: ApiUser | string | undefined | null) {
|
||||
if (!value || typeof value === "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getPostAuthor(post: ApiPost) {
|
||||
return asUser(post.author ?? post.authorId);
|
||||
}
|
||||
|
||||
export function getCommentAuthor(comment: ApiComment) {
|
||||
return asUser(comment.author ?? comment.authorId);
|
||||
}
|
||||
|
||||
export function getUserLabel(user: ApiUser | null) {
|
||||
if (!user) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return user.name ?? user.stageName ?? user.username ?? user.email ?? "-";
|
||||
}
|
||||
|
||||
export function getPostPreviewMedia(post: ApiPost) {
|
||||
const imageCount = post.imageUrls?.length ?? 0;
|
||||
const imageUrl = resolveMediaUrl(post.imageUrls?.[0]);
|
||||
const thumbnailUrl = resolveMediaUrl(post.thumbnailUrl);
|
||||
const videoUrl = resolveMediaUrl(post.videoUrl);
|
||||
const audioUrl = resolveMediaUrl(post.audioUrl);
|
||||
|
||||
if (imageCount > 0 && imageUrl) {
|
||||
return {
|
||||
kind: "image" as const,
|
||||
url: imageUrl,
|
||||
sourceUrl: "",
|
||||
count: imageCount,
|
||||
};
|
||||
}
|
||||
|
||||
if (thumbnailUrl) {
|
||||
return {
|
||||
kind: post.postType === "audio" ? ("audio" as const) : ("video" as const),
|
||||
url: thumbnailUrl,
|
||||
sourceUrl: post.postType === "video" ? videoUrl : audioUrl,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (post.postType === "video" && videoUrl) {
|
||||
return {
|
||||
kind: "video" as const,
|
||||
url: "",
|
||||
sourceUrl: videoUrl,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind:
|
||||
post.postType === "audio"
|
||||
? ("audio" as const)
|
||||
: post.postType === "video"
|
||||
? ("video" as const)
|
||||
: ("text" as const),
|
||||
url: "",
|
||||
sourceUrl: post.postType === "video" ? videoUrl : post.postType === "audio" ? audioUrl : "",
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
6
oudelaa_dashboard/lib/utils.ts
Normal file
6
oudelaa_dashboard/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم