Initial commit for new panel

This commit is contained in:
AmirReza Jamali
2025-09-09 11:20:26 +03:30
commit 1d4ccb3575
343 changed files with 20031 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import {
requestErrorHandler,
requestInterceptor,
} from "@/middleware/request-interceptor";
import {
responseErrorHandler,
responseInterceptor,
} from "@/middleware/response-interceptor";
import axios from "axios";
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
api.interceptors.request.use(requestInterceptor, requestErrorHandler);
api.interceptors.response.use(responseInterceptor, responseErrorHandler);
export default api;
+38
View File
@@ -0,0 +1,38 @@
export const API_ENDPOINTS = {
POSTS: {
READ_ALL: "posts ",
},
USER: {
LOGIN: "profile/login",
LOGOUT: "profile/logout",
REFRESH_TOKEN: "profile/refresh-token",
ADMINS: "users",
ADMIN_DETAILS: (id: string) => `users/${id}`,
},
PROFILE: {
READ: "profile",
},
COMPANIES: {
READ_ALL: "companies",
CREATE: "companies",
UPDATE: (id: string) => `companies/${id}`,
DELETE: (id: string) => `companies/${id}`,
DETAILS: (id: string) => `companies/${id}`,
},
TENDERS: {
READ_ALL: "tenders",
CREATE: "tenders",
UPDATE: (id: string) => `tenders/${id}`,
DELETE: (id: string) => `tenders/${id}`,
DETAILS: (id: string) => `tenders/${id}`,
},
CUSTOMERS: {
READ_ALL: "customers",
CREATE: "customers",
UPDATE: (id: string) => `customers/${id}`,
DELETE: (id: string) => `customers/${id}`,
DETAILS: (id: string) => `customers/${id}`,
ASSIGN_COMPANY_TO_CUSTOMER: (id: string) =>
`customers/${id}/companies/assign`,
},
} as const;
+6
View File
@@ -0,0 +1,6 @@
export { default as api } from "./axios";
export * from "./endpoints";
export * from "./types";
export * from "./services";
export { default } from "./axios";
+60
View File
@@ -0,0 +1,60 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import {
ApiResponse,
CompaniesListResponseSchema,
TCompaniesResponse,
} from "../types";
import { ICreateCompanyCredentials } from "../types";
export const companiesService = {
getCompanies: async (
params?: Record<string, any>,
): Promise<ApiResponse<TCompaniesResponse>> => {
try {
const response = await api.get(API_ENDPOINTS.COMPANIES.READ_ALL, {
params,
});
return CompaniesListResponseSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in Companies Services Read all:", error);
throw error;
}
},
createCompany: async (credentials: ICreateCompanyCredentials) => {
const response = await api.post(
API_ENDPOINTS.COMPANIES.CREATE,
credentials,
);
return response.data;
},
updateCompany: async ({
id,
credentials,
}: {
id: string;
credentials: ICreateCompanyCredentials;
}) => {
const response = await api.put(
API_ENDPOINTS.COMPANIES.UPDATE(id),
credentials,
);
return response.data;
},
companyDetails: async (id: string) => {
const response = await api.get(API_ENDPOINTS.COMPANIES.DETAILS(id));
return response.data;
},
deleteCompany: async (id: string) => {
try {
return (await api.delete(API_ENDPOINTS.COMPANIES.DELETE(id))).data;
} catch (error) {
console.error("ERROR caught in Companies Services Delete:", error);
throw error;
}
},
};
+74
View File
@@ -0,0 +1,74 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import {
CreateCustomerCredentials,
CustomerListResponseSchema,
TAssignCustomerToCompanyCredentials,
} from "../types/Customers";
export const customersService = {
getCustomers: async (params?: Record<string, any>) => {
try {
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
params,
});
return CustomerListResponseSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in Customers Services Read all:", error);
throw error;
}
},
deleteCustomer: async (id: string) => {
try {
return (await api.delete(API_ENDPOINTS.CUSTOMERS.DELETE(id))).data;
} catch (error) {
console.error("ERROR caught in Customers Services Delete:", error);
throw error;
}
},
createCustomer: async (credentials: CreateCustomerCredentials) => {
try {
return (await api.post(API_ENDPOINTS.CUSTOMERS.CREATE, credentials)).data;
} catch (error) {
console.error("ERROR caught in Customers Services Create:", error);
throw error;
}
},
updateCustomer: async ({
id,
credentials,
}: {
id: string;
credentials: CreateCustomerCredentials;
}) => {
try {
return (await api.put(API_ENDPOINTS.CUSTOMERS.UPDATE(id), credentials))
.data;
} catch (error) {
console.error("ERROR caught in Customers Services Update:", error);
throw error;
}
},
assignCompanyToCustomer: async ({
credentials,
id,
}: {
id: string;
credentials: TAssignCustomerToCompanyCredentials;
}): Promise<any> => {
try {
return (
await api.post(
API_ENDPOINTS.CUSTOMERS.ASSIGN_COMPANY_TO_CUSTOMER(id),
credentials,
)
).data;
} catch (error) {
console.error(
"ERROR caught in Customers Service => Assign company to customer",
error,
);
throw error;
}
},
};
+4
View File
@@ -0,0 +1,4 @@
export * from "./user-services";
export * from "./companies-service";
export * from "./tenders-service";
export * from "./profile-service";
+15
View File
@@ -0,0 +1,15 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import { CurrentUserProfileSchema } from "../types";
export const profileService = {
getProfile: async () => {
try {
const response = (await api.get(API_ENDPOINTS.PROFILE.READ)).data;
return CurrentUserProfileSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in Profile Service: Get Profile", error);
throw error;
}
},
};
+17
View File
@@ -0,0 +1,17 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import { ApiResponse } from "../types";
import { TTenderResponse } from "../types/Tenders";
export const tendersService = {
tendersList: async (
params?: Record<string, any>,
): Promise<ApiResponse<TTenderResponse>> => {
try {
return (await api.get(API_ENDPOINTS.TENDERS.READ_ALL, { params })).data;
} catch (error) {
console.error("ERROR caught in Tenders Services Read all:", error);
throw error;
}
},
};
+91
View File
@@ -0,0 +1,91 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import {
AdminListResponseSchema,
ApiResponse,
IAdminListResponse,
ICreateAdminCredentials,
ILoginCredentials,
ILoginResponse,
ILogoutResponse,
LoginResponseSchema,
LogoutResponseSchema,
} from "../types";
export const userService = {
login: async (
credentials: ILoginCredentials
): Promise<ApiResponse<ILoginResponse>> => {
try {
const response = await api.post(API_ENDPOINTS.USER.LOGIN, credentials);
return LoginResponseSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in User Service Login:", error);
throw error;
}
},
logout: async (): Promise<ApiResponse<ILogoutResponse>> => {
try {
const response = await api.delete(API_ENDPOINTS.USER.LOGOUT);
return LogoutResponseSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in User Service Logout", error);
throw error;
}
},
adminsList: async (
params?: Record<string, any>
): Promise<ApiResponse<IAdminListResponse>> => {
try {
const response = await api.get(API_ENDPOINTS.USER.ADMINS, { params });
return AdminListResponseSchema.parse(response.data);
} catch (error) {
console.error("ERROR caught in User Service User List", error);
throw error;
}
},
createAdmin: async (credentials: ICreateAdminCredentials) => {
try {
return (await api.post(API_ENDPOINTS.USER.ADMINS, credentials)).data
.message;
} catch (error) {
console.error("ERROR caught in User Service Create Admin", error);
throw error;
}
},
updateAdmin: async ({
id,
credentials,
}: {
id: string;
credentials: ICreateAdminCredentials;
}) => {
try {
return (await api.put(API_ENDPOINTS.USER.ADMIN_DETAILS(id), credentials))
.data.message;
} catch (error) {
console.error("ERROR caught in User Service Update Admin", error);
throw error;
}
},
adminDetails: async (id: string) => {
try {
return (await api.get(API_ENDPOINTS.USER.ADMIN_DETAILS(id))).data;
} catch (error) {
console.error("ERROR caught in User Service Admin Details", error);
throw error;
}
},
deleteAdmin: async (
id: string
): Promise<ApiResponse<{ message: string }>> => {
try {
return await api.delete(`${API_ENDPOINTS.USER.ADMINS}/${id}`);
} catch (error) {
console.error("ERROR caught in User Service Delete Admin", error);
throw error;
}
},
};
+108
View File
@@ -0,0 +1,108 @@
import { z } from "zod";
import { createApiResponseSchema } from "./Factory";
export interface IAddress {
street: string;
city: string;
state: string;
postal_code: string;
country: string;
}
export interface IContactPerson {
full_name: string;
first_name: string;
last_name: string;
email: string;
phone: string;
mobile: string;
position: string;
is_primary: boolean;
}
export interface ITags {
keywords: string[];
categories: string[];
specializations: string[];
certifications: string[];
cpv_codes: string[];
}
export interface ICreateCompanyCredentials {
name: string;
email: string;
phone: string;
website: string;
type: string;
description: string;
founded_year: number;
employee_count: number;
annual_revenue: number;
currency: string;
industry: string;
language: string;
tax_id: string;
registration_number: string;
timezone: string;
address: IAddress;
contact_person: IContactPerson;
tags: ITags;
}
export const companySchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().optional(),
phone: z.string().optional(),
website: z.string(),
type: z.string(),
description: z.string(),
founded_year: z.number(),
employee_count: z.number(),
annual_revenue: z.number(),
currency: z.string(),
industry: z.string(),
language: z.string(),
tax_id: z.string(),
registration_number: z.string(),
timezone: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
}),
contact_person: z.object({
full_name: z.string(),
first_name: z.string(),
last_name: z.string(),
email: z.string().email(),
phone: z.string(),
mobile: z.string(),
position: z.string(),
is_primary: z.boolean(),
}),
tags: z.object({
keywords: z.array(z.string()).optional(),
categories: z.array(z.string()).optional(),
specializations: z.array(z.string()).optional(),
certifications: z.array(z.string()).optional(),
cpv_codes: z.array(z.string()).optional(),
}),
});
export const companiesResponseSchema = z.object({
companies: z.array(companySchema),
offset: z.number(),
limit: z.number(),
total: z.number(),
});
export const companyDetailsApiResponseSchema =
createApiResponseSchema(companySchema);
export const CompaniesListResponseSchema = createApiResponseSchema(
companiesResponseSchema
);
export type TCompany = z.infer<typeof companySchema>;
export type TCompaniesResponse = z.infer<typeof companiesResponseSchema>;
+114
View File
@@ -0,0 +1,114 @@
import { z } from "zod";
import { createApiResponseSchema } from "./Factory";
const customersSchema = z.object({
companies: z
.array(
z.object({
id: z.string(),
name: z.string(),
}),
)
.or(z.null())
.optional(),
compliance_notes: z.any(),
created_at: z.number(),
email: z.string().email(),
full_name: z.string(),
id: z.string(),
phone: z.string(),
status: z.string(),
type: z.string(),
updated_at: z.number(),
updated_by: z.any(),
username: z.string(),
});
export const customersListResponse = z.object({
customers: z.array(customersSchema),
});
const createCustomerCredentials = z.object({
address: z
.object({
address_type: z.string(),
city: z.string(),
country: z.string(),
is_default: z.boolean(),
postal_code: z.string(),
state: z.string(),
street: z.string(),
})
.optional(),
annual_revenue: z.number().min(0).max(999999999999).optional(),
business_type: z.string().min(2).max(100).optional(),
company_ids: z.array(z.string()).optional(),
contact_person: z
.object({
email: z.string(),
first_name: z.string(),
full_name: z.string(),
is_primary: z.boolean(),
last_name: z.string(),
mobile: z.string(),
phone: z.string(),
position: z.string(),
})
.optional(),
currency: z
.enum([
"USD",
"EUR",
"GBP",
"JPY",
"CAD",
"AUD",
"CHF",
"CNY",
"INR",
"BRL",
])
.optional(),
email: z.string().email(),
employee_count: z.number().int().min(1).max(100000).optional(),
first_name: z.string().min(2).max(50).optional(),
founded_year: z.number().int().min(1800).max(2100).optional(),
full_name: z.string().min(2).max(100).optional(),
industry: z.string().min(2).max(100).optional(),
language: z.enum(["en", "ar", "fr", "es", "de", "zh", "ja", "ko"]).optional(),
last_name: z.string().min(2).max(50).optional(),
mobile: z.string().min(10).max(20).optional(),
password: z.string().min(8).max(128),
phone: z.string().min(10).max(20).optional(),
registration_number: z.string().min(5).max(50).optional(),
tax_id: z.string().min(5).max(50).optional(),
timezone: z.string().min(3).max(50).optional(),
type: z.string(),
username: z
.string()
.min(3)
.max(30)
.regex(
/^[a-zA-Z0-9]+$/,
"Username must contain only alphanumeric characters",
),
});
const assignCustomerToCompanySchema = z.object({
company_ids: z.array(z.string()),
});
export const CustomerListResponseSchema = createApiResponseSchema(
customersListResponse,
);
export type TAssignCustomerToCompanyCredentials = z.infer<
typeof assignCustomerToCompanySchema
>;
export type TCustomer = z.infer<typeof customersSchema>;
export type CreateCustomerCredentials = z.infer<
typeof createCustomerCredentials
>;
export type TCustomersListResponse = z.infer<typeof customersListResponse>;
+26
View File
@@ -0,0 +1,26 @@
import { z } from "zod";
const MetaDataSchema = z.object({
limit: z.number(),
offset: z.number(),
page: z.number(),
pages: z.number(),
total: z.number(),
});
export function createApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
return z.object({
data: dataSchema,
meta: z
.object({
limit: z.number(),
offset: z.number(),
page: z.number(),
pages: z.number(),
total: z.number(),
})
.optional(),
message: z.string(),
code: z.number().optional(),
success: z.boolean(),
});
}
export type IMetaData = z.infer<typeof MetaDataSchema>;
+10
View File
@@ -0,0 +1,10 @@
import { z } from "zod";
export const IPostSchema = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
body: z.string(),
});
export type IPost = z.infer<typeof IPostSchema>;
+21
View File
@@ -0,0 +1,21 @@
import { z } from "zod";
export const CurrentUserProfileSchema = z.object({
company_id: z.string().optional(),
created_at: z.number().optional(),
created_by: z.string().optional(),
department: z.string().optional(),
email: z.string().email().optional(),
full_name: z.string().optional(),
id: z.string().optional(),
is_verified: z.boolean().optional(),
last_login_at: z.number().optional(),
phone: z.string().optional(),
position: z.string().optional(),
profile_image: z.string().optional(),
role: z.string().optional(),
status: z.string().or(z.number()).optional(),
updated_at: z.string().or(z.number()).optional(),
updated_by: z.string().optional(),
username: z.string().optional(),
});
export type TCurrentUser = z.infer<typeof CurrentUserProfileSchema>;
+107
View File
@@ -0,0 +1,107 @@
import { z } from "zod";
import { createApiResponseSchema } from "./Factory";
export interface IAddress {
street: string;
city: string;
state: string;
postal_code: string;
country: string;
}
export interface IContactPerson {
full_name: string;
first_name: string;
last_name: string;
email: string;
phone: string;
mobile: string;
position: string;
is_primary: boolean;
}
export interface ITags {
keywords: string[];
categories: string[];
specializations: string[];
certifications: string[];
cpv_codes: string[];
}
export interface ICreateCompanyCredentials {
name: string;
email: string;
phone: string;
website: string;
type: string;
description: string;
founded_year: number;
employee_count: number;
annual_revenue: number;
currency: string;
industry: string;
language: string;
tax_id: string;
registration_number: string;
timezone: string;
address: IAddress;
contact_person?: IContactPerson;
tags: ITags;
}
export const companySchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().optional(),
phone: z.string().optional(),
website: z.string(),
type: z.string(),
description: z.string(),
founded_year: z.number(),
employee_count: z.number(),
annual_revenue: z.number(),
currency: z.string(),
industry: z.string(),
language: z.string(),
tax_id: z.string(),
registration_number: z.string(),
timezone: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
}),
contact_person: z
.object({
full_name: z.string(),
first_name: z.string(),
last_name: z.string(),
email: z.string().email(),
phone: z.string(),
mobile: z.string(),
position: z.string(),
is_primary: z.boolean(),
})
.optional(),
tags: z.object({
keywords: z.array(z.string()).or(z.null()).optional(),
categories: z.array(z.string()).or(z.null()).optional(),
specializations: z.array(z.string()).or(z.null()).optional(),
certifications: z.array(z.string()).or(z.null()).optional(),
cpv_codes: z.array(z.string()).or(z.null()).optional(),
}),
});
export const companiesResponseSchema = z.object({
companies: z.array(companySchema).or(z.null()),
});
export const companyDetailsApiResponseSchema =
createApiResponseSchema(companySchema);
export const CompaniesListResponseSchema = createApiResponseSchema(
companiesResponseSchema,
);
export type TCompany = z.infer<typeof companySchema>;
export type TCompaniesResponse = z.infer<typeof companiesResponseSchema>;
+29
View File
@@ -0,0 +1,29 @@
import { z } from "zod";
const TenderSchema = z.object({
id: z.string(),
buyer_organization: z.object({
name: z.string(),
}),
application_deadline: z.number(),
country_code: z.string(),
currency: z.string(),
description: z.string(),
duration: z.string(),
duration_unit: z.string(),
estimated_value: z.number(),
notice_publication_id: z.string(),
procedure_code: z.string(),
procurement_type_code: z.string(),
publication_date: z.number(),
status: z.string(),
submission_deadline: z.number(),
submission_url: z.string(),
tender_deadline: z.number(),
tender_id: z.string(),
title: z.string(),
});
export const TendersResponse = z.object({
companies: z.array(TenderSchema).or(z.null()),
});
export type TTenderResponse = z.infer<typeof TendersResponse>;
+72
View File
@@ -0,0 +1,72 @@
import { z } from "zod";
import { createApiResponseSchema } from "./Factory";
export const UserSchema = z.object({
id: z.string().regex(/^[0-9a-fA-F]{24}$/, {
message: "Invalid ObjectId",
}),
full_name: z.string().optional(),
username: z.string().optional(),
email: z.string().email().optional(),
role: z.string().optional(),
status: z.string().optional(),
company_id: z.string().optional(),
department: z.string().optional(),
position: z.string().optional(),
phone: z.string().optional(),
profile_image: z.string().or(z.null()).optional(),
is_verified: z.boolean().optional(),
last_login_at: z.number().or(z.null()).optional(),
created_at: z.number().optional(),
updated_at: z.number().optional(),
updated_by: z.string().optional(),
});
export const CreateAdminSchema = z.object({
full_name: z.string().min(1, "Full name is required"),
email: z.string().email("Invalid email address"),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
password: z
.string()
.min(8, "Password must be at least 8 characters long")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/\d/, "Password must contain at least one number")
.regex(/[\W_]/, "Password must contain at least one special character"),
profile_image: z.string().optional(),
department: z.string().min(1, "Department is required"),
position: z.string().min(1, "Position is required"),
role: z.enum(["manager", "employee", "admin", "guest"]),
username: z.string().min(3, "Username must be at least 3 characters long"),
});
export const LoginCredentials = z.object({
username: z.string(),
password: z.string(),
});
export const AdminListResponse = z.object({
users: z.array(UserSchema),
});
export const LoginResponse = z.object({
access_token: z.string(),
refresh_token: z.string(),
expires_at: z.number(),
user: UserSchema,
});
export const LogoutResponse = z.object({
message: z.string(),
});
export type IUser = z.infer<typeof UserSchema>;
export type ILogoutResponse = z.infer<typeof LogoutResponse>;
export type ILoginCredentials = z.infer<typeof LoginCredentials>;
export type ILoginResponse = z.infer<typeof LoginResponse>;
export type IAdminListResponse = z.infer<typeof AdminListResponse>;
export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse);
export const LoginResponseSchema = createApiResponseSchema(LoginResponse);
export const AdminListResponseSchema =
createApiResponseSchema(AdminListResponse);
export type ICreateAdminCredentials = z.infer<typeof CreateAdminSchema>;
+7
View File
@@ -0,0 +1,7 @@
export * from "./shared";
export * from "./User";
export * from "./Posts";
export * from "./Factory";
export * from "./TCompany";
export * from "./Customers";
export * from "./Profile";
+9
View File
@@ -0,0 +1,9 @@
import { IMetaData } from "./Factory";
export interface ApiResponse<T> {
data: T;
message: string;
code?: number;
meta?: IMetaData;
success: boolean;
}
+43
View File
@@ -0,0 +1,43 @@
export function formatMessageTime(timestamp: string) {
const messageDate = new Date(timestamp);
const now = new Date();
const diffInMinutes = Math.floor(
(now.getTime() - messageDate.getTime()) / (60 * 1000),
);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
// For messages from today, show time
if (diffInDays === 0) {
// If less than 60 minutes ago, show "X min"
if (diffInMinutes < 60) {
return diffInMinutes === 0 ? "just now" : `${diffInMinutes}m`;
}
// Otherwise show time like "4:39 PM"
return messageDate.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
// For messages from this week, show day name
if (diffInDays < 7) {
return messageDate.toLocaleDateString("en-US", { weekday: "long" });
}
// For messages from this year, show date
if (messageDate.getFullYear() === now.getFullYear()) {
return messageDate.toLocaleDateString("en-US", {
day: "numeric",
month: "short",
});
}
// For older messages, show date with year
return messageDate.toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
});
}
+15
View File
@@ -0,0 +1,15 @@
export function compactFormat(value: number) {
const formatter = new Intl.NumberFormat("en", {
notation: "compact",
compactDisplay: "short",
});
return formatter.format(value);
}
export function standardFormat(value: number) {
return value.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
+9
View File
@@ -0,0 +1,9 @@
const _COOKIE_PREFIX = "TENDER_PANEL";
export const COOKIE_KEYS = {
locale: `${_COOKIE_PREFIX}_locale`,
access_token: `${_COOKIE_PREFIX}_token`,
theme: `${_COOKIE_PREFIX}_theme`,
refresh_token: `${_COOKIE_PREFIX}_refresh_token`,
user: `${_COOKIE_PREFIX}_user`,
};
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}