@azlib/cms 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -38
- package/dist/index.cjs +2905 -6
- package/dist/index.d.cts +933 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +933 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2886 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1397,9 +1397,9 @@ var CMSEngine = class {
|
|
|
1397
1397
|
async create(input, authorId = null) {
|
|
1398
1398
|
const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
|
|
1399
1399
|
const inputData = { ...filteredInput.data || {} };
|
|
1400
|
-
if (filteredInput.title !== void 0) inputData.title = filteredInput.title;
|
|
1401
|
-
if (filteredInput.slug !== void 0) inputData.slug = filteredInput.slug;
|
|
1402
|
-
if (filteredInput.status !== void 0) inputData.status = filteredInput.status;
|
|
1400
|
+
if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
|
|
1401
|
+
if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
|
|
1402
|
+
if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
|
|
1403
1403
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
|
|
1404
1404
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
|
|
1405
1405
|
const title = filteredInput.title ?? normalizedData.title ?? "";
|
|
@@ -1451,11 +1451,11 @@ var CMSEngine = class {
|
|
|
1451
1451
|
...existing.data,
|
|
1452
1452
|
...filteredInput.data || {}
|
|
1453
1453
|
};
|
|
1454
|
-
if (filteredInput.title !== void 0) mergedData.title = filteredInput.title;
|
|
1454
|
+
if (filteredInput.title !== void 0 && filteredInput.data?.title === void 0) mergedData.title = filteredInput.title;
|
|
1455
1455
|
else if (existing.title !== void 0 && mergedData.title === void 0) mergedData.title = existing.title;
|
|
1456
|
-
if (filteredInput.slug !== void 0) mergedData.slug = filteredInput.slug;
|
|
1456
|
+
if (filteredInput.slug !== void 0 && filteredInput.data?.slug === void 0) mergedData.slug = filteredInput.slug;
|
|
1457
1457
|
else if (existing.slug !== void 0 && mergedData.slug === void 0) mergedData.slug = existing.slug;
|
|
1458
|
-
if (filteredInput.status !== void 0) mergedData.status = filteredInput.status;
|
|
1458
|
+
if (filteredInput.status !== void 0 && filteredInput.data?.status === void 0) mergedData.status = filteredInput.status;
|
|
1459
1459
|
else if (existing.status !== void 0 && mergedData.status === void 0) mergedData.status = existing.status;
|
|
1460
1460
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, mergedData);
|
|
1461
1461
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed updating '${slug}': ${JSON.stringify(errors)}`);
|
|
@@ -1842,6 +1842,12 @@ var CMSClient = class {
|
|
|
1842
1842
|
...options.headers || {}
|
|
1843
1843
|
};
|
|
1844
1844
|
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Access underlying CMSEngine instance when in in-process mode.
|
|
1847
|
+
*/
|
|
1848
|
+
getEngine() {
|
|
1849
|
+
return this.engine;
|
|
1850
|
+
}
|
|
1845
1851
|
collection(slug) {
|
|
1846
1852
|
const self = this;
|
|
1847
1853
|
if (this.engine) {
|
|
@@ -1925,6 +1931,9 @@ var CMSClient = class {
|
|
|
1925
1931
|
return defaultValue;
|
|
1926
1932
|
}
|
|
1927
1933
|
} };
|
|
1934
|
+
/**
|
|
1935
|
+
* Perform an HTTP request against the CMS API (available in remote mode).
|
|
1936
|
+
*/
|
|
1928
1937
|
async request(endpoint, init) {
|
|
1929
1938
|
if (!this.baseUrl) throw new Error(`[CMSClient] baseUrl must be provided when connecting to a remote CMS API.`);
|
|
1930
1939
|
const url = `${this.baseUrl}${endpoint}`;
|
|
@@ -1947,12 +1956,2886 @@ function createCmsClient(options) {
|
|
|
1947
1956
|
return new CMSClient(options);
|
|
1948
1957
|
}
|
|
1949
1958
|
//#endregion
|
|
1959
|
+
//#region src/plugins/ecommerce/schemas.ts
|
|
1960
|
+
/**
|
|
1961
|
+
* Creates the collection configuration for Products.
|
|
1962
|
+
*/
|
|
1963
|
+
function createProductCollection(options = {}) {
|
|
1964
|
+
const slug = options.productCollectionSlug ?? "products";
|
|
1965
|
+
const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
|
|
1966
|
+
const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
|
|
1967
|
+
const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
|
|
1968
|
+
const defaultCurrency = options.defaultCurrency ?? "USD";
|
|
1969
|
+
return collection({
|
|
1970
|
+
slug,
|
|
1971
|
+
label: "Products",
|
|
1972
|
+
singularLabel: "Product",
|
|
1973
|
+
description: "Catalog products with pricing, inventory, images, and variants",
|
|
1974
|
+
timestamps: true,
|
|
1975
|
+
revisions: true,
|
|
1976
|
+
draftable: true,
|
|
1977
|
+
taxonomies: [
|
|
1978
|
+
catSlug,
|
|
1979
|
+
tagSlug,
|
|
1980
|
+
brandSlug
|
|
1981
|
+
],
|
|
1982
|
+
defaultSort: {
|
|
1983
|
+
field: "createdAt",
|
|
1984
|
+
direction: "desc"
|
|
1985
|
+
},
|
|
1986
|
+
fields: [
|
|
1987
|
+
fields.text({
|
|
1988
|
+
name: "title",
|
|
1989
|
+
label: "Product Title",
|
|
1990
|
+
required: true
|
|
1991
|
+
}),
|
|
1992
|
+
fields.slug({
|
|
1993
|
+
from: "title",
|
|
1994
|
+
unique: true
|
|
1995
|
+
}),
|
|
1996
|
+
fields.text({
|
|
1997
|
+
name: "sku",
|
|
1998
|
+
label: "SKU",
|
|
1999
|
+
description: "Stock Keeping Unit identifier"
|
|
2000
|
+
}),
|
|
2001
|
+
fields.number({
|
|
2002
|
+
name: "price",
|
|
2003
|
+
label: "Price",
|
|
2004
|
+
required: true,
|
|
2005
|
+
min: 0
|
|
2006
|
+
}),
|
|
2007
|
+
fields.number({
|
|
2008
|
+
name: "compareAtPrice",
|
|
2009
|
+
label: "Compare At Price",
|
|
2010
|
+
description: "Original retail price for showing strike-through discounts",
|
|
2011
|
+
min: 0
|
|
2012
|
+
}),
|
|
2013
|
+
fields.number({
|
|
2014
|
+
name: "costPrice",
|
|
2015
|
+
label: "Cost Price",
|
|
2016
|
+
description: "Wholesale or production cost for margin tracking",
|
|
2017
|
+
min: 0
|
|
2018
|
+
}),
|
|
2019
|
+
fields.text({
|
|
2020
|
+
name: "currency",
|
|
2021
|
+
label: "Currency",
|
|
2022
|
+
defaultValue: defaultCurrency
|
|
2023
|
+
}),
|
|
2024
|
+
fields.number({
|
|
2025
|
+
name: "stock",
|
|
2026
|
+
label: "Inventory Quantity",
|
|
2027
|
+
defaultValue: 0,
|
|
2028
|
+
min: 0
|
|
2029
|
+
}),
|
|
2030
|
+
fields.boolean({
|
|
2031
|
+
name: "trackInventory",
|
|
2032
|
+
label: "Track Inventory",
|
|
2033
|
+
defaultValue: options.inventoryManagement ?? true
|
|
2034
|
+
}),
|
|
2035
|
+
fields.select({
|
|
2036
|
+
name: "status",
|
|
2037
|
+
label: "Status",
|
|
2038
|
+
options: [
|
|
2039
|
+
"draft",
|
|
2040
|
+
"published",
|
|
2041
|
+
"out_of_stock",
|
|
2042
|
+
"archived"
|
|
2043
|
+
],
|
|
2044
|
+
defaultValue: "draft"
|
|
2045
|
+
}),
|
|
2046
|
+
fields.richText({
|
|
2047
|
+
name: "description",
|
|
2048
|
+
label: "Full Description"
|
|
2049
|
+
}),
|
|
2050
|
+
fields.text({
|
|
2051
|
+
name: "shortDescription",
|
|
2052
|
+
label: "Short Description"
|
|
2053
|
+
}),
|
|
2054
|
+
fields.image({
|
|
2055
|
+
name: "featuredImage",
|
|
2056
|
+
label: "Featured Image"
|
|
2057
|
+
}),
|
|
2058
|
+
fields.json({
|
|
2059
|
+
name: "gallery",
|
|
2060
|
+
label: "Image Gallery",
|
|
2061
|
+
defaultValue: []
|
|
2062
|
+
}),
|
|
2063
|
+
fields.json({
|
|
2064
|
+
name: "variants",
|
|
2065
|
+
label: "Product Variants",
|
|
2066
|
+
defaultValue: []
|
|
2067
|
+
}),
|
|
2068
|
+
fields.json({
|
|
2069
|
+
name: "attributes",
|
|
2070
|
+
label: "Specifications / Attributes",
|
|
2071
|
+
defaultValue: {}
|
|
2072
|
+
}),
|
|
2073
|
+
fields.number({
|
|
2074
|
+
name: "weight",
|
|
2075
|
+
label: "Weight",
|
|
2076
|
+
min: 0
|
|
2077
|
+
})
|
|
2078
|
+
]
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Creates the collection configuration for Discounts / Coupons.
|
|
2083
|
+
*/
|
|
2084
|
+
function createDiscountCollection(options = {}) {
|
|
2085
|
+
return collection({
|
|
2086
|
+
slug: options.discountCollectionSlug ?? "discounts",
|
|
2087
|
+
label: "Discounts",
|
|
2088
|
+
singularLabel: "Discount",
|
|
2089
|
+
description: "Promotional discount codes and coupons",
|
|
2090
|
+
timestamps: true,
|
|
2091
|
+
revisions: true,
|
|
2092
|
+
draftable: false,
|
|
2093
|
+
defaultSort: {
|
|
2094
|
+
field: "createdAt",
|
|
2095
|
+
direction: "desc"
|
|
2096
|
+
},
|
|
2097
|
+
fields: [
|
|
2098
|
+
fields.text({
|
|
2099
|
+
name: "title",
|
|
2100
|
+
label: "Discount Name",
|
|
2101
|
+
required: true
|
|
2102
|
+
}),
|
|
2103
|
+
fields.text({
|
|
2104
|
+
name: "code",
|
|
2105
|
+
label: "Coupon Code",
|
|
2106
|
+
required: true,
|
|
2107
|
+
unique: true
|
|
2108
|
+
}),
|
|
2109
|
+
fields.select({
|
|
2110
|
+
name: "discountType",
|
|
2111
|
+
label: "Discount Type",
|
|
2112
|
+
options: [
|
|
2113
|
+
"percentage",
|
|
2114
|
+
"fixed_amount",
|
|
2115
|
+
"free_shipping"
|
|
2116
|
+
],
|
|
2117
|
+
defaultValue: "percentage"
|
|
2118
|
+
}),
|
|
2119
|
+
fields.number({
|
|
2120
|
+
name: "value",
|
|
2121
|
+
label: "Discount Value",
|
|
2122
|
+
required: true,
|
|
2123
|
+
min: 0
|
|
2124
|
+
}),
|
|
2125
|
+
fields.number({
|
|
2126
|
+
name: "minOrderAmount",
|
|
2127
|
+
label: "Minimum Order Amount",
|
|
2128
|
+
min: 0
|
|
2129
|
+
}),
|
|
2130
|
+
fields.number({
|
|
2131
|
+
name: "maxDiscountAmount",
|
|
2132
|
+
label: "Maximum Discount Cap",
|
|
2133
|
+
min: 0
|
|
2134
|
+
}),
|
|
2135
|
+
fields.number({
|
|
2136
|
+
name: "maxUses",
|
|
2137
|
+
label: "Maximum Uses",
|
|
2138
|
+
min: 1
|
|
2139
|
+
}),
|
|
2140
|
+
fields.number({
|
|
2141
|
+
name: "usedCount",
|
|
2142
|
+
label: "Times Used",
|
|
2143
|
+
defaultValue: 0,
|
|
2144
|
+
min: 0
|
|
2145
|
+
}),
|
|
2146
|
+
fields.date({
|
|
2147
|
+
name: "startDate",
|
|
2148
|
+
label: "Start Date"
|
|
2149
|
+
}),
|
|
2150
|
+
fields.date({
|
|
2151
|
+
name: "endDate",
|
|
2152
|
+
label: "Expiration Date"
|
|
2153
|
+
}),
|
|
2154
|
+
fields.select({
|
|
2155
|
+
name: "status",
|
|
2156
|
+
label: "Status",
|
|
2157
|
+
options: [
|
|
2158
|
+
"active",
|
|
2159
|
+
"disabled",
|
|
2160
|
+
"expired"
|
|
2161
|
+
],
|
|
2162
|
+
defaultValue: "active"
|
|
2163
|
+
}),
|
|
2164
|
+
fields.json({
|
|
2165
|
+
name: "appliesToProductIds",
|
|
2166
|
+
label: "Specific Product IDs",
|
|
2167
|
+
defaultValue: []
|
|
2168
|
+
}),
|
|
2169
|
+
fields.json({
|
|
2170
|
+
name: "appliesToCategoryIds",
|
|
2171
|
+
label: "Specific Category Term IDs",
|
|
2172
|
+
defaultValue: []
|
|
2173
|
+
})
|
|
2174
|
+
]
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Creates the collection configuration for Orders.
|
|
2179
|
+
*/
|
|
2180
|
+
function createOrderCollection(options = {}) {
|
|
2181
|
+
const slug = options.orderCollectionSlug ?? "orders";
|
|
2182
|
+
const defaultCurrency = options.defaultCurrency ?? "USD";
|
|
2183
|
+
return collection({
|
|
2184
|
+
slug,
|
|
2185
|
+
label: "Orders",
|
|
2186
|
+
singularLabel: "Order",
|
|
2187
|
+
description: "Customer orders, status lifecycle, and line items",
|
|
2188
|
+
timestamps: true,
|
|
2189
|
+
revisions: true,
|
|
2190
|
+
draftable: false,
|
|
2191
|
+
defaultSort: {
|
|
2192
|
+
field: "createdAt",
|
|
2193
|
+
direction: "desc"
|
|
2194
|
+
},
|
|
2195
|
+
fields: [
|
|
2196
|
+
fields.text({
|
|
2197
|
+
name: "orderNumber",
|
|
2198
|
+
label: "Order Number",
|
|
2199
|
+
required: true,
|
|
2200
|
+
unique: true
|
|
2201
|
+
}),
|
|
2202
|
+
fields.text({
|
|
2203
|
+
name: "customerEmail",
|
|
2204
|
+
label: "Customer Email",
|
|
2205
|
+
required: true
|
|
2206
|
+
}),
|
|
2207
|
+
fields.text({
|
|
2208
|
+
name: "customerName",
|
|
2209
|
+
label: "Customer Name"
|
|
2210
|
+
}),
|
|
2211
|
+
fields.select({
|
|
2212
|
+
name: "status",
|
|
2213
|
+
label: "Status",
|
|
2214
|
+
options: [
|
|
2215
|
+
"pending",
|
|
2216
|
+
"paid",
|
|
2217
|
+
"processing",
|
|
2218
|
+
"shipped",
|
|
2219
|
+
"delivered",
|
|
2220
|
+
"cancelled",
|
|
2221
|
+
"refunded"
|
|
2222
|
+
],
|
|
2223
|
+
defaultValue: "pending"
|
|
2224
|
+
}),
|
|
2225
|
+
fields.text({
|
|
2226
|
+
name: "currency",
|
|
2227
|
+
label: "Currency",
|
|
2228
|
+
defaultValue: defaultCurrency
|
|
2229
|
+
}),
|
|
2230
|
+
fields.json({
|
|
2231
|
+
name: "items",
|
|
2232
|
+
label: "Line Items",
|
|
2233
|
+
defaultValue: []
|
|
2234
|
+
}),
|
|
2235
|
+
fields.number({
|
|
2236
|
+
name: "subtotal",
|
|
2237
|
+
label: "Subtotal",
|
|
2238
|
+
defaultValue: 0,
|
|
2239
|
+
min: 0
|
|
2240
|
+
}),
|
|
2241
|
+
fields.number({
|
|
2242
|
+
name: "discountTotal",
|
|
2243
|
+
label: "Discount Total",
|
|
2244
|
+
defaultValue: 0,
|
|
2245
|
+
min: 0
|
|
2246
|
+
}),
|
|
2247
|
+
fields.text({
|
|
2248
|
+
name: "discountCode",
|
|
2249
|
+
label: "Discount Code"
|
|
2250
|
+
}),
|
|
2251
|
+
fields.number({
|
|
2252
|
+
name: "shippingTotal",
|
|
2253
|
+
label: "Shipping Total",
|
|
2254
|
+
defaultValue: 0,
|
|
2255
|
+
min: 0
|
|
2256
|
+
}),
|
|
2257
|
+
fields.number({
|
|
2258
|
+
name: "taxTotal",
|
|
2259
|
+
label: "Tax Total",
|
|
2260
|
+
defaultValue: 0,
|
|
2261
|
+
min: 0
|
|
2262
|
+
}),
|
|
2263
|
+
fields.number({
|
|
2264
|
+
name: "total",
|
|
2265
|
+
label: "Grand Total",
|
|
2266
|
+
defaultValue: 0,
|
|
2267
|
+
min: 0
|
|
2268
|
+
}),
|
|
2269
|
+
fields.json({
|
|
2270
|
+
name: "shippingAddress",
|
|
2271
|
+
label: "Shipping Address"
|
|
2272
|
+
}),
|
|
2273
|
+
fields.json({
|
|
2274
|
+
name: "billingAddress",
|
|
2275
|
+
label: "Billing Address"
|
|
2276
|
+
}),
|
|
2277
|
+
fields.text({
|
|
2278
|
+
name: "paymentMethod",
|
|
2279
|
+
label: "Payment Method"
|
|
2280
|
+
}),
|
|
2281
|
+
fields.text({
|
|
2282
|
+
name: "notes",
|
|
2283
|
+
label: "Notes"
|
|
2284
|
+
})
|
|
2285
|
+
]
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Creates the standard e-commerce taxonomies: product categories, tags, and brands.
|
|
2290
|
+
*/
|
|
2291
|
+
function createEcommerceTaxonomies(options = {}) {
|
|
2292
|
+
const productSlug = options.productCollectionSlug ?? "products";
|
|
2293
|
+
const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
|
|
2294
|
+
const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
|
|
2295
|
+
const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
|
|
2296
|
+
return [
|
|
2297
|
+
{
|
|
2298
|
+
slug: catSlug,
|
|
2299
|
+
label: "Product Categories",
|
|
2300
|
+
singularLabel: "Product Category",
|
|
2301
|
+
hierarchical: true,
|
|
2302
|
+
postTypes: [productSlug],
|
|
2303
|
+
description: "Hierarchical categories and catalog collections for organizing products"
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
slug: tagSlug,
|
|
2307
|
+
label: "Product Tags",
|
|
2308
|
+
singularLabel: "Product Tag",
|
|
2309
|
+
hierarchical: false,
|
|
2310
|
+
postTypes: [productSlug],
|
|
2311
|
+
description: "Flat keyword tags for filtering products"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
slug: brandSlug,
|
|
2315
|
+
label: "Brands",
|
|
2316
|
+
singularLabel: "Brand",
|
|
2317
|
+
hierarchical: false,
|
|
2318
|
+
postTypes: [productSlug],
|
|
2319
|
+
description: "Product manufacturers or brand labels"
|
|
2320
|
+
}
|
|
2321
|
+
];
|
|
2322
|
+
}
|
|
2323
|
+
//#endregion
|
|
2324
|
+
//#region src/plugins/ecommerce/service.ts
|
|
2325
|
+
var EcommerceService = class {
|
|
2326
|
+
engine;
|
|
2327
|
+
options;
|
|
2328
|
+
productSlug;
|
|
2329
|
+
discountSlug;
|
|
2330
|
+
orderSlug;
|
|
2331
|
+
categoriesTaxonomy;
|
|
2332
|
+
tagsTaxonomy;
|
|
2333
|
+
brandsTaxonomy;
|
|
2334
|
+
defaultCurrency;
|
|
2335
|
+
inventoryManagement;
|
|
2336
|
+
constructor(engine, options = {}) {
|
|
2337
|
+
this.engine = engine;
|
|
2338
|
+
this.options = options;
|
|
2339
|
+
this.productSlug = options.productCollectionSlug ?? "products";
|
|
2340
|
+
this.discountSlug = options.discountCollectionSlug ?? "discounts";
|
|
2341
|
+
this.orderSlug = options.orderCollectionSlug ?? "orders";
|
|
2342
|
+
this.categoriesTaxonomy = options.categoriesTaxonomySlug ?? "product_categories";
|
|
2343
|
+
this.tagsTaxonomy = options.tagsTaxonomySlug ?? "product_tags";
|
|
2344
|
+
this.brandsTaxonomy = options.brandsTaxonomySlug ?? "product_brands";
|
|
2345
|
+
this.defaultCurrency = options.defaultCurrency ?? "USD";
|
|
2346
|
+
this.inventoryManagement = options.inventoryManagement ?? true;
|
|
2347
|
+
}
|
|
2348
|
+
get productsCollection() {
|
|
2349
|
+
return this.engine.collection(this.productSlug);
|
|
2350
|
+
}
|
|
2351
|
+
get discountsCollection() {
|
|
2352
|
+
return this.engine.collection(this.discountSlug);
|
|
2353
|
+
}
|
|
2354
|
+
get ordersCollection() {
|
|
2355
|
+
return this.engine.collection(this.orderSlug);
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Create a new product in the catalog.
|
|
2359
|
+
*/
|
|
2360
|
+
async createProduct(input, authorId) {
|
|
2361
|
+
const productData = {
|
|
2362
|
+
price: input.price,
|
|
2363
|
+
compareAtPrice: input.compareAtPrice,
|
|
2364
|
+
costPrice: input.costPrice,
|
|
2365
|
+
sku: input.sku,
|
|
2366
|
+
currency: input.currency ?? this.defaultCurrency,
|
|
2367
|
+
stock: input.stock ?? 0,
|
|
2368
|
+
trackInventory: input.trackInventory ?? this.inventoryManagement,
|
|
2369
|
+
status: input.status ?? "draft",
|
|
2370
|
+
description: input.description,
|
|
2371
|
+
shortDescription: input.shortDescription,
|
|
2372
|
+
featuredImage: input.featuredImage,
|
|
2373
|
+
gallery: input.gallery ?? [],
|
|
2374
|
+
variants: input.variants ?? [],
|
|
2375
|
+
attributes: input.attributes ?? {},
|
|
2376
|
+
weight: input.weight
|
|
2377
|
+
};
|
|
2378
|
+
const product = await this.productsCollection.create({
|
|
2379
|
+
title: input.title,
|
|
2380
|
+
slug: input.slug,
|
|
2381
|
+
status: input.status === "published" ? "published" : "draft",
|
|
2382
|
+
data: productData
|
|
2383
|
+
}, authorId);
|
|
2384
|
+
if (input.categoryIds && input.categoryIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.categoryIds);
|
|
2385
|
+
if (input.tagIds && input.tagIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.tagIds);
|
|
2386
|
+
if (input.brandIds && input.brandIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.brandIds);
|
|
2387
|
+
await this.engine.hooks.doAction("ecommerce.product_created", product);
|
|
2388
|
+
return product;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Update an existing product.
|
|
2392
|
+
*/
|
|
2393
|
+
async updateProduct(id, input, authorId) {
|
|
2394
|
+
const existing = await this.getProduct(id);
|
|
2395
|
+
if (!existing) return null;
|
|
2396
|
+
const updatedData = {
|
|
2397
|
+
...existing.data,
|
|
2398
|
+
...input.price !== void 0 ? { price: input.price } : {},
|
|
2399
|
+
...input.compareAtPrice !== void 0 ? { compareAtPrice: input.compareAtPrice } : {},
|
|
2400
|
+
...input.costPrice !== void 0 ? { costPrice: input.costPrice } : {},
|
|
2401
|
+
...input.sku !== void 0 ? { sku: input.sku } : {},
|
|
2402
|
+
...input.currency !== void 0 ? { currency: input.currency } : {},
|
|
2403
|
+
...input.stock !== void 0 ? { stock: input.stock } : {},
|
|
2404
|
+
...input.trackInventory !== void 0 ? { trackInventory: input.trackInventory } : {},
|
|
2405
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
2406
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
2407
|
+
...input.shortDescription !== void 0 ? { shortDescription: input.shortDescription } : {},
|
|
2408
|
+
...input.featuredImage !== void 0 ? { featuredImage: input.featuredImage } : {},
|
|
2409
|
+
...input.gallery !== void 0 ? { gallery: input.gallery } : {},
|
|
2410
|
+
...input.variants !== void 0 ? { variants: input.variants } : {},
|
|
2411
|
+
...input.attributes !== void 0 ? { attributes: input.attributes } : {},
|
|
2412
|
+
...input.weight !== void 0 ? { weight: input.weight } : {}
|
|
2413
|
+
};
|
|
2414
|
+
const updated = await this.productsCollection.update(id, {
|
|
2415
|
+
...input.title ? { title: input.title } : {},
|
|
2416
|
+
...input.slug ? { slug: input.slug } : {},
|
|
2417
|
+
...input.status === "published" ? { status: "published" } : input.status ? { status: "draft" } : {},
|
|
2418
|
+
data: updatedData
|
|
2419
|
+
}, authorId);
|
|
2420
|
+
if (updated) {
|
|
2421
|
+
if (input.categoryIds) await this.engine.taxonomies.assignTerms(id, input.categoryIds);
|
|
2422
|
+
if (input.tagIds) await this.engine.taxonomies.assignTerms(id, input.tagIds);
|
|
2423
|
+
if (input.brandIds) await this.engine.taxonomies.assignTerms(id, input.brandIds);
|
|
2424
|
+
await this.engine.hooks.doAction("ecommerce.product_updated", updated);
|
|
2425
|
+
}
|
|
2426
|
+
return updated;
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Get a product by ID.
|
|
2430
|
+
*/
|
|
2431
|
+
async getProduct(id) {
|
|
2432
|
+
return this.productsCollection.findById(id);
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Get a product by its URL slug.
|
|
2436
|
+
*/
|
|
2437
|
+
async getProductBySlug(slug) {
|
|
2438
|
+
return this.productsCollection.findBySlug(slug);
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Delete a product by ID.
|
|
2442
|
+
*/
|
|
2443
|
+
async deleteProduct(id) {
|
|
2444
|
+
const deleted = await this.productsCollection.delete(id);
|
|
2445
|
+
if (deleted) await this.engine.hooks.doAction("ecommerce.product_deleted", id);
|
|
2446
|
+
return deleted;
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* List and filter catalog products.
|
|
2450
|
+
*/
|
|
2451
|
+
async listProducts(query = {}) {
|
|
2452
|
+
let termIds;
|
|
2453
|
+
if (query.categorySlug) {
|
|
2454
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.categoriesTaxonomy, query.categorySlug);
|
|
2455
|
+
if (term) termIds = [term.id];
|
|
2456
|
+
else return {
|
|
2457
|
+
items: [],
|
|
2458
|
+
total: 0,
|
|
2459
|
+
limit: query.limit ?? 20,
|
|
2460
|
+
offset: query.offset ?? 0,
|
|
2461
|
+
hasMore: false
|
|
2462
|
+
};
|
|
2463
|
+
} else if (query.categoryId) termIds = [query.categoryId];
|
|
2464
|
+
if (query.tagSlug) {
|
|
2465
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.tagsTaxonomy, query.tagSlug);
|
|
2466
|
+
if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
|
|
2467
|
+
}
|
|
2468
|
+
if (query.brandSlug) {
|
|
2469
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.brandsTaxonomy, query.brandSlug);
|
|
2470
|
+
if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
|
|
2471
|
+
}
|
|
2472
|
+
const contentStatus = query.status === "draft" ? "draft" : query.status === "published" ? "published" : void 0;
|
|
2473
|
+
const result = await this.productsCollection.find({
|
|
2474
|
+
search: query.search,
|
|
2475
|
+
termIds,
|
|
2476
|
+
status: contentStatus ?? "published",
|
|
2477
|
+
limit: query.limit ?? 20,
|
|
2478
|
+
offset: query.offset ?? 0,
|
|
2479
|
+
orderBy: query.orderBy === "price" || query.orderBy === "stock" ? void 0 : query.orderBy,
|
|
2480
|
+
orderDirection: query.orderDirection ?? "desc"
|
|
2481
|
+
});
|
|
2482
|
+
let items = result.items;
|
|
2483
|
+
if (query.status) {
|
|
2484
|
+
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
2485
|
+
items = items.filter((p) => statuses.includes(p.data.status));
|
|
2486
|
+
}
|
|
2487
|
+
if (query.minPrice !== void 0) items = items.filter((p) => p.data.price >= query.minPrice);
|
|
2488
|
+
if (query.maxPrice !== void 0) items = items.filter((p) => p.data.price <= query.maxPrice);
|
|
2489
|
+
if (query.inStock) items = items.filter((p) => !p.data.trackInventory || p.data.stock > 0);
|
|
2490
|
+
if (query.orderBy === "price") items.sort((a, b) => query.orderDirection === "asc" ? a.data.price - b.data.price : b.data.price - a.data.price);
|
|
2491
|
+
else if (query.orderBy === "stock") items.sort((a, b) => query.orderDirection === "asc" ? a.data.stock - b.data.stock : b.data.stock - a.data.stock);
|
|
2492
|
+
return {
|
|
2493
|
+
items,
|
|
2494
|
+
total: items.length,
|
|
2495
|
+
limit: result.limit,
|
|
2496
|
+
offset: result.offset,
|
|
2497
|
+
hasMore: result.offset + items.length < result.total
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Upload and link a product image to its gallery and featured slot.
|
|
2502
|
+
*/
|
|
2503
|
+
async uploadProductImage(productId, file, authorId) {
|
|
2504
|
+
const product = await this.getProduct(productId);
|
|
2505
|
+
if (!product) throw new Error(`[EcommerceService] Product '${productId}' not found.`);
|
|
2506
|
+
const media = await this.engine.media.upload({
|
|
2507
|
+
filename: file.filename,
|
|
2508
|
+
mimeType: file.mimeType,
|
|
2509
|
+
sizeBytes: file.sizeBytes,
|
|
2510
|
+
url: file.url,
|
|
2511
|
+
altText: file.altText ?? product.title,
|
|
2512
|
+
caption: file.caption,
|
|
2513
|
+
width: file.width,
|
|
2514
|
+
height: file.height
|
|
2515
|
+
}, authorId);
|
|
2516
|
+
const gallery = [...product.data.gallery || []];
|
|
2517
|
+
gallery.push({
|
|
2518
|
+
id: media.id,
|
|
2519
|
+
url: media.url,
|
|
2520
|
+
altText: media.altText,
|
|
2521
|
+
caption: media.caption,
|
|
2522
|
+
width: media.width,
|
|
2523
|
+
height: media.height
|
|
2524
|
+
});
|
|
2525
|
+
const isFeatured = file.isFeatured ?? !product.data.featuredImage;
|
|
2526
|
+
const updated = await this.productsCollection.update(productId, { data: {
|
|
2527
|
+
...product.data,
|
|
2528
|
+
gallery,
|
|
2529
|
+
...isFeatured ? { featuredImage: media.url } : {}
|
|
2530
|
+
} });
|
|
2531
|
+
await this.engine.hooks.doAction("ecommerce.product_image_added", {
|
|
2532
|
+
product: updated ?? product,
|
|
2533
|
+
media
|
|
2534
|
+
});
|
|
2535
|
+
return {
|
|
2536
|
+
media,
|
|
2537
|
+
product: updated ?? product
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Adjust inventory stock for a product or variant.
|
|
2542
|
+
*/
|
|
2543
|
+
async adjustStock(productId, delta, variantId) {
|
|
2544
|
+
const product = await this.getProduct(productId);
|
|
2545
|
+
if (!product) return null;
|
|
2546
|
+
if (!product.data.trackInventory) return product;
|
|
2547
|
+
let newStock = product.data.stock;
|
|
2548
|
+
const variants = [...product.data.variants || []];
|
|
2549
|
+
if (variantId) {
|
|
2550
|
+
const idx = variants.findIndex((v) => v.id === variantId);
|
|
2551
|
+
if (idx !== -1) {
|
|
2552
|
+
const currentVariantStock = variants[idx].stock ?? 0;
|
|
2553
|
+
const updatedVariantStock = Math.max(0, currentVariantStock + delta);
|
|
2554
|
+
variants[idx] = {
|
|
2555
|
+
...variants[idx],
|
|
2556
|
+
stock: updatedVariantStock
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
} else newStock = Math.max(0, product.data.stock + delta);
|
|
2560
|
+
const newStatus = newStock === 0 && product.data.status === "published" ? "out_of_stock" : product.data.status === "out_of_stock" && newStock > 0 ? "published" : product.data.status;
|
|
2561
|
+
const updated = await this.productsCollection.update(productId, { data: {
|
|
2562
|
+
...product.data,
|
|
2563
|
+
stock: newStock,
|
|
2564
|
+
variants,
|
|
2565
|
+
status: newStatus
|
|
2566
|
+
} });
|
|
2567
|
+
await this.engine.hooks.doAction("ecommerce.stock_changed", {
|
|
2568
|
+
product: updated ?? product,
|
|
2569
|
+
delta,
|
|
2570
|
+
variantId,
|
|
2571
|
+
oldStock: product.data.stock,
|
|
2572
|
+
newStock
|
|
2573
|
+
});
|
|
2574
|
+
return updated;
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Create a catalog category in the hierarchical category taxonomy.
|
|
2578
|
+
*/
|
|
2579
|
+
async createCategory(input) {
|
|
2580
|
+
return this.engine.taxonomies.createTerm(this.categoriesTaxonomy, input);
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Get all catalog categories.
|
|
2584
|
+
*/
|
|
2585
|
+
async getCategories(options) {
|
|
2586
|
+
return this.engine.taxonomies.getTerms(this.categoriesTaxonomy, options);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Get full hierarchical catalog category tree.
|
|
2590
|
+
*/
|
|
2591
|
+
async getCategoryTree() {
|
|
2592
|
+
return this.engine.taxonomies.getTermTree(this.categoriesTaxonomy);
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Assign category IDs to a product.
|
|
2596
|
+
*/
|
|
2597
|
+
async assignProductCategory(productId, categoryIds) {
|
|
2598
|
+
const ids = Array.isArray(categoryIds) ? categoryIds : [categoryIds];
|
|
2599
|
+
await this.engine.taxonomies.assignTerms(productId, ids);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Get assigned categories for a product.
|
|
2603
|
+
*/
|
|
2604
|
+
async getProductCategories(productId) {
|
|
2605
|
+
return this.engine.taxonomies.getContentTerms(productId, this.categoriesTaxonomy);
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Create a promotional coupon / discount code.
|
|
2609
|
+
*/
|
|
2610
|
+
async createDiscount(input, authorId) {
|
|
2611
|
+
const normalizedCode = input.code.trim().toUpperCase();
|
|
2612
|
+
const discountData = {
|
|
2613
|
+
code: normalizedCode,
|
|
2614
|
+
discountType: input.discountType,
|
|
2615
|
+
value: input.value,
|
|
2616
|
+
minOrderAmount: input.minOrderAmount,
|
|
2617
|
+
maxDiscountAmount: input.maxDiscountAmount,
|
|
2618
|
+
maxUses: input.maxUses,
|
|
2619
|
+
usedCount: 0,
|
|
2620
|
+
startDate: input.startDate,
|
|
2621
|
+
endDate: input.endDate,
|
|
2622
|
+
status: input.status ?? "active",
|
|
2623
|
+
appliesToProductIds: input.appliesToProductIds ?? [],
|
|
2624
|
+
appliesToCategoryIds: input.appliesToCategoryIds ?? []
|
|
2625
|
+
};
|
|
2626
|
+
const discount = await this.discountsCollection.create({
|
|
2627
|
+
title: input.title,
|
|
2628
|
+
slug: normalizedCode.toLowerCase(),
|
|
2629
|
+
status: "published",
|
|
2630
|
+
data: discountData
|
|
2631
|
+
}, authorId);
|
|
2632
|
+
await this.engine.hooks.doAction("ecommerce.discount_created", discount);
|
|
2633
|
+
return discount;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Find a discount code.
|
|
2637
|
+
*/
|
|
2638
|
+
async getDiscountByCode(code) {
|
|
2639
|
+
const normalized = code.trim().toUpperCase();
|
|
2640
|
+
return (await this.discountsCollection.find({ limit: 100 })).items.find((d) => d.data.code?.toUpperCase() === normalized) ?? null;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Validate a discount coupon against cart items and order subtotal.
|
|
2644
|
+
*/
|
|
2645
|
+
async validateDiscount(code, cartSubtotal, productIds = []) {
|
|
2646
|
+
const normalized = code.trim().toUpperCase();
|
|
2647
|
+
const discount = await this.getDiscountByCode(normalized);
|
|
2648
|
+
if (!discount) return {
|
|
2649
|
+
valid: false,
|
|
2650
|
+
code: normalized,
|
|
2651
|
+
discountAmount: 0,
|
|
2652
|
+
message: `Discount code '${code}' not found.`
|
|
2653
|
+
};
|
|
2654
|
+
const { data } = discount;
|
|
2655
|
+
if (data.status !== "active") return {
|
|
2656
|
+
valid: false,
|
|
2657
|
+
code: normalized,
|
|
2658
|
+
discountAmount: 0,
|
|
2659
|
+
message: "Discount code is currently inactive."
|
|
2660
|
+
};
|
|
2661
|
+
const now = Date.now();
|
|
2662
|
+
if (data.startDate && new Date(data.startDate).getTime() > now) return {
|
|
2663
|
+
valid: false,
|
|
2664
|
+
code: normalized,
|
|
2665
|
+
discountAmount: 0,
|
|
2666
|
+
message: "Discount code is not yet active."
|
|
2667
|
+
};
|
|
2668
|
+
if (data.endDate && new Date(data.endDate).getTime() < now) return {
|
|
2669
|
+
valid: false,
|
|
2670
|
+
code: normalized,
|
|
2671
|
+
discountAmount: 0,
|
|
2672
|
+
message: "Discount code has expired."
|
|
2673
|
+
};
|
|
2674
|
+
if (data.maxUses !== void 0 && data.usedCount >= data.maxUses) return {
|
|
2675
|
+
valid: false,
|
|
2676
|
+
code: normalized,
|
|
2677
|
+
discountAmount: 0,
|
|
2678
|
+
message: "Discount code usage limit reached."
|
|
2679
|
+
};
|
|
2680
|
+
if (data.minOrderAmount !== void 0 && cartSubtotal < data.minOrderAmount) return {
|
|
2681
|
+
valid: false,
|
|
2682
|
+
code: normalized,
|
|
2683
|
+
discountAmount: 0,
|
|
2684
|
+
message: `Minimum order amount of ${data.minOrderAmount} required for this coupon.`
|
|
2685
|
+
};
|
|
2686
|
+
if (data.appliesToProductIds && data.appliesToProductIds.length > 0 && productIds.length > 0) {
|
|
2687
|
+
if (!productIds.some((id) => data.appliesToProductIds?.includes(id))) return {
|
|
2688
|
+
valid: false,
|
|
2689
|
+
code: normalized,
|
|
2690
|
+
discountAmount: 0,
|
|
2691
|
+
message: "Coupon is not applicable to any products in your cart."
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
let discountAmount = 0;
|
|
2695
|
+
if (data.discountType === "percentage") {
|
|
2696
|
+
discountAmount = cartSubtotal * data.value / 100;
|
|
2697
|
+
if (data.maxDiscountAmount) discountAmount = Math.min(discountAmount, data.maxDiscountAmount);
|
|
2698
|
+
} else if (data.discountType === "fixed_amount") discountAmount = Math.min(data.value, cartSubtotal);
|
|
2699
|
+
else if (data.discountType === "free_shipping") discountAmount = 0;
|
|
2700
|
+
discountAmount = await this.engine.hooks.applyFilters("ecommerce.apply_discount", discountAmount, {
|
|
2701
|
+
discount,
|
|
2702
|
+
cartSubtotal
|
|
2703
|
+
});
|
|
2704
|
+
return {
|
|
2705
|
+
valid: true,
|
|
2706
|
+
code: normalized,
|
|
2707
|
+
discountAmount: Math.round(discountAmount * 100) / 100,
|
|
2708
|
+
discountType: data.discountType,
|
|
2709
|
+
discount
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Calculate cart subtotals, apply discounts, shipping, and taxes.
|
|
2714
|
+
*/
|
|
2715
|
+
async calculateCart(input) {
|
|
2716
|
+
const lineItems = [];
|
|
2717
|
+
let subtotal = 0;
|
|
2718
|
+
const productIds = [];
|
|
2719
|
+
for (const item of input.items) {
|
|
2720
|
+
const product = await this.getProduct(item.productId);
|
|
2721
|
+
if (!product) throw new Error(`[EcommerceService] Product '${item.productId}' not found.`);
|
|
2722
|
+
productIds.push(product.id);
|
|
2723
|
+
let price = product.data.price;
|
|
2724
|
+
let sku = product.data.sku;
|
|
2725
|
+
let title = product.title ?? "Product";
|
|
2726
|
+
let availableStock = product.data.stock;
|
|
2727
|
+
let image = product.data.featuredImage;
|
|
2728
|
+
if (item.variantId && product.data.variants) {
|
|
2729
|
+
const variant = product.data.variants.find((v) => v.id === item.variantId);
|
|
2730
|
+
if (variant) {
|
|
2731
|
+
price = variant.price ?? price;
|
|
2732
|
+
sku = variant.sku ?? sku;
|
|
2733
|
+
title = `${title} (${variant.title})`;
|
|
2734
|
+
availableStock = variant.stock ?? availableStock;
|
|
2735
|
+
image = variant.image ?? image;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (this.inventoryManagement && product.data.trackInventory && availableStock < item.quantity) throw new Error(`[EcommerceService] Insufficient stock for '${title}'. Available: ${availableStock}, Requested: ${item.quantity}.`);
|
|
2739
|
+
const itemSubtotal = Math.round(price * item.quantity * 100) / 100;
|
|
2740
|
+
subtotal += itemSubtotal;
|
|
2741
|
+
lineItems.push({
|
|
2742
|
+
productId: product.id,
|
|
2743
|
+
variantId: item.variantId,
|
|
2744
|
+
title,
|
|
2745
|
+
sku,
|
|
2746
|
+
price,
|
|
2747
|
+
quantity: item.quantity,
|
|
2748
|
+
subtotal: itemSubtotal,
|
|
2749
|
+
image
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
subtotal = Math.round(subtotal * 100) / 100;
|
|
2753
|
+
let discountTotal = 0;
|
|
2754
|
+
let discountType;
|
|
2755
|
+
if (input.discountCode) {
|
|
2756
|
+
const validation = await this.validateDiscount(input.discountCode, subtotal, productIds);
|
|
2757
|
+
if (validation.valid) {
|
|
2758
|
+
discountTotal = validation.discountAmount;
|
|
2759
|
+
discountType = validation.discountType;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
let shippingTotal = input.shippingCost ?? this.options.defaultShippingCost ?? 0;
|
|
2763
|
+
if (discountType === "free_shipping") shippingTotal = 0;
|
|
2764
|
+
const taxRate = input.taxRate ?? this.options.defaultTaxRate ?? 0;
|
|
2765
|
+
const taxableAmount = Math.max(0, subtotal - discountTotal);
|
|
2766
|
+
const taxTotal = Math.round(taxableAmount * taxRate * 100) / 100;
|
|
2767
|
+
const total = Math.round((taxableAmount + shippingTotal + taxTotal) * 100) / 100;
|
|
2768
|
+
const result = {
|
|
2769
|
+
items: lineItems,
|
|
2770
|
+
subtotal,
|
|
2771
|
+
discountTotal,
|
|
2772
|
+
discountCode: input.discountCode,
|
|
2773
|
+
shippingTotal,
|
|
2774
|
+
taxTotal,
|
|
2775
|
+
total,
|
|
2776
|
+
currency: this.defaultCurrency
|
|
2777
|
+
};
|
|
2778
|
+
return this.engine.hooks.applyFilters("ecommerce.calculate_totals", result, input);
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Place a new order with cart validation, inventory deduction, and coupon counter updates.
|
|
2782
|
+
*/
|
|
2783
|
+
async createOrder(input, authorId) {
|
|
2784
|
+
const calc = await this.calculateCart({
|
|
2785
|
+
items: input.items,
|
|
2786
|
+
discountCode: input.discountCode,
|
|
2787
|
+
shippingCost: input.shippingCost,
|
|
2788
|
+
taxRate: input.taxRate
|
|
2789
|
+
});
|
|
2790
|
+
const orderNumber = `ORD-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
|
|
2791
|
+
if (this.inventoryManagement) for (const item of input.items) await this.adjustStock(item.productId, -item.quantity, item.variantId);
|
|
2792
|
+
if (input.discountCode) {
|
|
2793
|
+
const discount = await this.getDiscountByCode(input.discountCode);
|
|
2794
|
+
if (discount) await this.discountsCollection.update(discount.id, { data: {
|
|
2795
|
+
...discount.data,
|
|
2796
|
+
usedCount: (discount.data.usedCount || 0) + 1
|
|
2797
|
+
} });
|
|
2798
|
+
}
|
|
2799
|
+
const orderData = {
|
|
2800
|
+
orderNumber,
|
|
2801
|
+
customerEmail: input.customerEmail,
|
|
2802
|
+
customerName: input.customerName,
|
|
2803
|
+
status: "pending",
|
|
2804
|
+
currency: calc.currency,
|
|
2805
|
+
items: calc.items,
|
|
2806
|
+
subtotal: calc.subtotal,
|
|
2807
|
+
discountTotal: calc.discountTotal,
|
|
2808
|
+
discountCode: calc.discountCode,
|
|
2809
|
+
shippingTotal: calc.shippingTotal,
|
|
2810
|
+
taxTotal: calc.taxTotal,
|
|
2811
|
+
total: calc.total,
|
|
2812
|
+
shippingAddress: input.shippingAddress,
|
|
2813
|
+
billingAddress: input.billingAddress,
|
|
2814
|
+
paymentMethod: input.paymentMethod,
|
|
2815
|
+
notes: input.notes
|
|
2816
|
+
};
|
|
2817
|
+
const order = await this.ordersCollection.create({
|
|
2818
|
+
title: `Order #${orderNumber}`,
|
|
2819
|
+
slug: orderNumber.toLowerCase(),
|
|
2820
|
+
status: "published",
|
|
2821
|
+
data: orderData
|
|
2822
|
+
}, authorId);
|
|
2823
|
+
await this.engine.hooks.doAction("ecommerce.order_created", order);
|
|
2824
|
+
return order;
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Get order by ID.
|
|
2828
|
+
*/
|
|
2829
|
+
async getOrder(id) {
|
|
2830
|
+
return this.ordersCollection.findById(id);
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Get order by order number.
|
|
2834
|
+
*/
|
|
2835
|
+
async getOrderByNumber(orderNumber) {
|
|
2836
|
+
return (await this.ordersCollection.find({ limit: 100 })).items.find((o) => o.data.orderNumber?.toUpperCase() === orderNumber.toUpperCase()) ?? null;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Update the status of an order (e.g. pending -> paid -> shipped).
|
|
2840
|
+
*/
|
|
2841
|
+
async updateOrderStatus(id, status, note) {
|
|
2842
|
+
const existing = await this.getOrder(id);
|
|
2843
|
+
if (!existing) return null;
|
|
2844
|
+
const oldStatus = existing.data.status;
|
|
2845
|
+
const updated = await this.ordersCollection.update(id, { data: {
|
|
2846
|
+
...existing.data,
|
|
2847
|
+
status
|
|
2848
|
+
} }, void 0, note);
|
|
2849
|
+
if (updated) await this.engine.hooks.doAction("ecommerce.order_status_changed", {
|
|
2850
|
+
order: updated,
|
|
2851
|
+
oldStatus,
|
|
2852
|
+
newStatus: status,
|
|
2853
|
+
note
|
|
2854
|
+
});
|
|
2855
|
+
return updated;
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
//#endregion
|
|
2859
|
+
//#region src/plugins/ecommerce/routes.ts
|
|
2860
|
+
function json$1(data, status = 200) {
|
|
2861
|
+
return new Response(JSON.stringify(data), {
|
|
2862
|
+
status,
|
|
2863
|
+
headers: {
|
|
2864
|
+
"Content-Type": "application/json",
|
|
2865
|
+
"Access-Control-Allow-Origin": "*"
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
function badRequest$1(message) {
|
|
2870
|
+
return json$1({ error: message }, 400);
|
|
2871
|
+
}
|
|
2872
|
+
function notFound$1(message) {
|
|
2873
|
+
return json$1({ error: message }, 404);
|
|
2874
|
+
}
|
|
2875
|
+
function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
2876
|
+
const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
|
|
2877
|
+
ctx.registerRoute("GET", `${prefix}/products`, async (_req, { url }) => {
|
|
2878
|
+
try {
|
|
2879
|
+
const categorySlug = url.searchParams.get("category") ?? void 0;
|
|
2880
|
+
const categoryId = url.searchParams.get("categoryId") ?? void 0;
|
|
2881
|
+
const tagSlug = url.searchParams.get("tag") ?? void 0;
|
|
2882
|
+
const brandSlug = url.searchParams.get("brand") ?? void 0;
|
|
2883
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
2884
|
+
const statusParam = url.searchParams.get("status");
|
|
2885
|
+
const inStock = url.searchParams.get("inStock") === "true";
|
|
2886
|
+
const minPriceStr = url.searchParams.get("minPrice");
|
|
2887
|
+
const maxPriceStr = url.searchParams.get("maxPrice");
|
|
2888
|
+
const minPrice = minPriceStr ? parseFloat(minPriceStr) : void 0;
|
|
2889
|
+
const maxPrice = maxPriceStr ? parseFloat(maxPriceStr) : void 0;
|
|
2890
|
+
const orderBy = url.searchParams.get("orderBy");
|
|
2891
|
+
const orderDirection = url.searchParams.get("orderDirection") ?? "desc";
|
|
2892
|
+
const limitStr = url.searchParams.get("limit");
|
|
2893
|
+
const offsetStr = url.searchParams.get("offset");
|
|
2894
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 20;
|
|
2895
|
+
const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
|
|
2896
|
+
return json$1(await service.listProducts({
|
|
2897
|
+
categorySlug,
|
|
2898
|
+
categoryId,
|
|
2899
|
+
tagSlug,
|
|
2900
|
+
brandSlug,
|
|
2901
|
+
search,
|
|
2902
|
+
status: statusParam,
|
|
2903
|
+
inStock,
|
|
2904
|
+
minPrice,
|
|
2905
|
+
maxPrice,
|
|
2906
|
+
orderBy,
|
|
2907
|
+
orderDirection,
|
|
2908
|
+
limit,
|
|
2909
|
+
offset
|
|
2910
|
+
}));
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
|
|
2916
|
+
const id = params.id;
|
|
2917
|
+
const by = url.searchParams.get("by");
|
|
2918
|
+
let product = null;
|
|
2919
|
+
if (by === "slug") product = await service.getProductBySlug(id);
|
|
2920
|
+
else {
|
|
2921
|
+
product = await service.getProduct(id);
|
|
2922
|
+
if (!product) product = await service.getProductBySlug(id);
|
|
2923
|
+
}
|
|
2924
|
+
if (!product) return notFound$1(`Product '${id}' not found`);
|
|
2925
|
+
return json$1(product);
|
|
2926
|
+
});
|
|
2927
|
+
ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
|
|
2928
|
+
try {
|
|
2929
|
+
const body = await req.json();
|
|
2930
|
+
if (!body.title) return badRequest$1("Product 'title' is required.");
|
|
2931
|
+
if (body.price === void 0 || body.price < 0) return badRequest$1("Valid product 'price' is required.");
|
|
2932
|
+
return json$1(await service.createProduct(body), 201);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
|
|
2938
|
+
try {
|
|
2939
|
+
const body = await req.json();
|
|
2940
|
+
const updated = await service.updateProduct(params.id, body);
|
|
2941
|
+
if (!updated) return notFound$1(`Product '${params.id}' not found.`);
|
|
2942
|
+
return json$1(updated);
|
|
2943
|
+
} catch (err) {
|
|
2944
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
|
|
2948
|
+
if (!await service.deleteProduct(params.id)) return notFound$1(`Product '${params.id}' not found.`);
|
|
2949
|
+
return json$1({
|
|
2950
|
+
success: true,
|
|
2951
|
+
id: params.id
|
|
2952
|
+
});
|
|
2953
|
+
});
|
|
2954
|
+
ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
|
|
2955
|
+
try {
|
|
2956
|
+
const body = await req.json();
|
|
2957
|
+
if (!body.filename || !body.mimeType) return badRequest$1("'filename' and 'mimeType' are required.");
|
|
2958
|
+
return json$1(await service.uploadProductImage(params.id, {
|
|
2959
|
+
filename: body.filename,
|
|
2960
|
+
mimeType: body.mimeType,
|
|
2961
|
+
sizeBytes: body.sizeBytes ?? 0,
|
|
2962
|
+
url: body.url,
|
|
2963
|
+
altText: body.altText,
|
|
2964
|
+
caption: body.caption,
|
|
2965
|
+
width: body.width,
|
|
2966
|
+
height: body.height,
|
|
2967
|
+
isFeatured: body.isFeatured
|
|
2968
|
+
}), 201);
|
|
2969
|
+
} catch (err) {
|
|
2970
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
|
|
2974
|
+
try {
|
|
2975
|
+
if (url.searchParams.get("tree") === "true") return json$1(await service.getCategoryTree());
|
|
2976
|
+
const parentId = url.searchParams.get("parentId");
|
|
2977
|
+
return json$1(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
|
|
2983
|
+
try {
|
|
2984
|
+
const body = await req.json();
|
|
2985
|
+
if (!body.name) return badRequest$1("Category 'name' is required.");
|
|
2986
|
+
return json$1(await service.createCategory(body), 201);
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
if (options.enableDiscounts !== false) {
|
|
2992
|
+
ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
|
|
2993
|
+
try {
|
|
2994
|
+
const body = await req.json();
|
|
2995
|
+
if (!body.title || !body.code) return badRequest$1("'title' and 'code' are required.");
|
|
2996
|
+
if (body.value === void 0 || body.value < 0) return badRequest$1("Valid discount 'value' is required.");
|
|
2997
|
+
return json$1(await service.createDiscount(body), 201);
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
|
|
3003
|
+
try {
|
|
3004
|
+
const body = await req.json();
|
|
3005
|
+
if (!body.code) return badRequest$1("Discount 'code' is required.");
|
|
3006
|
+
const subtotal = Number(body.subtotal ?? 0);
|
|
3007
|
+
const productIds = Array.isArray(body.productIds) ? body.productIds : [];
|
|
3008
|
+
return json$1(await service.validateDiscount(body.code, subtotal, productIds));
|
|
3009
|
+
} catch (err) {
|
|
3010
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
|
|
3015
|
+
try {
|
|
3016
|
+
const body = await req.json();
|
|
3017
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$1("'items' array is required and must not be empty.");
|
|
3018
|
+
return json$1(await service.calculateCart(body));
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
if (options.enableOrders !== false) {
|
|
3024
|
+
ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
|
|
3025
|
+
try {
|
|
3026
|
+
const body = await req.json();
|
|
3027
|
+
if (!body.customerEmail) return badRequest$1("'customerEmail' is required.");
|
|
3028
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$1("'items' array is required.");
|
|
3029
|
+
return json$1(await service.createOrder(body), 201);
|
|
3030
|
+
} catch (err) {
|
|
3031
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
|
|
3035
|
+
const id = params.id;
|
|
3036
|
+
const by = url.searchParams.get("by");
|
|
3037
|
+
let order = null;
|
|
3038
|
+
if (by === "number") order = await service.getOrderByNumber(id);
|
|
3039
|
+
else {
|
|
3040
|
+
order = await service.getOrder(id);
|
|
3041
|
+
if (!order) order = await service.getOrderByNumber(id);
|
|
3042
|
+
}
|
|
3043
|
+
if (!order) return notFound$1(`Order '${id}' not found.`);
|
|
3044
|
+
return json$1(order);
|
|
3045
|
+
});
|
|
3046
|
+
ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
|
|
3047
|
+
try {
|
|
3048
|
+
const body = await req.json();
|
|
3049
|
+
if (!body.status) return badRequest$1("New 'status' is required.");
|
|
3050
|
+
const updated = await service.updateOrderStatus(params.id, body.status, body.note);
|
|
3051
|
+
if (!updated) return notFound$1(`Order '${params.id}' not found.`);
|
|
3052
|
+
return json$1(updated);
|
|
3053
|
+
} catch (err) {
|
|
3054
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
//#endregion
|
|
3060
|
+
//#region src/plugins/ecommerce/client.ts
|
|
3061
|
+
var EcommerceClient = class {
|
|
3062
|
+
client;
|
|
3063
|
+
options;
|
|
3064
|
+
service;
|
|
3065
|
+
prefix;
|
|
3066
|
+
constructor(client, options = {}) {
|
|
3067
|
+
this.client = client;
|
|
3068
|
+
this.options = options;
|
|
3069
|
+
this.prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
|
|
3070
|
+
const engine = client.getEngine();
|
|
3071
|
+
if (engine) this.service = new EcommerceService(engine, options);
|
|
3072
|
+
}
|
|
3073
|
+
products = {
|
|
3074
|
+
find: async (query = {}) => {
|
|
3075
|
+
if (this.service) return this.service.listProducts(query);
|
|
3076
|
+
const params = new URLSearchParams();
|
|
3077
|
+
if (query.categorySlug) params.set("category", query.categorySlug);
|
|
3078
|
+
if (query.categoryId) params.set("categoryId", query.categoryId);
|
|
3079
|
+
if (query.tagSlug) params.set("tag", query.tagSlug);
|
|
3080
|
+
if (query.brandSlug) params.set("brand", query.brandSlug);
|
|
3081
|
+
if (query.search) params.set("search", query.search);
|
|
3082
|
+
if (query.inStock) params.set("inStock", "true");
|
|
3083
|
+
if (query.minPrice !== void 0) params.set("minPrice", String(query.minPrice));
|
|
3084
|
+
if (query.maxPrice !== void 0) params.set("maxPrice", String(query.maxPrice));
|
|
3085
|
+
if (query.orderBy) params.set("orderBy", query.orderBy);
|
|
3086
|
+
if (query.orderDirection) params.set("orderDirection", query.orderDirection);
|
|
3087
|
+
if (query.limit !== void 0) params.set("limit", String(query.limit));
|
|
3088
|
+
if (query.offset !== void 0) params.set("offset", String(query.offset));
|
|
3089
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
3090
|
+
return this.client.request(`${this.prefix}/products${q}`);
|
|
3091
|
+
},
|
|
3092
|
+
get: async (idOrSlug, by) => {
|
|
3093
|
+
if (this.service) return by === "slug" ? this.service.getProductBySlug(idOrSlug) : this.service.getProduct(idOrSlug);
|
|
3094
|
+
const query = by ? `?by=${by}` : "";
|
|
3095
|
+
return this.client.request(`${this.prefix}/products/${encodeURIComponent(idOrSlug)}${query}`);
|
|
3096
|
+
},
|
|
3097
|
+
create: async (data) => {
|
|
3098
|
+
if (this.service) return this.service.createProduct(data);
|
|
3099
|
+
return this.client.request(`${this.prefix}/products`, {
|
|
3100
|
+
method: "POST",
|
|
3101
|
+
body: JSON.stringify(data)
|
|
3102
|
+
});
|
|
3103
|
+
},
|
|
3104
|
+
update: async (id, data) => {
|
|
3105
|
+
if (this.service) return this.service.updateProduct(id, data);
|
|
3106
|
+
return this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, {
|
|
3107
|
+
method: "PUT",
|
|
3108
|
+
body: JSON.stringify(data)
|
|
3109
|
+
});
|
|
3110
|
+
},
|
|
3111
|
+
delete: async (id) => {
|
|
3112
|
+
if (this.service) return this.service.deleteProduct(id);
|
|
3113
|
+
const res = await this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
3114
|
+
return Boolean(res?.success);
|
|
3115
|
+
},
|
|
3116
|
+
uploadImage: async (productId, file) => {
|
|
3117
|
+
if (this.service) return this.service.uploadProductImage(productId, file);
|
|
3118
|
+
return this.client.request(`${this.prefix}/products/${encodeURIComponent(productId)}/images`, {
|
|
3119
|
+
method: "POST",
|
|
3120
|
+
body: JSON.stringify(file)
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
categories = {
|
|
3125
|
+
list: async (options) => {
|
|
3126
|
+
if (this.service) return this.service.getCategories(options);
|
|
3127
|
+
const q = options?.parentId !== void 0 ? `?parentId=${options.parentId}` : "";
|
|
3128
|
+
return this.client.request(`${this.prefix}/categories${q}`);
|
|
3129
|
+
},
|
|
3130
|
+
tree: async () => {
|
|
3131
|
+
if (this.service) return this.service.getCategoryTree();
|
|
3132
|
+
return this.client.request(`${this.prefix}/categories?tree=true`);
|
|
3133
|
+
},
|
|
3134
|
+
create: async (input) => {
|
|
3135
|
+
if (this.service) return this.service.createCategory(input);
|
|
3136
|
+
return this.client.request(`${this.prefix}/categories`, {
|
|
3137
|
+
method: "POST",
|
|
3138
|
+
body: JSON.stringify(input)
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
discounts = {
|
|
3143
|
+
validate: async (code, subtotal, productIds) => {
|
|
3144
|
+
if (this.service) return this.service.validateDiscount(code, subtotal, productIds);
|
|
3145
|
+
return this.client.request(`${this.prefix}/discounts/validate`, {
|
|
3146
|
+
method: "POST",
|
|
3147
|
+
body: JSON.stringify({
|
|
3148
|
+
code,
|
|
3149
|
+
subtotal,
|
|
3150
|
+
productIds
|
|
3151
|
+
})
|
|
3152
|
+
});
|
|
3153
|
+
},
|
|
3154
|
+
create: async (input) => {
|
|
3155
|
+
if (this.service) return this.service.createDiscount(input);
|
|
3156
|
+
return this.client.request(`${this.prefix}/discounts`, {
|
|
3157
|
+
method: "POST",
|
|
3158
|
+
body: JSON.stringify(input)
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
};
|
|
3162
|
+
cart = { calculate: async (input) => {
|
|
3163
|
+
if (this.service) return this.service.calculateCart(input);
|
|
3164
|
+
return this.client.request(`${this.prefix}/cart/calculate`, {
|
|
3165
|
+
method: "POST",
|
|
3166
|
+
body: JSON.stringify(input)
|
|
3167
|
+
});
|
|
3168
|
+
} };
|
|
3169
|
+
orders = {
|
|
3170
|
+
create: async (input) => {
|
|
3171
|
+
if (this.service) return this.service.createOrder(input);
|
|
3172
|
+
return this.client.request(`${this.prefix}/orders`, {
|
|
3173
|
+
method: "POST",
|
|
3174
|
+
body: JSON.stringify(input)
|
|
3175
|
+
});
|
|
3176
|
+
},
|
|
3177
|
+
get: async (idOrNumber, by) => {
|
|
3178
|
+
if (this.service) return by === "number" ? this.service.getOrderByNumber(idOrNumber) : this.service.getOrder(idOrNumber);
|
|
3179
|
+
const q = by ? `?by=${by}` : "";
|
|
3180
|
+
return this.client.request(`${this.prefix}/orders/${encodeURIComponent(idOrNumber)}${q}`);
|
|
3181
|
+
},
|
|
3182
|
+
updateStatus: async (id, status, note) => {
|
|
3183
|
+
if (this.service) return this.service.updateOrderStatus(id, status, note);
|
|
3184
|
+
return this.client.request(`${this.prefix}/orders/${encodeURIComponent(id)}/status`, {
|
|
3185
|
+
method: "PATCH",
|
|
3186
|
+
body: JSON.stringify({
|
|
3187
|
+
status,
|
|
3188
|
+
note
|
|
3189
|
+
})
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
};
|
|
3194
|
+
/**
|
|
3195
|
+
* Get or create an EcommerceClient adapter for a CMSClient.
|
|
3196
|
+
*/
|
|
3197
|
+
function getEcommerceClient(client, options) {
|
|
3198
|
+
return new EcommerceClient(client, options);
|
|
3199
|
+
}
|
|
3200
|
+
//#endregion
|
|
3201
|
+
//#region src/plugins/ecommerce/index.ts
|
|
3202
|
+
/**
|
|
3203
|
+
* @azlib/cms - Built-in E-commerce Plugin
|
|
3204
|
+
*/
|
|
3205
|
+
/**
|
|
3206
|
+
* Built-in E-commerce plugin factory for @azlib/cms.
|
|
3207
|
+
* Equips the CMS engine with product catalogs, hierarchical categories,
|
|
3208
|
+
* image uploading, discount coupons, cart calculation, and order tracking.
|
|
3209
|
+
*/
|
|
3210
|
+
const ecommercePlugin = definePlugin((options) => {
|
|
3211
|
+
const opts = options || {};
|
|
3212
|
+
const collections = [createProductCollection(opts)];
|
|
3213
|
+
if (opts.enableDiscounts !== false) collections.push(createDiscountCollection(opts));
|
|
3214
|
+
if (opts.enableOrders !== false) collections.push(createOrderCollection(opts));
|
|
3215
|
+
return {
|
|
3216
|
+
name: "ecommerce",
|
|
3217
|
+
version: "1.0.0",
|
|
3218
|
+
description: "Built-in E-commerce shopping, products, catalogs, discounts, and orders plugin",
|
|
3219
|
+
collections,
|
|
3220
|
+
taxonomies: createEcommerceTaxonomies(opts),
|
|
3221
|
+
setup(ctx) {
|
|
3222
|
+
const service = new EcommerceService(ctx.engine, opts);
|
|
3223
|
+
ctx.engine.__ecommerceService = service;
|
|
3224
|
+
registerEcommerceRoutes(ctx, service, opts);
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
});
|
|
3228
|
+
/**
|
|
3229
|
+
* Retrieve the active EcommerceService instance associated with a CMSEngine.
|
|
3230
|
+
*/
|
|
3231
|
+
function getEcommerceService(engine, options) {
|
|
3232
|
+
if (engine.__ecommerceService) return engine.__ecommerceService;
|
|
3233
|
+
const service = new EcommerceService(engine, options);
|
|
3234
|
+
engine.__ecommerceService = service;
|
|
3235
|
+
return service;
|
|
3236
|
+
}
|
|
3237
|
+
//#endregion
|
|
3238
|
+
//#region src/plugins/hrms/schemas.ts
|
|
3239
|
+
/**
|
|
3240
|
+
* Creates the collection configuration for Employers (Organizations / Companies).
|
|
3241
|
+
*/
|
|
3242
|
+
function createEmployerCollection(options = {}) {
|
|
3243
|
+
return collection({
|
|
3244
|
+
slug: options.employerCollectionSlug ?? "employers",
|
|
3245
|
+
label: "Employers",
|
|
3246
|
+
singularLabel: "Employer",
|
|
3247
|
+
description: "Companies and organizational entities managing employees, shifts, and leave policies",
|
|
3248
|
+
timestamps: true,
|
|
3249
|
+
revisions: true,
|
|
3250
|
+
draftable: true,
|
|
3251
|
+
defaultSort: {
|
|
3252
|
+
field: "createdAt",
|
|
3253
|
+
direction: "desc"
|
|
3254
|
+
},
|
|
3255
|
+
fields: [
|
|
3256
|
+
fields.text({
|
|
3257
|
+
name: "companyName",
|
|
3258
|
+
label: "Company Name",
|
|
3259
|
+
required: true
|
|
3260
|
+
}),
|
|
3261
|
+
fields.slug({
|
|
3262
|
+
from: "companyName",
|
|
3263
|
+
unique: true
|
|
3264
|
+
}),
|
|
3265
|
+
fields.text({
|
|
3266
|
+
name: "legalName",
|
|
3267
|
+
label: "Legal Entity Name"
|
|
3268
|
+
}),
|
|
3269
|
+
fields.text({
|
|
3270
|
+
name: "taxId",
|
|
3271
|
+
label: "Tax / EIN Number"
|
|
3272
|
+
}),
|
|
3273
|
+
fields.text({
|
|
3274
|
+
name: "email",
|
|
3275
|
+
label: "Primary Email"
|
|
3276
|
+
}),
|
|
3277
|
+
fields.text({
|
|
3278
|
+
name: "phone",
|
|
3279
|
+
label: "Phone Number"
|
|
3280
|
+
}),
|
|
3281
|
+
fields.text({
|
|
3282
|
+
name: "website",
|
|
3283
|
+
label: "Website URL"
|
|
3284
|
+
}),
|
|
3285
|
+
fields.image({
|
|
3286
|
+
name: "logo",
|
|
3287
|
+
label: "Company Logo"
|
|
3288
|
+
}),
|
|
3289
|
+
fields.json({
|
|
3290
|
+
name: "address",
|
|
3291
|
+
label: "Company Address"
|
|
3292
|
+
}),
|
|
3293
|
+
fields.text({
|
|
3294
|
+
name: "timezone",
|
|
3295
|
+
label: "Primary Timezone",
|
|
3296
|
+
defaultValue: "UTC"
|
|
3297
|
+
}),
|
|
3298
|
+
fields.json({
|
|
3299
|
+
name: "workSchedule",
|
|
3300
|
+
label: "Standard Work Schedule",
|
|
3301
|
+
defaultValue: {
|
|
3302
|
+
startTime: options.workScheduleStart ?? "09:00",
|
|
3303
|
+
endTime: options.workScheduleEnd ?? "17:00",
|
|
3304
|
+
standardHoursPerDay: options.standardWorkDayHours ?? 8,
|
|
3305
|
+
gracePeriodMinutes: options.gracePeriodMinutes ?? 15,
|
|
3306
|
+
workDays: [
|
|
3307
|
+
1,
|
|
3308
|
+
2,
|
|
3309
|
+
3,
|
|
3310
|
+
4,
|
|
3311
|
+
5
|
|
3312
|
+
]
|
|
3313
|
+
}
|
|
3314
|
+
}),
|
|
3315
|
+
fields.select({
|
|
3316
|
+
name: "status",
|
|
3317
|
+
label: "Status",
|
|
3318
|
+
options: ["active", "inactive"],
|
|
3319
|
+
defaultValue: "active"
|
|
3320
|
+
})
|
|
3321
|
+
]
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Creates the collection configuration for Employees.
|
|
3326
|
+
*/
|
|
3327
|
+
function createEmployeeCollection(options = {}) {
|
|
3328
|
+
const slug = options.employeeCollectionSlug ?? "employees";
|
|
3329
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3330
|
+
return collection({
|
|
3331
|
+
slug,
|
|
3332
|
+
label: "Employees",
|
|
3333
|
+
singularLabel: "Employee",
|
|
3334
|
+
description: "Employee profiles, job assignments, emergency contacts, and attached records",
|
|
3335
|
+
timestamps: true,
|
|
3336
|
+
revisions: true,
|
|
3337
|
+
draftable: true,
|
|
3338
|
+
taxonomies: [options.departmentsTaxonomySlug ?? "hrms_departments", options.designationsTaxonomySlug ?? "hrms_designations"],
|
|
3339
|
+
defaultSort: {
|
|
3340
|
+
field: "createdAt",
|
|
3341
|
+
direction: "desc"
|
|
3342
|
+
},
|
|
3343
|
+
fields: [
|
|
3344
|
+
fields.relationship({
|
|
3345
|
+
name: "employerId",
|
|
3346
|
+
label: "Employer",
|
|
3347
|
+
targetCollection: employerSlug,
|
|
3348
|
+
required: true
|
|
3349
|
+
}),
|
|
3350
|
+
fields.text({
|
|
3351
|
+
name: "userId",
|
|
3352
|
+
label: "Associated User ID",
|
|
3353
|
+
description: "Optional reference to an authenticated user account"
|
|
3354
|
+
}),
|
|
3355
|
+
fields.text({
|
|
3356
|
+
name: "employeeNumber",
|
|
3357
|
+
label: "Employee Number",
|
|
3358
|
+
required: true,
|
|
3359
|
+
unique: true
|
|
3360
|
+
}),
|
|
3361
|
+
fields.text({
|
|
3362
|
+
name: "firstName",
|
|
3363
|
+
label: "First Name",
|
|
3364
|
+
required: true
|
|
3365
|
+
}),
|
|
3366
|
+
fields.text({
|
|
3367
|
+
name: "lastName",
|
|
3368
|
+
label: "Last Name",
|
|
3369
|
+
required: true
|
|
3370
|
+
}),
|
|
3371
|
+
fields.text({
|
|
3372
|
+
name: "email",
|
|
3373
|
+
label: "Work Email",
|
|
3374
|
+
required: true
|
|
3375
|
+
}),
|
|
3376
|
+
fields.text({
|
|
3377
|
+
name: "phone",
|
|
3378
|
+
label: "Contact Phone"
|
|
3379
|
+
}),
|
|
3380
|
+
fields.image({
|
|
3381
|
+
name: "avatar",
|
|
3382
|
+
label: "Profile Picture"
|
|
3383
|
+
}),
|
|
3384
|
+
fields.text({
|
|
3385
|
+
name: "jobTitle",
|
|
3386
|
+
label: "Job Title"
|
|
3387
|
+
}),
|
|
3388
|
+
fields.select({
|
|
3389
|
+
name: "employmentType",
|
|
3390
|
+
label: "Employment Type",
|
|
3391
|
+
options: [
|
|
3392
|
+
"full_time",
|
|
3393
|
+
"part_time",
|
|
3394
|
+
"contractor",
|
|
3395
|
+
"intern"
|
|
3396
|
+
],
|
|
3397
|
+
defaultValue: "full_time"
|
|
3398
|
+
}),
|
|
3399
|
+
fields.select({
|
|
3400
|
+
name: "status",
|
|
3401
|
+
label: "Status",
|
|
3402
|
+
options: [
|
|
3403
|
+
"active",
|
|
3404
|
+
"on_leave",
|
|
3405
|
+
"terminated",
|
|
3406
|
+
"suspended"
|
|
3407
|
+
],
|
|
3408
|
+
defaultValue: "active"
|
|
3409
|
+
}),
|
|
3410
|
+
fields.date({
|
|
3411
|
+
name: "hireDate",
|
|
3412
|
+
label: "Hire Date",
|
|
3413
|
+
required: true
|
|
3414
|
+
}),
|
|
3415
|
+
fields.date({
|
|
3416
|
+
name: "terminationDate",
|
|
3417
|
+
label: "Termination Date"
|
|
3418
|
+
}),
|
|
3419
|
+
fields.relationship({
|
|
3420
|
+
name: "managerId",
|
|
3421
|
+
label: "Reporting Manager",
|
|
3422
|
+
targetCollection: slug
|
|
3423
|
+
}),
|
|
3424
|
+
fields.json({
|
|
3425
|
+
name: "emergencyContact",
|
|
3426
|
+
label: "Emergency Contact"
|
|
3427
|
+
}),
|
|
3428
|
+
fields.repeater({
|
|
3429
|
+
name: "documents",
|
|
3430
|
+
label: "Attached Documents & Contracts",
|
|
3431
|
+
fields: [
|
|
3432
|
+
fields.text({
|
|
3433
|
+
name: "title",
|
|
3434
|
+
label: "Document Title",
|
|
3435
|
+
required: true
|
|
3436
|
+
}),
|
|
3437
|
+
fields.text({
|
|
3438
|
+
name: "fileUrl",
|
|
3439
|
+
label: "File URL",
|
|
3440
|
+
required: true
|
|
3441
|
+
}),
|
|
3442
|
+
fields.text({
|
|
3443
|
+
name: "category",
|
|
3444
|
+
label: "Category"
|
|
3445
|
+
}),
|
|
3446
|
+
fields.text({
|
|
3447
|
+
name: "uploadedAt",
|
|
3448
|
+
label: "Uploaded Date"
|
|
3449
|
+
})
|
|
3450
|
+
]
|
|
3451
|
+
}),
|
|
3452
|
+
fields.number({
|
|
3453
|
+
name: "salary",
|
|
3454
|
+
label: "Base Salary"
|
|
3455
|
+
}),
|
|
3456
|
+
fields.text({
|
|
3457
|
+
name: "notes",
|
|
3458
|
+
label: "Internal Notes"
|
|
3459
|
+
})
|
|
3460
|
+
]
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Creates the collection configuration for Daily Attendance.
|
|
3465
|
+
*/
|
|
3466
|
+
function createAttendanceCollection(options = {}) {
|
|
3467
|
+
const slug = options.attendanceCollectionSlug ?? "attendance";
|
|
3468
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3469
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3470
|
+
return collection({
|
|
3471
|
+
slug,
|
|
3472
|
+
label: "Attendance",
|
|
3473
|
+
singularLabel: "Attendance Record",
|
|
3474
|
+
description: "Daily check-in and check-out logs, hours worked, overtime, and punctuality status",
|
|
3475
|
+
timestamps: true,
|
|
3476
|
+
revisions: false,
|
|
3477
|
+
draftable: false,
|
|
3478
|
+
defaultSort: {
|
|
3479
|
+
field: "date",
|
|
3480
|
+
direction: "desc"
|
|
3481
|
+
},
|
|
3482
|
+
fields: [
|
|
3483
|
+
fields.relationship({
|
|
3484
|
+
name: "employerId",
|
|
3485
|
+
label: "Employer",
|
|
3486
|
+
targetCollection: employerSlug,
|
|
3487
|
+
required: true
|
|
3488
|
+
}),
|
|
3489
|
+
fields.relationship({
|
|
3490
|
+
name: "employeeId",
|
|
3491
|
+
label: "Employee",
|
|
3492
|
+
targetCollection: employeeSlug,
|
|
3493
|
+
required: true
|
|
3494
|
+
}),
|
|
3495
|
+
fields.date({
|
|
3496
|
+
name: "date",
|
|
3497
|
+
label: "Date",
|
|
3498
|
+
required: true
|
|
3499
|
+
}),
|
|
3500
|
+
fields.text({
|
|
3501
|
+
name: "checkInAt",
|
|
3502
|
+
label: "Check-In Timestamp",
|
|
3503
|
+
required: true
|
|
3504
|
+
}),
|
|
3505
|
+
fields.text({
|
|
3506
|
+
name: "checkOutAt",
|
|
3507
|
+
label: "Check-Out Timestamp"
|
|
3508
|
+
}),
|
|
3509
|
+
fields.number({
|
|
3510
|
+
name: "totalHours",
|
|
3511
|
+
label: "Total Hours",
|
|
3512
|
+
defaultValue: 0
|
|
3513
|
+
}),
|
|
3514
|
+
fields.number({
|
|
3515
|
+
name: "overtimeHours",
|
|
3516
|
+
label: "Overtime Hours",
|
|
3517
|
+
defaultValue: 0
|
|
3518
|
+
}),
|
|
3519
|
+
fields.select({
|
|
3520
|
+
name: "status",
|
|
3521
|
+
label: "Status",
|
|
3522
|
+
options: [
|
|
3523
|
+
"present",
|
|
3524
|
+
"late",
|
|
3525
|
+
"half_day",
|
|
3526
|
+
"absent",
|
|
3527
|
+
"on_leave"
|
|
3528
|
+
],
|
|
3529
|
+
defaultValue: "present"
|
|
3530
|
+
}),
|
|
3531
|
+
fields.text({
|
|
3532
|
+
name: "location",
|
|
3533
|
+
label: "Location / IP / Geofence"
|
|
3534
|
+
}),
|
|
3535
|
+
fields.text({
|
|
3536
|
+
name: "notes",
|
|
3537
|
+
label: "Notes"
|
|
3538
|
+
})
|
|
3539
|
+
]
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Creates the collection configuration for Leave Types.
|
|
3544
|
+
*/
|
|
3545
|
+
function createLeaveTypeCollection(options = {}) {
|
|
3546
|
+
const slug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3547
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3548
|
+
return collection({
|
|
3549
|
+
slug,
|
|
3550
|
+
label: "Leave Types",
|
|
3551
|
+
singularLabel: "Leave Type",
|
|
3552
|
+
description: "Available leave categories (Annual, Sick, Unpaid) and yearly quotas",
|
|
3553
|
+
timestamps: true,
|
|
3554
|
+
revisions: true,
|
|
3555
|
+
draftable: false,
|
|
3556
|
+
fields: [
|
|
3557
|
+
fields.relationship({
|
|
3558
|
+
name: "employerId",
|
|
3559
|
+
label: "Employer",
|
|
3560
|
+
targetCollection: employerSlug
|
|
3561
|
+
}),
|
|
3562
|
+
fields.text({
|
|
3563
|
+
name: "name",
|
|
3564
|
+
label: "Leave Name",
|
|
3565
|
+
required: true
|
|
3566
|
+
}),
|
|
3567
|
+
fields.text({
|
|
3568
|
+
name: "code",
|
|
3569
|
+
label: "Leave Code",
|
|
3570
|
+
required: true
|
|
3571
|
+
}),
|
|
3572
|
+
fields.number({
|
|
3573
|
+
name: "daysAllowedPerYear",
|
|
3574
|
+
label: "Days Allowed Per Year",
|
|
3575
|
+
required: true,
|
|
3576
|
+
defaultValue: 15,
|
|
3577
|
+
min: 0
|
|
3578
|
+
}),
|
|
3579
|
+
fields.boolean({
|
|
3580
|
+
name: "paid",
|
|
3581
|
+
label: "Paid Leave",
|
|
3582
|
+
defaultValue: true
|
|
3583
|
+
}),
|
|
3584
|
+
fields.boolean({
|
|
3585
|
+
name: "requiresApproval",
|
|
3586
|
+
label: "Requires Manager Approval",
|
|
3587
|
+
defaultValue: true
|
|
3588
|
+
}),
|
|
3589
|
+
fields.text({
|
|
3590
|
+
name: "color",
|
|
3591
|
+
label: "Display Color Code"
|
|
3592
|
+
}),
|
|
3593
|
+
fields.text({
|
|
3594
|
+
name: "description",
|
|
3595
|
+
label: "Description / Policy"
|
|
3596
|
+
})
|
|
3597
|
+
]
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
/**
|
|
3601
|
+
* Creates the collection configuration for Leave Requests.
|
|
3602
|
+
*/
|
|
3603
|
+
function createLeaveRequestCollection(options = {}) {
|
|
3604
|
+
const slug = options.leaveRequestCollectionSlug ?? "leave_requests";
|
|
3605
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3606
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3607
|
+
const leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3608
|
+
return collection({
|
|
3609
|
+
slug,
|
|
3610
|
+
label: "Leave Requests",
|
|
3611
|
+
singularLabel: "Leave Request",
|
|
3612
|
+
description: "Employee time-off requests, approval tracking, and deducted balances",
|
|
3613
|
+
timestamps: true,
|
|
3614
|
+
revisions: true,
|
|
3615
|
+
draftable: false,
|
|
3616
|
+
defaultSort: {
|
|
3617
|
+
field: "createdAt",
|
|
3618
|
+
direction: "desc"
|
|
3619
|
+
},
|
|
3620
|
+
fields: [
|
|
3621
|
+
fields.relationship({
|
|
3622
|
+
name: "employerId",
|
|
3623
|
+
label: "Employer",
|
|
3624
|
+
targetCollection: employerSlug,
|
|
3625
|
+
required: true
|
|
3626
|
+
}),
|
|
3627
|
+
fields.relationship({
|
|
3628
|
+
name: "employeeId",
|
|
3629
|
+
label: "Employee",
|
|
3630
|
+
targetCollection: employeeSlug,
|
|
3631
|
+
required: true
|
|
3632
|
+
}),
|
|
3633
|
+
fields.relationship({
|
|
3634
|
+
name: "leaveTypeId",
|
|
3635
|
+
label: "Leave Type",
|
|
3636
|
+
targetCollection: leaveTypeSlug,
|
|
3637
|
+
required: true
|
|
3638
|
+
}),
|
|
3639
|
+
fields.date({
|
|
3640
|
+
name: "startDate",
|
|
3641
|
+
label: "Start Date",
|
|
3642
|
+
required: true
|
|
3643
|
+
}),
|
|
3644
|
+
fields.date({
|
|
3645
|
+
name: "endDate",
|
|
3646
|
+
label: "End Date",
|
|
3647
|
+
required: true
|
|
3648
|
+
}),
|
|
3649
|
+
fields.number({
|
|
3650
|
+
name: "daysCount",
|
|
3651
|
+
label: "Days Count",
|
|
3652
|
+
required: true,
|
|
3653
|
+
min: .5
|
|
3654
|
+
}),
|
|
3655
|
+
fields.text({
|
|
3656
|
+
name: "reason",
|
|
3657
|
+
label: "Reason"
|
|
3658
|
+
}),
|
|
3659
|
+
fields.select({
|
|
3660
|
+
name: "status",
|
|
3661
|
+
label: "Request Status",
|
|
3662
|
+
options: [
|
|
3663
|
+
"pending",
|
|
3664
|
+
"approved",
|
|
3665
|
+
"rejected",
|
|
3666
|
+
"cancelled"
|
|
3667
|
+
],
|
|
3668
|
+
defaultValue: "pending"
|
|
3669
|
+
}),
|
|
3670
|
+
fields.relationship({
|
|
3671
|
+
name: "approvedBy",
|
|
3672
|
+
label: "Approved / Rejected By",
|
|
3673
|
+
targetCollection: employeeSlug
|
|
3674
|
+
}),
|
|
3675
|
+
fields.text({
|
|
3676
|
+
name: "approvedAt",
|
|
3677
|
+
label: "Decision Timestamp"
|
|
3678
|
+
}),
|
|
3679
|
+
fields.text({
|
|
3680
|
+
name: "rejectionReason",
|
|
3681
|
+
label: "Rejection Reason"
|
|
3682
|
+
})
|
|
3683
|
+
]
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Creates the standard HRMS taxonomies: departments and designations.
|
|
3688
|
+
*/
|
|
3689
|
+
function createHRMSTaxonomies(options = {}) {
|
|
3690
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3691
|
+
const deptSlug = options.departmentsTaxonomySlug ?? "hrms_departments";
|
|
3692
|
+
const desigSlug = options.designationsTaxonomySlug ?? "hrms_designations";
|
|
3693
|
+
return [{
|
|
3694
|
+
slug: deptSlug,
|
|
3695
|
+
label: "Departments",
|
|
3696
|
+
singularLabel: "Department",
|
|
3697
|
+
hierarchical: true,
|
|
3698
|
+
postTypes: [employeeSlug],
|
|
3699
|
+
description: "Hierarchical departmental tree (e.g. Engineering, Sales, HR)"
|
|
3700
|
+
}, {
|
|
3701
|
+
slug: desigSlug,
|
|
3702
|
+
label: "Designations",
|
|
3703
|
+
singularLabel: "Designation",
|
|
3704
|
+
hierarchical: false,
|
|
3705
|
+
postTypes: [employeeSlug],
|
|
3706
|
+
description: "Job titles and designations across the workforce"
|
|
3707
|
+
}];
|
|
3708
|
+
}
|
|
3709
|
+
//#endregion
|
|
3710
|
+
//#region src/plugins/hrms/service.ts
|
|
3711
|
+
var HRMSService = class {
|
|
3712
|
+
engine;
|
|
3713
|
+
options;
|
|
3714
|
+
employerSlug;
|
|
3715
|
+
employeeSlug;
|
|
3716
|
+
attendanceSlug;
|
|
3717
|
+
leaveTypeSlug;
|
|
3718
|
+
leaveRequestSlug;
|
|
3719
|
+
departmentsTaxonomy;
|
|
3720
|
+
designationsTaxonomy;
|
|
3721
|
+
standardWorkDayHours;
|
|
3722
|
+
workScheduleStart;
|
|
3723
|
+
workScheduleEnd;
|
|
3724
|
+
gracePeriodMinutes;
|
|
3725
|
+
constructor(engine, options = {}) {
|
|
3726
|
+
this.engine = engine;
|
|
3727
|
+
this.options = options;
|
|
3728
|
+
this.employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3729
|
+
this.employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3730
|
+
this.attendanceSlug = options.attendanceCollectionSlug ?? "attendance";
|
|
3731
|
+
this.leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3732
|
+
this.leaveRequestSlug = options.leaveRequestCollectionSlug ?? "leave_requests";
|
|
3733
|
+
this.departmentsTaxonomy = options.departmentsTaxonomySlug ?? "hrms_departments";
|
|
3734
|
+
this.designationsTaxonomy = options.designationsTaxonomySlug ?? "hrms_designations";
|
|
3735
|
+
this.standardWorkDayHours = options.standardWorkDayHours ?? 8;
|
|
3736
|
+
this.workScheduleStart = options.workScheduleStart ?? "09:00";
|
|
3737
|
+
this.workScheduleEnd = options.workScheduleEnd ?? "17:00";
|
|
3738
|
+
this.gracePeriodMinutes = options.gracePeriodMinutes ?? 15;
|
|
3739
|
+
}
|
|
3740
|
+
get employersCollection() {
|
|
3741
|
+
return this.engine.collection(this.employerSlug);
|
|
3742
|
+
}
|
|
3743
|
+
get employeesCollection() {
|
|
3744
|
+
return this.engine.collection(this.employeeSlug);
|
|
3745
|
+
}
|
|
3746
|
+
get attendanceCollection() {
|
|
3747
|
+
return this.engine.collection(this.attendanceSlug);
|
|
3748
|
+
}
|
|
3749
|
+
get leaveTypesCollection() {
|
|
3750
|
+
return this.engine.collection(this.leaveTypeSlug);
|
|
3751
|
+
}
|
|
3752
|
+
get leaveRequestsCollection() {
|
|
3753
|
+
return this.engine.collection(this.leaveRequestSlug);
|
|
3754
|
+
}
|
|
3755
|
+
async createEmployer(input, authorId) {
|
|
3756
|
+
const employerData = {
|
|
3757
|
+
companyName: input.companyName,
|
|
3758
|
+
legalName: input.legalName,
|
|
3759
|
+
taxId: input.taxId,
|
|
3760
|
+
email: input.email,
|
|
3761
|
+
phone: input.phone,
|
|
3762
|
+
website: input.website,
|
|
3763
|
+
logo: input.logo,
|
|
3764
|
+
address: input.address,
|
|
3765
|
+
timezone: input.timezone ?? "UTC",
|
|
3766
|
+
workSchedule: input.workSchedule ?? {
|
|
3767
|
+
startTime: this.workScheduleStart,
|
|
3768
|
+
endTime: this.workScheduleEnd,
|
|
3769
|
+
standardHoursPerDay: this.standardWorkDayHours,
|
|
3770
|
+
gracePeriodMinutes: this.gracePeriodMinutes,
|
|
3771
|
+
workDays: [
|
|
3772
|
+
1,
|
|
3773
|
+
2,
|
|
3774
|
+
3,
|
|
3775
|
+
4,
|
|
3776
|
+
5
|
|
3777
|
+
]
|
|
3778
|
+
},
|
|
3779
|
+
status: input.status ?? "active"
|
|
3780
|
+
};
|
|
3781
|
+
const employer = await this.employersCollection.create({
|
|
3782
|
+
title: input.companyName,
|
|
3783
|
+
status: input.status === "inactive" ? "draft" : "published",
|
|
3784
|
+
data: employerData
|
|
3785
|
+
}, authorId);
|
|
3786
|
+
await this.engine.hooks.doAction("hrms.employer_created", employer);
|
|
3787
|
+
return employer;
|
|
3788
|
+
}
|
|
3789
|
+
async getEmployer(id) {
|
|
3790
|
+
return this.employersCollection.findById(id);
|
|
3791
|
+
}
|
|
3792
|
+
async getEmployerBySlug(slug) {
|
|
3793
|
+
return this.employersCollection.findBySlug(slug);
|
|
3794
|
+
}
|
|
3795
|
+
async updateEmployer(id, input, authorId) {
|
|
3796
|
+
const existing = await this.getEmployer(id);
|
|
3797
|
+
if (!existing) return null;
|
|
3798
|
+
const updatedData = {
|
|
3799
|
+
...existing.data,
|
|
3800
|
+
...input.companyName !== void 0 ? { companyName: input.companyName } : {},
|
|
3801
|
+
...input.legalName !== void 0 ? { legalName: input.legalName } : {},
|
|
3802
|
+
...input.taxId !== void 0 ? { taxId: input.taxId } : {},
|
|
3803
|
+
...input.email !== void 0 ? { email: input.email } : {},
|
|
3804
|
+
...input.phone !== void 0 ? { phone: input.phone } : {},
|
|
3805
|
+
...input.website !== void 0 ? { website: input.website } : {},
|
|
3806
|
+
...input.logo !== void 0 ? { logo: input.logo } : {},
|
|
3807
|
+
...input.address !== void 0 ? { address: input.address } : {},
|
|
3808
|
+
...input.timezone !== void 0 ? { timezone: input.timezone } : {},
|
|
3809
|
+
...input.workSchedule !== void 0 ? { workSchedule: input.workSchedule } : {},
|
|
3810
|
+
...input.status !== void 0 ? { status: input.status } : {}
|
|
3811
|
+
};
|
|
3812
|
+
const updated = await this.employersCollection.update(id, {
|
|
3813
|
+
title: input.companyName ?? existing.title,
|
|
3814
|
+
data: updatedData
|
|
3815
|
+
}, authorId);
|
|
3816
|
+
if (updated) await this.engine.hooks.doAction("hrms.employer_updated", updated);
|
|
3817
|
+
return updated;
|
|
3818
|
+
}
|
|
3819
|
+
async listEmployers(query = {}) {
|
|
3820
|
+
const limit = query.limit ?? 20;
|
|
3821
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
3822
|
+
let items = (await this.employersCollection.find({ limit: 500 })).items;
|
|
3823
|
+
if (query.status) items = items.filter((item) => item.data.status === query.status);
|
|
3824
|
+
const total = items.length;
|
|
3825
|
+
return {
|
|
3826
|
+
items: items.slice(offset, offset + limit),
|
|
3827
|
+
total,
|
|
3828
|
+
limit,
|
|
3829
|
+
offset,
|
|
3830
|
+
hasMore: offset + limit < total
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
async createEmployee(input, authorId) {
|
|
3834
|
+
if (!await this.getEmployer(input.employerId)) throw new Error(`[HRMSService] Employer with ID '${input.employerId}' not found.`);
|
|
3835
|
+
if (await this.getEmployeeByNumber(input.employerId, input.employeeNumber)) throw new Error(`[HRMSService] Employee number '${input.employeeNumber}' is already registered for this employer.`);
|
|
3836
|
+
const employeeData = {
|
|
3837
|
+
employerId: input.employerId,
|
|
3838
|
+
userId: input.userId,
|
|
3839
|
+
employeeNumber: input.employeeNumber,
|
|
3840
|
+
firstName: input.firstName,
|
|
3841
|
+
lastName: input.lastName,
|
|
3842
|
+
email: input.email,
|
|
3843
|
+
phone: input.phone,
|
|
3844
|
+
avatar: input.avatar,
|
|
3845
|
+
jobTitle: input.jobTitle,
|
|
3846
|
+
employmentType: input.employmentType ?? "full_time",
|
|
3847
|
+
status: input.status ?? "active",
|
|
3848
|
+
hireDate: input.hireDate,
|
|
3849
|
+
managerId: input.managerId,
|
|
3850
|
+
emergencyContact: input.emergencyContact,
|
|
3851
|
+
documents: input.documents ?? [],
|
|
3852
|
+
salary: input.salary,
|
|
3853
|
+
notes: input.notes
|
|
3854
|
+
};
|
|
3855
|
+
const title = `${input.firstName} ${input.lastName}`;
|
|
3856
|
+
const employee = await this.employeesCollection.create({
|
|
3857
|
+
title,
|
|
3858
|
+
status: input.status === "terminated" ? "draft" : "published",
|
|
3859
|
+
data: employeeData
|
|
3860
|
+
}, authorId);
|
|
3861
|
+
if (input.departmentSlug) {
|
|
3862
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
|
|
3863
|
+
if (term) await this.engine.taxonomies.assignTerms(employee.id, [term.id]);
|
|
3864
|
+
}
|
|
3865
|
+
await this.engine.hooks.doAction("hrms.employee_created", employee);
|
|
3866
|
+
return employee;
|
|
3867
|
+
}
|
|
3868
|
+
async getEmployee(id) {
|
|
3869
|
+
return this.employeesCollection.findById(id);
|
|
3870
|
+
}
|
|
3871
|
+
async getEmployeeByNumber(employerId, employeeNumber) {
|
|
3872
|
+
return (await this.employeesCollection.find({ limit: 500 })).items.find((e) => e.data.employerId === employerId && e.data.employeeNumber === employeeNumber) ?? null;
|
|
3873
|
+
}
|
|
3874
|
+
async updateEmployee(id, input, authorId) {
|
|
3875
|
+
const existing = await this.getEmployee(id);
|
|
3876
|
+
if (!existing) return null;
|
|
3877
|
+
const updatedData = {
|
|
3878
|
+
...existing.data,
|
|
3879
|
+
...input.employerId !== void 0 ? { employerId: input.employerId } : {},
|
|
3880
|
+
...input.userId !== void 0 ? { userId: input.userId } : {},
|
|
3881
|
+
...input.employeeNumber !== void 0 ? { employeeNumber: input.employeeNumber } : {},
|
|
3882
|
+
...input.firstName !== void 0 ? { firstName: input.firstName } : {},
|
|
3883
|
+
...input.lastName !== void 0 ? { lastName: input.lastName } : {},
|
|
3884
|
+
...input.email !== void 0 ? { email: input.email } : {},
|
|
3885
|
+
...input.phone !== void 0 ? { phone: input.phone } : {},
|
|
3886
|
+
...input.avatar !== void 0 ? { avatar: input.avatar } : {},
|
|
3887
|
+
...input.jobTitle !== void 0 ? { jobTitle: input.jobTitle } : {},
|
|
3888
|
+
...input.employmentType !== void 0 ? { employmentType: input.employmentType } : {},
|
|
3889
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
3890
|
+
...input.hireDate !== void 0 ? { hireDate: input.hireDate } : {},
|
|
3891
|
+
...input.terminationDate !== void 0 ? { terminationDate: input.terminationDate } : {},
|
|
3892
|
+
...input.managerId !== void 0 ? { managerId: input.managerId } : {},
|
|
3893
|
+
...input.emergencyContact !== void 0 ? { emergencyContact: input.emergencyContact } : {},
|
|
3894
|
+
...input.documents !== void 0 ? { documents: input.documents } : {},
|
|
3895
|
+
...input.salary !== void 0 ? { salary: input.salary } : {},
|
|
3896
|
+
...input.notes !== void 0 ? { notes: input.notes } : {}
|
|
3897
|
+
};
|
|
3898
|
+
const title = input.firstName || input.lastName ? `${updatedData.firstName} ${updatedData.lastName}` : existing.title;
|
|
3899
|
+
const updated = await this.employeesCollection.update(id, {
|
|
3900
|
+
title,
|
|
3901
|
+
data: updatedData
|
|
3902
|
+
}, authorId);
|
|
3903
|
+
if (updated) {
|
|
3904
|
+
if (input.departmentSlug) {
|
|
3905
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
|
|
3906
|
+
if (term) await this.engine.taxonomies.assignTerms(id, [term.id]);
|
|
3907
|
+
}
|
|
3908
|
+
await this.engine.hooks.doAction("hrms.employee_updated", updated);
|
|
3909
|
+
}
|
|
3910
|
+
return updated;
|
|
3911
|
+
}
|
|
3912
|
+
async deleteEmployee(id) {
|
|
3913
|
+
const deleted = await this.employeesCollection.delete(id);
|
|
3914
|
+
if (deleted) await this.engine.hooks.doAction("hrms.employee_deleted", id);
|
|
3915
|
+
return deleted;
|
|
3916
|
+
}
|
|
3917
|
+
async listEmployees(query = {}) {
|
|
3918
|
+
const limit = query.limit ?? 20;
|
|
3919
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
3920
|
+
let termIds;
|
|
3921
|
+
if (query.department) {
|
|
3922
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, query.department);
|
|
3923
|
+
if (term) termIds = [term.id];
|
|
3924
|
+
else return {
|
|
3925
|
+
items: [],
|
|
3926
|
+
total: 0,
|
|
3927
|
+
limit,
|
|
3928
|
+
offset,
|
|
3929
|
+
hasMore: false
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
let items = (await this.employeesCollection.find({
|
|
3933
|
+
termIds,
|
|
3934
|
+
limit: 1e3
|
|
3935
|
+
})).items;
|
|
3936
|
+
if (query.employerId) items = items.filter((e) => e.data.employerId === query.employerId);
|
|
3937
|
+
if (query.employmentType) items = items.filter((e) => e.data.employmentType === query.employmentType);
|
|
3938
|
+
if (query.status) items = items.filter((e) => e.data.status === query.status);
|
|
3939
|
+
if (query.search) {
|
|
3940
|
+
const s = query.search.toLowerCase();
|
|
3941
|
+
items = items.filter((e) => e.data.firstName.toLowerCase().includes(s) || e.data.lastName.toLowerCase().includes(s) || e.data.email.toLowerCase().includes(s) || e.data.employeeNumber.toLowerCase().includes(s) || e.data.jobTitle && e.data.jobTitle.toLowerCase().includes(s));
|
|
3942
|
+
}
|
|
3943
|
+
const total = items.length;
|
|
3944
|
+
return {
|
|
3945
|
+
items: items.slice(offset, offset + limit),
|
|
3946
|
+
total,
|
|
3947
|
+
limit,
|
|
3948
|
+
offset,
|
|
3949
|
+
hasMore: offset + limit < total
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
async getDirectReports(managerId) {
|
|
3953
|
+
return (await this.employeesCollection.find({ limit: 1e3 })).items.filter((e) => e.data.managerId === managerId);
|
|
3954
|
+
}
|
|
3955
|
+
parseTimeToMinutes(timeStr) {
|
|
3956
|
+
const [hours, minutes] = timeStr.split(":").map((v) => parseInt(v, 10));
|
|
3957
|
+
return (hours || 0) * 60 + (minutes || 0);
|
|
3958
|
+
}
|
|
3959
|
+
formatDateString(date, timezone) {
|
|
3960
|
+
if (!timezone || timezone === "UTC") return date.toISOString().slice(0, 10);
|
|
3961
|
+
try {
|
|
3962
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
3963
|
+
timeZone: timezone,
|
|
3964
|
+
year: "numeric",
|
|
3965
|
+
month: "2-digit",
|
|
3966
|
+
day: "2-digit"
|
|
3967
|
+
}).format(date);
|
|
3968
|
+
} catch {
|
|
3969
|
+
return date.toISOString().slice(0, 10);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
getHoursAndMinutes(date, timezone) {
|
|
3973
|
+
if (!timezone || timezone === "UTC") return {
|
|
3974
|
+
hours: date.getUTCHours(),
|
|
3975
|
+
minutes: date.getUTCMinutes()
|
|
3976
|
+
};
|
|
3977
|
+
try {
|
|
3978
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
3979
|
+
timeZone: timezone,
|
|
3980
|
+
hour: "numeric",
|
|
3981
|
+
minute: "numeric",
|
|
3982
|
+
hour12: false
|
|
3983
|
+
}).formatToParts(date);
|
|
3984
|
+
const hoursPart = parts.find((p) => p.type === "hour");
|
|
3985
|
+
const minutesPart = parts.find((p) => p.type === "minute");
|
|
3986
|
+
return {
|
|
3987
|
+
hours: hoursPart ? parseInt(hoursPart.value, 10) : date.getUTCHours(),
|
|
3988
|
+
minutes: minutesPart ? parseInt(minutesPart.value, 10) : date.getUTCMinutes()
|
|
3989
|
+
};
|
|
3990
|
+
} catch {
|
|
3991
|
+
return {
|
|
3992
|
+
hours: date.getUTCHours(),
|
|
3993
|
+
minutes: date.getUTCMinutes()
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
/**
|
|
3998
|
+
* Check in an employee for today (or specified timestamp).
|
|
3999
|
+
*/
|
|
4000
|
+
async checkIn(input) {
|
|
4001
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4002
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4003
|
+
const employer = await this.getEmployer(employee.data.employerId);
|
|
4004
|
+
const timezone = employer?.data?.timezone ?? "UTC";
|
|
4005
|
+
const checkInDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
|
|
4006
|
+
const dateStr = this.formatDateString(checkInDate, timezone);
|
|
4007
|
+
const existing = await this.getDailyAttendance(employee.id, dateStr);
|
|
4008
|
+
if (existing && existing.data.checkInAt) throw new Error(`[HRMSService] Employee '${employee.id}' is already checked in for date ${dateStr}.`);
|
|
4009
|
+
const schedule = employer?.data?.workSchedule;
|
|
4010
|
+
const schedStart = schedule?.startTime ?? this.workScheduleStart;
|
|
4011
|
+
const grace = schedule?.gracePeriodMinutes ?? this.gracePeriodMinutes;
|
|
4012
|
+
const schedMinutes = this.parseTimeToMinutes(schedStart);
|
|
4013
|
+
const { hours: punchHours, minutes: punchMins } = this.getHoursAndMinutes(checkInDate, timezone);
|
|
4014
|
+
const status = punchHours * 60 + punchMins > schedMinutes + grace ? "late" : "present";
|
|
4015
|
+
const attendanceData = {
|
|
4016
|
+
employerId: employee.data.employerId,
|
|
4017
|
+
employeeId: employee.id,
|
|
4018
|
+
date: dateStr,
|
|
4019
|
+
checkInAt: checkInDate.toISOString(),
|
|
4020
|
+
totalHours: 0,
|
|
4021
|
+
overtimeHours: 0,
|
|
4022
|
+
status,
|
|
4023
|
+
location: input.location,
|
|
4024
|
+
notes: input.notes
|
|
4025
|
+
};
|
|
4026
|
+
const attendance = await this.attendanceCollection.create({
|
|
4027
|
+
title: `${employee.title} - ${dateStr}`,
|
|
4028
|
+
status: "published",
|
|
4029
|
+
data: attendanceData
|
|
4030
|
+
});
|
|
4031
|
+
await this.engine.hooks.doAction("hrms.checked_in", attendance, employee);
|
|
4032
|
+
return attendance;
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Check out an employee for today (or specified timestamp).
|
|
4036
|
+
*/
|
|
4037
|
+
async checkOut(input) {
|
|
4038
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4039
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4040
|
+
const employer = await this.getEmployer(employee.data.employerId);
|
|
4041
|
+
const timezone = employer?.data?.timezone ?? "UTC";
|
|
4042
|
+
const checkOutDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
|
|
4043
|
+
const dateStr = this.formatDateString(checkOutDate, timezone);
|
|
4044
|
+
const record = await this.getDailyAttendance(employee.id, dateStr);
|
|
4045
|
+
if (!record || !record.data.checkInAt) throw new Error(`[HRMSService] No active check-in record found for employee '${employee.id}' on ${dateStr}.`);
|
|
4046
|
+
if (record.data.checkOutAt) throw new Error(`[HRMSService] Employee '${employee.id}' has already checked out for date ${dateStr}.`);
|
|
4047
|
+
const checkInDate = new Date(record.data.checkInAt);
|
|
4048
|
+
const durationMs = Math.max(0, checkOutDate.getTime() - checkInDate.getTime());
|
|
4049
|
+
const totalHours = Math.round(durationMs / (1e3 * 60 * 60) * 100) / 100;
|
|
4050
|
+
const standardHours = employer?.data?.workSchedule?.standardHoursPerDay ?? this.standardWorkDayHours;
|
|
4051
|
+
const overtimeHours = Math.max(0, Math.round((totalHours - standardHours) * 100) / 100);
|
|
4052
|
+
let status = record.data.status;
|
|
4053
|
+
if (totalHours < standardHours / 2 && status === "present") status = "half_day";
|
|
4054
|
+
const updatedData = {
|
|
4055
|
+
...record.data,
|
|
4056
|
+
checkOutAt: checkOutDate.toISOString(),
|
|
4057
|
+
totalHours,
|
|
4058
|
+
overtimeHours,
|
|
4059
|
+
status,
|
|
4060
|
+
...input.location ? { location: input.location } : {},
|
|
4061
|
+
...input.notes ? { notes: input.notes } : {}
|
|
4062
|
+
};
|
|
4063
|
+
const updated = await this.attendanceCollection.update(record.id, { data: updatedData });
|
|
4064
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update attendance record for '${employee.id}'.`);
|
|
4065
|
+
await this.engine.hooks.doAction("hrms.checked_out", updated, employee);
|
|
4066
|
+
return updated;
|
|
4067
|
+
}
|
|
4068
|
+
async getDailyAttendance(employeeId, date) {
|
|
4069
|
+
return (await this.attendanceCollection.find({ limit: 1e3 })).items.find((a) => a.data.employeeId === employeeId && a.data.date === date) ?? null;
|
|
4070
|
+
}
|
|
4071
|
+
async recordAttendanceManual(input) {
|
|
4072
|
+
const existing = await this.getDailyAttendance(input.employeeId, input.date);
|
|
4073
|
+
const attendanceData = {
|
|
4074
|
+
employerId: input.employerId,
|
|
4075
|
+
employeeId: input.employeeId,
|
|
4076
|
+
date: input.date,
|
|
4077
|
+
checkInAt: input.checkInAt,
|
|
4078
|
+
checkOutAt: input.checkOutAt,
|
|
4079
|
+
totalHours: input.totalHours ?? 0,
|
|
4080
|
+
overtimeHours: input.overtimeHours ?? 0,
|
|
4081
|
+
status: input.status ?? "present",
|
|
4082
|
+
location: input.location,
|
|
4083
|
+
notes: input.notes
|
|
4084
|
+
};
|
|
4085
|
+
if (existing) {
|
|
4086
|
+
const updated = await this.attendanceCollection.update(existing.id, { data: attendanceData });
|
|
4087
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update attendance for '${input.employeeId}'.`);
|
|
4088
|
+
return updated;
|
|
4089
|
+
}
|
|
4090
|
+
return this.attendanceCollection.create({
|
|
4091
|
+
title: `${input.employeeId} - ${input.date}`,
|
|
4092
|
+
status: "published",
|
|
4093
|
+
data: attendanceData
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
async listAttendance(query = {}) {
|
|
4097
|
+
const limit = query.limit ?? 30;
|
|
4098
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
4099
|
+
let items = (await this.attendanceCollection.find({ limit: 2e3 })).items;
|
|
4100
|
+
if (query.employerId) items = items.filter((a) => a.data.employerId === query.employerId);
|
|
4101
|
+
if (query.employeeId) items = items.filter((a) => a.data.employeeId === query.employeeId);
|
|
4102
|
+
if (query.date) items = items.filter((a) => a.data.date === query.date);
|
|
4103
|
+
if (query.startDate) items = items.filter((a) => a.data.date >= query.startDate);
|
|
4104
|
+
if (query.endDate) items = items.filter((a) => a.data.date <= query.endDate);
|
|
4105
|
+
if (query.status) items = items.filter((a) => a.data.status === query.status);
|
|
4106
|
+
const total = items.length;
|
|
4107
|
+
return {
|
|
4108
|
+
items: items.slice(offset, offset + limit),
|
|
4109
|
+
total,
|
|
4110
|
+
limit,
|
|
4111
|
+
offset,
|
|
4112
|
+
hasMore: offset + limit < total
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
async createLeaveType(input, authorId) {
|
|
4116
|
+
const leaveTypeData = {
|
|
4117
|
+
employerId: input.employerId,
|
|
4118
|
+
name: input.name,
|
|
4119
|
+
code: input.code.toUpperCase(),
|
|
4120
|
+
daysAllowedPerYear: input.daysAllowedPerYear,
|
|
4121
|
+
paid: input.paid ?? true,
|
|
4122
|
+
requiresApproval: input.requiresApproval ?? true,
|
|
4123
|
+
color: input.color,
|
|
4124
|
+
description: input.description
|
|
4125
|
+
};
|
|
4126
|
+
const item = await this.leaveTypesCollection.create({
|
|
4127
|
+
title: input.name,
|
|
4128
|
+
status: "published",
|
|
4129
|
+
data: leaveTypeData
|
|
4130
|
+
}, authorId);
|
|
4131
|
+
await this.engine.hooks.doAction("hrms.leave_type_created", item);
|
|
4132
|
+
return item;
|
|
4133
|
+
}
|
|
4134
|
+
async getLeaveType(id) {
|
|
4135
|
+
return this.leaveTypesCollection.findById(id);
|
|
4136
|
+
}
|
|
4137
|
+
async listLeaveTypes(employerId) {
|
|
4138
|
+
const result = await this.leaveTypesCollection.find({ limit: 100 });
|
|
4139
|
+
if (!employerId) return result.items;
|
|
4140
|
+
return result.items.filter((lt) => !lt.data.employerId || lt.data.employerId === employerId);
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Calculate leave balance report for an employee for a given year.
|
|
4144
|
+
*/
|
|
4145
|
+
async calculateLeaveBalance(employeeId, year) {
|
|
4146
|
+
const targetYear = year ?? (/* @__PURE__ */ new Date()).getFullYear();
|
|
4147
|
+
const employee = await this.getEmployee(employeeId);
|
|
4148
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${employeeId}' not found.`);
|
|
4149
|
+
const leaveTypes = await this.listLeaveTypes(employee.data.employerId);
|
|
4150
|
+
const allRequests = await this.leaveRequestsCollection.find({ limit: 1e3 });
|
|
4151
|
+
const yearStr = String(targetYear);
|
|
4152
|
+
const employeeRequests = allRequests.items.filter((r) => r.data.employeeId === employeeId && (r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr)));
|
|
4153
|
+
const balances = leaveTypes.map((lt) => {
|
|
4154
|
+
const approved = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "approved");
|
|
4155
|
+
const pending = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "pending");
|
|
4156
|
+
const usedDays = approved.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
|
|
4157
|
+
const pendingDays = pending.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
|
|
4158
|
+
const allocatedDays = lt.data.daysAllowedPerYear ?? 0;
|
|
4159
|
+
const remainingDays = Math.max(0, allocatedDays - usedDays);
|
|
4160
|
+
return {
|
|
4161
|
+
leaveTypeId: lt.id,
|
|
4162
|
+
leaveTypeName: lt.data.name,
|
|
4163
|
+
leaveTypeCode: lt.data.code,
|
|
4164
|
+
allocatedDays,
|
|
4165
|
+
usedDays,
|
|
4166
|
+
pendingDays,
|
|
4167
|
+
remainingDays
|
|
4168
|
+
};
|
|
4169
|
+
});
|
|
4170
|
+
const report = {
|
|
4171
|
+
employeeId,
|
|
4172
|
+
year: targetYear,
|
|
4173
|
+
balances,
|
|
4174
|
+
totalAllocated: balances.reduce((sum, b) => sum + b.allocatedDays, 0),
|
|
4175
|
+
totalUsed: balances.reduce((sum, b) => sum + b.usedDays, 0),
|
|
4176
|
+
totalRemaining: balances.reduce((sum, b) => sum + b.remainingDays, 0)
|
|
4177
|
+
};
|
|
4178
|
+
return this.engine.hooks.applyFilters("hrms.calculate_leave_balance", report, employeeId, targetYear);
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Submit a new leave request.
|
|
4182
|
+
*/
|
|
4183
|
+
async requestLeave(input, authorId) {
|
|
4184
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4185
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4186
|
+
const leaveType = await this.getLeaveType(input.leaveTypeId);
|
|
4187
|
+
if (!leaveType) throw new Error(`[HRMSService] Leave type with ID '${input.leaveTypeId}' not found.`);
|
|
4188
|
+
if (input.startDate > input.endDate) throw new Error(`[HRMSService] Start date '${input.startDate}' cannot be after end date '${input.endDate}'.`);
|
|
4189
|
+
const start = new Date(input.startDate);
|
|
4190
|
+
const end = new Date(input.endDate);
|
|
4191
|
+
const calculatedDays = Math.max(1, Math.round((end.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)) + 1);
|
|
4192
|
+
const daysCount = input.daysCount ?? calculatedDays;
|
|
4193
|
+
const startYear = start.getFullYear();
|
|
4194
|
+
const balance = (await this.calculateLeaveBalance(employee.id, startYear)).balances.find((b) => b.leaveTypeId === leaveType.id);
|
|
4195
|
+
if (balance && leaveType.data.requiresApproval && daysCount > balance.remainingDays) throw new Error(`[HRMSService] Insufficient leave balance for ${leaveType.data.name}. Requested: ${daysCount}, Remaining: ${balance.remainingDays}.`);
|
|
4196
|
+
const leaveRequestData = {
|
|
4197
|
+
employerId: input.employerId ?? employee.data.employerId,
|
|
4198
|
+
employeeId: employee.id,
|
|
4199
|
+
leaveTypeId: leaveType.id,
|
|
4200
|
+
startDate: input.startDate,
|
|
4201
|
+
endDate: input.endDate,
|
|
4202
|
+
daysCount,
|
|
4203
|
+
reason: input.reason,
|
|
4204
|
+
status: "pending"
|
|
4205
|
+
};
|
|
4206
|
+
const item = await this.leaveRequestsCollection.create({
|
|
4207
|
+
title: `${employee.title} - ${leaveType.data.name} (${input.startDate})`,
|
|
4208
|
+
status: "published",
|
|
4209
|
+
data: leaveRequestData
|
|
4210
|
+
}, authorId);
|
|
4211
|
+
await this.engine.hooks.doAction("hrms.leave_requested", item, employee);
|
|
4212
|
+
return item;
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Approve a pending leave request.
|
|
4216
|
+
*/
|
|
4217
|
+
async approveLeave(input) {
|
|
4218
|
+
const request = await this.leaveRequestsCollection.findById(input.requestId);
|
|
4219
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
|
|
4220
|
+
if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot approve leave request with status '${request.data.status}'.`);
|
|
4221
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4222
|
+
...request.data,
|
|
4223
|
+
status: "approved",
|
|
4224
|
+
approvedBy: input.approverId,
|
|
4225
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4226
|
+
} });
|
|
4227
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
|
|
4228
|
+
await this.engine.hooks.doAction("hrms.leave_approved", updated);
|
|
4229
|
+
return updated;
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* Reject a pending leave request.
|
|
4233
|
+
*/
|
|
4234
|
+
async rejectLeave(input) {
|
|
4235
|
+
const request = await this.leaveRequestsCollection.findById(input.requestId);
|
|
4236
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
|
|
4237
|
+
if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot reject leave request with status '${request.data.status}'.`);
|
|
4238
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4239
|
+
...request.data,
|
|
4240
|
+
status: "rejected",
|
|
4241
|
+
approvedBy: input.approverId,
|
|
4242
|
+
rejectionReason: input.reason,
|
|
4243
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4244
|
+
} });
|
|
4245
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
|
|
4246
|
+
await this.engine.hooks.doAction("hrms.leave_rejected", updated);
|
|
4247
|
+
return updated;
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Cancel a leave request.
|
|
4251
|
+
*/
|
|
4252
|
+
async cancelLeave(requestId) {
|
|
4253
|
+
const request = await this.leaveRequestsCollection.findById(requestId);
|
|
4254
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${requestId}' not found.`);
|
|
4255
|
+
if (request.data.status === "cancelled") return request;
|
|
4256
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4257
|
+
...request.data,
|
|
4258
|
+
status: "cancelled"
|
|
4259
|
+
} });
|
|
4260
|
+
if (!updated) throw new Error(`[HRMSService] Failed to cancel leave request.`);
|
|
4261
|
+
await this.engine.hooks.doAction("hrms.leave_cancelled", updated);
|
|
4262
|
+
return updated;
|
|
4263
|
+
}
|
|
4264
|
+
async listLeaveRequests(query = {}) {
|
|
4265
|
+
const limit = query.limit ?? 20;
|
|
4266
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
4267
|
+
let items = (await this.leaveRequestsCollection.find({ limit: 1e3 })).items;
|
|
4268
|
+
if (query.employerId) items = items.filter((r) => r.data.employerId === query.employerId);
|
|
4269
|
+
if (query.employeeId) items = items.filter((r) => r.data.employeeId === query.employeeId);
|
|
4270
|
+
if (query.leaveTypeId) items = items.filter((r) => r.data.leaveTypeId === query.leaveTypeId);
|
|
4271
|
+
if (query.status) items = items.filter((r) => r.data.status === query.status);
|
|
4272
|
+
if (query.year) {
|
|
4273
|
+
const yearStr = String(query.year);
|
|
4274
|
+
items = items.filter((r) => r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr));
|
|
4275
|
+
}
|
|
4276
|
+
const total = items.length;
|
|
4277
|
+
return {
|
|
4278
|
+
items: items.slice(offset, offset + limit),
|
|
4279
|
+
total,
|
|
4280
|
+
limit,
|
|
4281
|
+
offset,
|
|
4282
|
+
hasMore: offset + limit < total
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
//#endregion
|
|
4287
|
+
//#region src/plugins/hrms/routes.ts
|
|
4288
|
+
function json(data, status = 200) {
|
|
4289
|
+
return new Response(JSON.stringify(data), {
|
|
4290
|
+
status,
|
|
4291
|
+
headers: {
|
|
4292
|
+
"Content-Type": "application/json",
|
|
4293
|
+
"Access-Control-Allow-Origin": "*"
|
|
4294
|
+
}
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
function badRequest(message) {
|
|
4298
|
+
return json({ error: message }, 400);
|
|
4299
|
+
}
|
|
4300
|
+
function notFound(message) {
|
|
4301
|
+
return json({ error: message }, 404);
|
|
4302
|
+
}
|
|
4303
|
+
function registerHRMSRoutes(ctx, service, options = {}) {
|
|
4304
|
+
const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
4305
|
+
ctx.registerRoute("GET", `${prefix}/employers`, async (_req, { url }) => {
|
|
4306
|
+
try {
|
|
4307
|
+
const status = url.searchParams.get("status");
|
|
4308
|
+
const pageStr = url.searchParams.get("page");
|
|
4309
|
+
const limitStr = url.searchParams.get("limit");
|
|
4310
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4311
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4312
|
+
return json(await service.listEmployers({
|
|
4313
|
+
status,
|
|
4314
|
+
page,
|
|
4315
|
+
limit
|
|
4316
|
+
}));
|
|
4317
|
+
} catch (err) {
|
|
4318
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
ctx.registerRoute("POST", `${prefix}/employers`, async (req) => {
|
|
4322
|
+
try {
|
|
4323
|
+
const body = await req.json();
|
|
4324
|
+
if (!body.companyName) return badRequest("companyName is required.");
|
|
4325
|
+
return json(await service.createEmployer(body), 201);
|
|
4326
|
+
} catch (err) {
|
|
4327
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
ctx.registerRoute("GET", `${prefix}/employers/:id`, async (_req, { params }) => {
|
|
4331
|
+
try {
|
|
4332
|
+
const employer = await service.getEmployer(params.id);
|
|
4333
|
+
if (!employer) return notFound(`Employer '${params.id}' not found.`);
|
|
4334
|
+
return json(employer);
|
|
4335
|
+
} catch (err) {
|
|
4336
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
ctx.registerRoute("PUT", `${prefix}/employers/:id`, async (req, { params }) => {
|
|
4340
|
+
try {
|
|
4341
|
+
const body = await req.json();
|
|
4342
|
+
const updated = await service.updateEmployer(params.id, body);
|
|
4343
|
+
if (!updated) return notFound(`Employer '${params.id}' not found.`);
|
|
4344
|
+
return json(updated);
|
|
4345
|
+
} catch (err) {
|
|
4346
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4347
|
+
}
|
|
4348
|
+
});
|
|
4349
|
+
ctx.registerRoute("GET", `${prefix}/employees`, async (_req, { url }) => {
|
|
4350
|
+
try {
|
|
4351
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4352
|
+
const department = url.searchParams.get("department") ?? void 0;
|
|
4353
|
+
const employmentType = url.searchParams.get("employmentType");
|
|
4354
|
+
const status = url.searchParams.get("status");
|
|
4355
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
4356
|
+
const pageStr = url.searchParams.get("page");
|
|
4357
|
+
const limitStr = url.searchParams.get("limit");
|
|
4358
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4359
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4360
|
+
return json(await service.listEmployees({
|
|
4361
|
+
employerId,
|
|
4362
|
+
department,
|
|
4363
|
+
employmentType,
|
|
4364
|
+
status,
|
|
4365
|
+
search,
|
|
4366
|
+
page,
|
|
4367
|
+
limit
|
|
4368
|
+
}));
|
|
4369
|
+
} catch (err) {
|
|
4370
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4371
|
+
}
|
|
4372
|
+
});
|
|
4373
|
+
ctx.registerRoute("POST", `${prefix}/employees`, async (req) => {
|
|
4374
|
+
try {
|
|
4375
|
+
const body = await req.json();
|
|
4376
|
+
if (!body.employerId) return badRequest("employerId is required.");
|
|
4377
|
+
if (!body.employeeNumber) return badRequest("employeeNumber is required.");
|
|
4378
|
+
if (!body.firstName || !body.lastName) return badRequest("firstName and lastName are required.");
|
|
4379
|
+
if (!body.email) return badRequest("email is required.");
|
|
4380
|
+
return json(await service.createEmployee(body), 201);
|
|
4381
|
+
} catch (err) {
|
|
4382
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4383
|
+
}
|
|
4384
|
+
});
|
|
4385
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4386
|
+
try {
|
|
4387
|
+
const employee = await service.getEmployee(params.id);
|
|
4388
|
+
if (!employee) return notFound(`Employee '${params.id}' not found.`);
|
|
4389
|
+
return json(employee);
|
|
4390
|
+
} catch (err) {
|
|
4391
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
ctx.registerRoute("PUT", `${prefix}/employees/:id`, async (req, { params }) => {
|
|
4395
|
+
try {
|
|
4396
|
+
const body = await req.json();
|
|
4397
|
+
const updated = await service.updateEmployee(params.id, body);
|
|
4398
|
+
if (!updated) return notFound(`Employee '${params.id}' not found.`);
|
|
4399
|
+
return json(updated);
|
|
4400
|
+
} catch (err) {
|
|
4401
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4405
|
+
try {
|
|
4406
|
+
if (!await service.deleteEmployee(params.id)) return notFound(`Employee '${params.id}' not found.`);
|
|
4407
|
+
return json({ success: true });
|
|
4408
|
+
} catch (err) {
|
|
4409
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id/leave-balance`, async (_req, { params, url }) => {
|
|
4413
|
+
try {
|
|
4414
|
+
const yearStr = url.searchParams.get("year");
|
|
4415
|
+
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
4416
|
+
return json(await service.calculateLeaveBalance(params.id, year));
|
|
4417
|
+
} catch (err) {
|
|
4418
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4419
|
+
}
|
|
4420
|
+
});
|
|
4421
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
|
|
4422
|
+
try {
|
|
4423
|
+
const reports = await service.getDirectReports(params.id);
|
|
4424
|
+
return json({
|
|
4425
|
+
items: reports,
|
|
4426
|
+
total: reports.length
|
|
4427
|
+
});
|
|
4428
|
+
} catch (err) {
|
|
4429
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4432
|
+
ctx.registerRoute("POST", `${prefix}/attendance/check-in`, async (req) => {
|
|
4433
|
+
try {
|
|
4434
|
+
const body = await req.json();
|
|
4435
|
+
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4436
|
+
return json(await service.checkIn(body), 201);
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
ctx.registerRoute("POST", `${prefix}/attendance/check-out`, async (req) => {
|
|
4442
|
+
try {
|
|
4443
|
+
const body = await req.json();
|
|
4444
|
+
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4445
|
+
return json(await service.checkOut(body));
|
|
4446
|
+
} catch (err) {
|
|
4447
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
ctx.registerRoute("GET", `${prefix}/attendance`, async (_req, { url }) => {
|
|
4451
|
+
try {
|
|
4452
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4453
|
+
const employeeId = url.searchParams.get("employeeId") ?? void 0;
|
|
4454
|
+
const date = url.searchParams.get("date") ?? void 0;
|
|
4455
|
+
const startDate = url.searchParams.get("startDate") ?? void 0;
|
|
4456
|
+
const endDate = url.searchParams.get("endDate") ?? void 0;
|
|
4457
|
+
const status = url.searchParams.get("status");
|
|
4458
|
+
const pageStr = url.searchParams.get("page");
|
|
4459
|
+
const limitStr = url.searchParams.get("limit");
|
|
4460
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4461
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4462
|
+
return json(await service.listAttendance({
|
|
4463
|
+
employerId,
|
|
4464
|
+
employeeId,
|
|
4465
|
+
date,
|
|
4466
|
+
startDate,
|
|
4467
|
+
endDate,
|
|
4468
|
+
status,
|
|
4469
|
+
page,
|
|
4470
|
+
limit
|
|
4471
|
+
}));
|
|
4472
|
+
} catch (err) {
|
|
4473
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
ctx.registerRoute("POST", `${prefix}/attendance/manual`, async (req) => {
|
|
4477
|
+
try {
|
|
4478
|
+
const body = await req.json();
|
|
4479
|
+
if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest("employerId, employeeId, date, and checkInAt are required.");
|
|
4480
|
+
return json(await service.recordAttendanceManual(body), 201);
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4483
|
+
}
|
|
4484
|
+
});
|
|
4485
|
+
ctx.registerRoute("GET", `${prefix}/leave-types`, async (_req, { url }) => {
|
|
4486
|
+
try {
|
|
4487
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4488
|
+
const types = await service.listLeaveTypes(employerId);
|
|
4489
|
+
return json({
|
|
4490
|
+
items: types,
|
|
4491
|
+
total: types.length
|
|
4492
|
+
});
|
|
4493
|
+
} catch (err) {
|
|
4494
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
ctx.registerRoute("POST", `${prefix}/leave-types`, async (req) => {
|
|
4498
|
+
try {
|
|
4499
|
+
const body = await req.json();
|
|
4500
|
+
if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest("name, code, and daysAllowedPerYear are required.");
|
|
4501
|
+
return json(await service.createLeaveType(body), 201);
|
|
4502
|
+
} catch (err) {
|
|
4503
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4504
|
+
}
|
|
4505
|
+
});
|
|
4506
|
+
ctx.registerRoute("GET", `${prefix}/leave-types/:id`, async (_req, { params }) => {
|
|
4507
|
+
try {
|
|
4508
|
+
const leaveType = await service.getLeaveType(params.id);
|
|
4509
|
+
if (!leaveType) return notFound(`Leave type '${params.id}' not found.`);
|
|
4510
|
+
return json(leaveType);
|
|
4511
|
+
} catch (err) {
|
|
4512
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4513
|
+
}
|
|
4514
|
+
});
|
|
4515
|
+
ctx.registerRoute("GET", `${prefix}/leave-requests`, async (_req, { url }) => {
|
|
4516
|
+
try {
|
|
4517
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4518
|
+
const employeeId = url.searchParams.get("employeeId") ?? void 0;
|
|
4519
|
+
const leaveTypeId = url.searchParams.get("leaveTypeId") ?? void 0;
|
|
4520
|
+
const status = url.searchParams.get("status");
|
|
4521
|
+
const yearStr = url.searchParams.get("year");
|
|
4522
|
+
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
4523
|
+
const pageStr = url.searchParams.get("page");
|
|
4524
|
+
const limitStr = url.searchParams.get("limit");
|
|
4525
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4526
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4527
|
+
return json(await service.listLeaveRequests({
|
|
4528
|
+
employerId,
|
|
4529
|
+
employeeId,
|
|
4530
|
+
leaveTypeId,
|
|
4531
|
+
status,
|
|
4532
|
+
year,
|
|
4533
|
+
page,
|
|
4534
|
+
limit
|
|
4535
|
+
}));
|
|
4536
|
+
} catch (err) {
|
|
4537
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests`, async (req) => {
|
|
4541
|
+
try {
|
|
4542
|
+
const body = await req.json();
|
|
4543
|
+
if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest("employeeId, leaveTypeId, startDate, and endDate are required.");
|
|
4544
|
+
return json(await service.requestLeave(body), 201);
|
|
4545
|
+
} catch (err) {
|
|
4546
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
|
|
4550
|
+
try {
|
|
4551
|
+
const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
|
|
4552
|
+
return json(await service.approveLeave({
|
|
4553
|
+
requestId: params.id,
|
|
4554
|
+
approverId
|
|
4555
|
+
}));
|
|
4556
|
+
} catch (err) {
|
|
4557
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4558
|
+
}
|
|
4559
|
+
});
|
|
4560
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/reject`, async (req, { params }) => {
|
|
4561
|
+
try {
|
|
4562
|
+
const body = await req.json().catch(() => ({}));
|
|
4563
|
+
const approverId = body.approverId ?? "admin";
|
|
4564
|
+
return json(await service.rejectLeave({
|
|
4565
|
+
requestId: params.id,
|
|
4566
|
+
approverId,
|
|
4567
|
+
reason: body.reason
|
|
4568
|
+
}));
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4573
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
|
|
4574
|
+
try {
|
|
4575
|
+
return json(await service.cancelLeave(params.id));
|
|
4576
|
+
} catch (err) {
|
|
4577
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
}
|
|
4581
|
+
//#endregion
|
|
4582
|
+
//#region src/plugins/hrms/client.ts
|
|
4583
|
+
var HRMSClient = class {
|
|
4584
|
+
client;
|
|
4585
|
+
options;
|
|
4586
|
+
service;
|
|
4587
|
+
prefix;
|
|
4588
|
+
constructor(client, options = {}) {
|
|
4589
|
+
this.client = client;
|
|
4590
|
+
this.options = options;
|
|
4591
|
+
this.prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
4592
|
+
const engine = client.getEngine();
|
|
4593
|
+
if (engine) this.service = new HRMSService(engine, options);
|
|
4594
|
+
}
|
|
4595
|
+
employers = {
|
|
4596
|
+
find: async (query = {}) => {
|
|
4597
|
+
if (this.service) return this.service.listEmployers(query);
|
|
4598
|
+
const params = new URLSearchParams();
|
|
4599
|
+
if (query.status) params.set("status", query.status);
|
|
4600
|
+
if (query.page) params.set("page", String(query.page));
|
|
4601
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4602
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4603
|
+
return this.client.request(`${this.prefix}/employers${q}`);
|
|
4604
|
+
},
|
|
4605
|
+
get: async (id) => {
|
|
4606
|
+
if (this.service) return this.service.getEmployer(id);
|
|
4607
|
+
return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`);
|
|
4608
|
+
},
|
|
4609
|
+
create: async (input) => {
|
|
4610
|
+
if (this.service) return this.service.createEmployer(input);
|
|
4611
|
+
return this.client.request(`${this.prefix}/employers`, {
|
|
4612
|
+
method: "POST",
|
|
4613
|
+
body: JSON.stringify(input)
|
|
4614
|
+
});
|
|
4615
|
+
},
|
|
4616
|
+
update: async (id, input) => {
|
|
4617
|
+
if (this.service) return this.service.updateEmployer(id, input);
|
|
4618
|
+
return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`, {
|
|
4619
|
+
method: "PUT",
|
|
4620
|
+
body: JSON.stringify(input)
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4624
|
+
employees = {
|
|
4625
|
+
find: async (query = {}) => {
|
|
4626
|
+
if (this.service) return this.service.listEmployees(query);
|
|
4627
|
+
const params = new URLSearchParams();
|
|
4628
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4629
|
+
if (query.department) params.set("department", query.department);
|
|
4630
|
+
if (query.employmentType) params.set("employmentType", query.employmentType);
|
|
4631
|
+
if (query.status) params.set("status", query.status);
|
|
4632
|
+
if (query.search) params.set("search", query.search);
|
|
4633
|
+
if (query.page) params.set("page", String(query.page));
|
|
4634
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4635
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4636
|
+
return this.client.request(`${this.prefix}/employees${q}`);
|
|
4637
|
+
},
|
|
4638
|
+
get: async (id) => {
|
|
4639
|
+
if (this.service) return this.service.getEmployee(id);
|
|
4640
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`);
|
|
4641
|
+
},
|
|
4642
|
+
getByNumber: async (employerId, employeeNumber) => {
|
|
4643
|
+
if (this.service) return this.service.getEmployeeByNumber(employerId, employeeNumber);
|
|
4644
|
+
return (await this.employees.find({
|
|
4645
|
+
employerId,
|
|
4646
|
+
search: employeeNumber
|
|
4647
|
+
})).items.find((e) => e.data.employeeNumber === employeeNumber) ?? null;
|
|
4648
|
+
},
|
|
4649
|
+
create: async (input) => {
|
|
4650
|
+
if (this.service) return this.service.createEmployee(input);
|
|
4651
|
+
return this.client.request(`${this.prefix}/employees`, {
|
|
4652
|
+
method: "POST",
|
|
4653
|
+
body: JSON.stringify(input)
|
|
4654
|
+
});
|
|
4655
|
+
},
|
|
4656
|
+
update: async (id, input) => {
|
|
4657
|
+
if (this.service) return this.service.updateEmployee(id, input);
|
|
4658
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, {
|
|
4659
|
+
method: "PUT",
|
|
4660
|
+
body: JSON.stringify(input)
|
|
4661
|
+
});
|
|
4662
|
+
},
|
|
4663
|
+
delete: async (id) => {
|
|
4664
|
+
if (this.service) return this.service.deleteEmployee(id);
|
|
4665
|
+
return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, { method: "DELETE" })).success;
|
|
4666
|
+
},
|
|
4667
|
+
getLeaveBalance: async (employeeId, year) => {
|
|
4668
|
+
if (this.service) return this.service.calculateLeaveBalance(employeeId, year);
|
|
4669
|
+
const q = year ? `?year=${year}` : "";
|
|
4670
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(employeeId)}/leave-balance${q}`);
|
|
4671
|
+
},
|
|
4672
|
+
getDirectReports: async (managerId) => {
|
|
4673
|
+
if (this.service) return this.service.getDirectReports(managerId);
|
|
4674
|
+
return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(managerId)}/direct-reports`)).items;
|
|
4675
|
+
}
|
|
4676
|
+
};
|
|
4677
|
+
attendance = {
|
|
4678
|
+
checkIn: async (input) => {
|
|
4679
|
+
if (this.service) return this.service.checkIn(input);
|
|
4680
|
+
return this.client.request(`${this.prefix}/attendance/check-in`, {
|
|
4681
|
+
method: "POST",
|
|
4682
|
+
body: JSON.stringify(input)
|
|
4683
|
+
});
|
|
4684
|
+
},
|
|
4685
|
+
checkOut: async (input) => {
|
|
4686
|
+
if (this.service) return this.service.checkOut(input);
|
|
4687
|
+
return this.client.request(`${this.prefix}/attendance/check-out`, {
|
|
4688
|
+
method: "POST",
|
|
4689
|
+
body: JSON.stringify(input)
|
|
4690
|
+
});
|
|
4691
|
+
},
|
|
4692
|
+
getDaily: async (employeeId, date) => {
|
|
4693
|
+
if (this.service) return this.service.getDailyAttendance(employeeId, date);
|
|
4694
|
+
return (await this.attendance.find({
|
|
4695
|
+
employeeId,
|
|
4696
|
+
date
|
|
4697
|
+
})).items[0] ?? null;
|
|
4698
|
+
},
|
|
4699
|
+
find: async (query = {}) => {
|
|
4700
|
+
if (this.service) return this.service.listAttendance(query);
|
|
4701
|
+
const params = new URLSearchParams();
|
|
4702
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4703
|
+
if (query.employeeId) params.set("employeeId", query.employeeId);
|
|
4704
|
+
if (query.date) params.set("date", query.date);
|
|
4705
|
+
if (query.startDate) params.set("startDate", query.startDate);
|
|
4706
|
+
if (query.endDate) params.set("endDate", query.endDate);
|
|
4707
|
+
if (query.status) params.set("status", query.status);
|
|
4708
|
+
if (query.page) params.set("page", String(query.page));
|
|
4709
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4710
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4711
|
+
return this.client.request(`${this.prefix}/attendance${q}`);
|
|
4712
|
+
},
|
|
4713
|
+
recordManual: async (input) => {
|
|
4714
|
+
if (this.service) return this.service.recordAttendanceManual(input);
|
|
4715
|
+
return this.client.request(`${this.prefix}/attendance/manual`, {
|
|
4716
|
+
method: "POST",
|
|
4717
|
+
body: JSON.stringify(input)
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
leaves = {
|
|
4722
|
+
listTypes: async (employerId) => {
|
|
4723
|
+
if (this.service) return this.service.listLeaveTypes(employerId);
|
|
4724
|
+
const q = employerId ? `?employerId=${encodeURIComponent(employerId)}` : "";
|
|
4725
|
+
return (await this.client.request(`${this.prefix}/leave-types${q}`)).items;
|
|
4726
|
+
},
|
|
4727
|
+
createType: async (input) => {
|
|
4728
|
+
if (this.service) return this.service.createLeaveType(input);
|
|
4729
|
+
return this.client.request(`${this.prefix}/leave-types`, {
|
|
4730
|
+
method: "POST",
|
|
4731
|
+
body: JSON.stringify(input)
|
|
4732
|
+
});
|
|
4733
|
+
},
|
|
4734
|
+
getType: async (id) => {
|
|
4735
|
+
if (this.service) return this.service.getLeaveType(id);
|
|
4736
|
+
return this.client.request(`${this.prefix}/leave-types/${encodeURIComponent(id)}`);
|
|
4737
|
+
},
|
|
4738
|
+
findRequests: async (query = {}) => {
|
|
4739
|
+
if (this.service) return this.service.listLeaveRequests(query);
|
|
4740
|
+
const params = new URLSearchParams();
|
|
4741
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4742
|
+
if (query.employeeId) params.set("employeeId", query.employeeId);
|
|
4743
|
+
if (query.leaveTypeId) params.set("leaveTypeId", query.leaveTypeId);
|
|
4744
|
+
if (query.status) params.set("status", query.status);
|
|
4745
|
+
if (query.year) params.set("year", String(query.year));
|
|
4746
|
+
if (query.page) params.set("page", String(query.page));
|
|
4747
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4748
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4749
|
+
return this.client.request(`${this.prefix}/leave-requests${q}`);
|
|
4750
|
+
},
|
|
4751
|
+
request: async (input) => {
|
|
4752
|
+
if (this.service) return this.service.requestLeave(input);
|
|
4753
|
+
return this.client.request(`${this.prefix}/leave-requests`, {
|
|
4754
|
+
method: "POST",
|
|
4755
|
+
body: JSON.stringify(input)
|
|
4756
|
+
});
|
|
4757
|
+
},
|
|
4758
|
+
approve: async (input) => {
|
|
4759
|
+
if (this.service) return this.service.approveLeave(input);
|
|
4760
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/approve`, {
|
|
4761
|
+
method: "POST",
|
|
4762
|
+
body: JSON.stringify({ approverId: input.approverId })
|
|
4763
|
+
});
|
|
4764
|
+
},
|
|
4765
|
+
reject: async (input) => {
|
|
4766
|
+
if (this.service) return this.service.rejectLeave(input);
|
|
4767
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/reject`, {
|
|
4768
|
+
method: "POST",
|
|
4769
|
+
body: JSON.stringify({
|
|
4770
|
+
approverId: input.approverId,
|
|
4771
|
+
reason: input.reason
|
|
4772
|
+
})
|
|
4773
|
+
});
|
|
4774
|
+
},
|
|
4775
|
+
cancel: async (requestId) => {
|
|
4776
|
+
if (this.service) return this.service.cancelLeave(requestId);
|
|
4777
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(requestId)}/cancel`, { method: "POST" });
|
|
4778
|
+
}
|
|
4779
|
+
};
|
|
4780
|
+
};
|
|
4781
|
+
/**
|
|
4782
|
+
* Get or create an HRMSClient adapter for a CMSClient.
|
|
4783
|
+
*/
|
|
4784
|
+
function getHRMSClient(client, options) {
|
|
4785
|
+
return new HRMSClient(client, options);
|
|
4786
|
+
}
|
|
4787
|
+
//#endregion
|
|
4788
|
+
//#region src/plugins/hrms/index.ts
|
|
4789
|
+
/**
|
|
4790
|
+
* @azlib/cms - Built-in HRMS Plugin
|
|
4791
|
+
*/
|
|
4792
|
+
/**
|
|
4793
|
+
* Built-in HRMS plugin factory for @azlib/cms.
|
|
4794
|
+
* Equips the CMS engine with multi-tenant employers, employee profiles,
|
|
4795
|
+
* daily attendance check-in/out tracking, and leave quota approval workflows.
|
|
4796
|
+
*/
|
|
4797
|
+
const hrmsPlugin = definePlugin((options) => {
|
|
4798
|
+
const opts = options || {};
|
|
4799
|
+
const collections = [createEmployeeCollection(opts)];
|
|
4800
|
+
if (opts.enableEmployers !== false) collections.unshift(createEmployerCollection(opts));
|
|
4801
|
+
if (opts.enableAttendance !== false) collections.push(createAttendanceCollection(opts));
|
|
4802
|
+
if (opts.enableLeaves !== false) {
|
|
4803
|
+
collections.push(createLeaveTypeCollection(opts));
|
|
4804
|
+
collections.push(createLeaveRequestCollection(opts));
|
|
4805
|
+
}
|
|
4806
|
+
return {
|
|
4807
|
+
name: "hrms",
|
|
4808
|
+
version: "1.0.0",
|
|
4809
|
+
description: "Built-in Human Resource Management System (HRMS) plugin for employers, employees, attendance, and leave management",
|
|
4810
|
+
collections,
|
|
4811
|
+
taxonomies: createHRMSTaxonomies(opts),
|
|
4812
|
+
setup(ctx) {
|
|
4813
|
+
const service = new HRMSService(ctx.engine, opts);
|
|
4814
|
+
ctx.engine.__hrmsService = service;
|
|
4815
|
+
registerHRMSRoutes(ctx, service, opts);
|
|
4816
|
+
}
|
|
4817
|
+
};
|
|
4818
|
+
});
|
|
4819
|
+
/**
|
|
4820
|
+
* Retrieve the active HRMSService instance associated with a CMSEngine.
|
|
4821
|
+
*/
|
|
4822
|
+
function getHRMSService(engine, options) {
|
|
4823
|
+
if (engine.__hrmsService) return engine.__hrmsService;
|
|
4824
|
+
const service = new HRMSService(engine, options);
|
|
4825
|
+
engine.__hrmsService = service;
|
|
4826
|
+
return service;
|
|
4827
|
+
}
|
|
4828
|
+
//#endregion
|
|
1950
4829
|
exports.CMSClient = CMSClient;
|
|
1951
4830
|
exports.CMSEngine = CMSEngine;
|
|
1952
4831
|
exports.CMSRouter = CMSRouter;
|
|
1953
4832
|
exports.ContentLifecycle = ContentLifecycle;
|
|
1954
4833
|
exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
|
|
1955
4834
|
exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
|
|
4835
|
+
exports.EcommerceClient = EcommerceClient;
|
|
4836
|
+
exports.EcommerceService = EcommerceService;
|
|
4837
|
+
exports.HRMSClient = HRMSClient;
|
|
4838
|
+
exports.HRMSService = HRMSService;
|
|
1956
4839
|
exports.HooksManager = HooksManager;
|
|
1957
4840
|
exports.MediaManager = MediaManager;
|
|
1958
4841
|
exports.MemoryStorageAdapter = MemoryStorageAdapter;
|
|
@@ -1962,13 +4845,29 @@ exports.RevisionManager = RevisionManager;
|
|
|
1962
4845
|
exports.TaxonomyManager = TaxonomyManager;
|
|
1963
4846
|
exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
|
|
1964
4847
|
exports.collection = collection;
|
|
4848
|
+
exports.createAttendanceCollection = createAttendanceCollection;
|
|
1965
4849
|
exports.createCMSEngine = createCMSEngine;
|
|
1966
4850
|
exports.createCMSRouter = createCMSRouter;
|
|
1967
4851
|
exports.createCmsClient = createCmsClient;
|
|
4852
|
+
exports.createDiscountCollection = createDiscountCollection;
|
|
4853
|
+
exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
|
|
4854
|
+
exports.createEmployeeCollection = createEmployeeCollection;
|
|
4855
|
+
exports.createEmployerCollection = createEmployerCollection;
|
|
4856
|
+
exports.createHRMSTaxonomies = createHRMSTaxonomies;
|
|
4857
|
+
exports.createLeaveRequestCollection = createLeaveRequestCollection;
|
|
4858
|
+
exports.createLeaveTypeCollection = createLeaveTypeCollection;
|
|
4859
|
+
exports.createOrderCollection = createOrderCollection;
|
|
4860
|
+
exports.createProductCollection = createProductCollection;
|
|
1968
4861
|
exports.defaultHooks = defaultHooks;
|
|
1969
4862
|
exports.defineConfig = defineConfig;
|
|
1970
4863
|
exports.definePlugin = definePlugin;
|
|
4864
|
+
exports.ecommercePlugin = ecommercePlugin;
|
|
1971
4865
|
exports.fields = fields;
|
|
4866
|
+
exports.getEcommerceClient = getEcommerceClient;
|
|
4867
|
+
exports.getEcommerceService = getEcommerceService;
|
|
4868
|
+
exports.getHRMSClient = getHRMSClient;
|
|
4869
|
+
exports.getHRMSService = getHRMSService;
|
|
4870
|
+
exports.hrmsPlugin = hrmsPlugin;
|
|
1972
4871
|
exports.normalizeConfig = normalizeConfig;
|
|
1973
4872
|
exports.resolveUniqueSlug = resolveUniqueSlug;
|
|
1974
4873
|
exports.slugify = slugify;
|