This commit is contained in:
2026-03-09 11:38:43 +03:30
commit 5fda2d6d8c
339 changed files with 186841 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
name: deploy to danak
on:
push:
branches:
- production
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
DANAK_SERVER: "https://captain.dev.danakcorp.com"
APP_TOKEN: e7d4e8ae6b8164d3c1669c161f7cab295549b6fdfaea90eaf833f24122380841
APP_NAME: dmenuplus-api-production
GITHUB_TOKEN: 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
steps:
- name: Check out repositorys
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: zmihamid
password: ${{ env.GITHUB_TOKEN }}
- name: Preset Image Name
run: echo "IMAGE_URL=$(echo ghcr.io/zmihamid/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Build and push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ env.IMAGE_URL }}
- name: Install CapRover CLI
run: npm install -g caprover
- name: deploy to server
run: |
caprover deploy -a $APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL"
+58
View File
@@ -0,0 +1,58 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/react/*
+16
View File
@@ -0,0 +1,16 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"insertPragma": false,
"bracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}
+15
View File
@@ -0,0 +1,15 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.format.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}
+79
View File
@@ -0,0 +1,79 @@
# Stage 1: Base
FROM node:22-alpine AS base
# Install timezone support
RUN apk add --no-cache tzdata
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
# Set workdir for clarity
WORKDIR /app
# Stage 2: Dependencies
FROM base AS deps
WORKDIR /temp-deps
COPY package*.json ./
RUN npm ci --ignore-scripts
# Stage 3: Build
FROM base AS builder
WORKDIR /build
COPY . ./
COPY --from=deps /temp-deps/node_modules ./node_modules
RUN if [ -f package.json ] && grep -q '"build":' package.json; then npm run build; fi
# Stage 4: Runner
FROM base AS runner
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY . ./
COPY --from=builder --chown=appuser:appgroup /build/ ./
RUN chown -R appuser:appgroup /app
USER appuser
ENV NODE_ENV=production
EXPOSE 4000
CMD ["npm", "start"]
# # Stage 1: Build Stage
# FROM node:22-alpine AS base
# RUN npm install -g corepack@latest
# RUN corepack enable && corepack prepare pnpm@9 --activate
# # Install tzdata to support timezone settings
# RUN apk add --no-cache tzdata
# # Set the timezone to Asia/Tehran
# RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
# FROM base AS deps
# WORKDIR /temp-deps
# COPY package*.json pnpm-lock.yaml ./
# RUN pnpm install --frozen-lockfile --loglevel info
# FROM base AS builder
# WORKDIR /build
# COPY . ./
# COPY --from=deps /temp-deps/node_modules ./node_modules
# RUN if [ -f package.json ] && grep -q '"build":' package.json; then pnpm run build; fi
# RUN pnpm install --prod --frozen-lockfile --loglevel info
# FROM base AS runner
# RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# WORKDIR /app
# COPY . ./
# COPY --from=builder --chown=appuser:appgroup /build/ ./
# RUN chown -R appuser:appgroup /app
# USER appuser
# ENV NODE_ENV=production
# EXPOSE 3000
# CMD ["npm", "start"]
+98
View File
@@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260105085439 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
this.addSql(`alter table "categories" add column "order" int null;`);
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
}
override async down(): Promise<void> {
this.addSql(`create table "user_wallets" ("id" char(26) not null, "created_at" timestamptz(6) not null default now(), "updated_at" timestamptz(6) not null default now(), "deleted_at" timestamptz(6) null, "restaurant_id" char(26) not null, "user_id" char(26) not null, "wallet" int4 not null default 0, "points" int4 not null default 0, constraint "user_wallets_pkey" primary key ("id"));`);
this.addSql(`alter table "user_wallets" add constraint "unique_user_restaurant" unique ("user_id", "restaurant_id");`);
this.addSql(`create index "user_wallets_created_at_index" on "user_wallets" ("created_at");`);
this.addSql(`create index "user_wallets_deleted_at_index" on "user_wallets" ("deleted_at");`);
this.addSql(`create index "user_wallets_restaurant_id_index" on "user_wallets" ("restaurant_id");`);
this.addSql(`create index "user_wallets_user_id_index" on "user_wallets" ("user_id");`);
this.addSql(`create index "user_wallets_user_id_restaurant_id_index" on "user_wallets" ("user_id", "restaurant_id");`);
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
}
}
@@ -0,0 +1,23 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260106054151 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
this.addSql(`alter table "restaurants" alter column "subscription_id" drop not null;`);
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
this.addSql(`alter table "restaurants" alter column "subscription_end_date" drop not null;`);
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
this.addSql(`alter table "restaurants" alter column "subscription_start_date" drop not null;`);
}
override async down(): Promise<void> {
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
this.addSql(`alter table "restaurants" alter column "subscription_id" set not null;`);
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
this.addSql(`alter table "restaurants" alter column "subscription_end_date" set not null;`);
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
this.addSql(`alter table "restaurants" alter column "subscription_start_date" set not null;`);
}
}
@@ -0,0 +1,14 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260106192946 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "contacts" add column "restaurant_id" char(26) null;`);
this.addSql(`alter table "contacts" add constraint "contacts_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurants" ("id") on update cascade on delete set null;`);
}
override async down(): Promise<void> {
this.addSql(`alter table "contacts" drop constraint "contacts_restaurant_id_foreign";`);
}
}
@@ -0,0 +1,19 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260226063544 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "deliveries" alter column "delivery_fee" type int using ("delivery_fee"::int);`);
this.addSql(`alter table "deliveries" alter column "per_kilometer_fee" type int using ("per_kilometer_fee"::int);`);
this.addSql(`alter table "deliveries" alter column "distance_based_min_cost" type int using ("distance_based_min_cost"::int);`);
this.addSql(`alter table "deliveries" alter column "min_order_price" type int using ("min_order_price"::int);`);
}
override async down(): Promise<void> {
this.addSql(`alter table "deliveries" alter column "delivery_fee" type numeric(10,0) using ("delivery_fee"::numeric(10,0));`);
this.addSql(`alter table "deliveries" alter column "per_kilometer_fee" type numeric(10,0) using ("per_kilometer_fee"::numeric(10,0));`);
this.addSql(`alter table "deliveries" alter column "distance_based_min_cost" type numeric(10,0) using ("distance_based_min_cost"::numeric(10,0));`);
this.addSql(`alter table "deliveries" alter column "min_order_price" type numeric(10,0) using ("min_order_price"::numeric(10,0));`);
}
}
+41
View File
@@ -0,0 +1,41 @@
# Icon Naming Process
## Files Created:
- `icons.csv` - Updated with "icon_name" column
- `icon_viewer.html` - Web interface to view and name icons
- `populate_icon_names.js` - Script to add names to CSV
## Step-by-Step Process:
### 1. View the Icons
- Open `icon_viewer.html` in your web browser
- You'll see all 138 icons in a grid layout
- Each icon shows its number and has an input field for naming
### 2. Name the Icons
- Look at each icon and enter appropriate names in the input fields
- The progress counter shows how many you've named
- Names should be descriptive (e.g., "hamburger", "pizza", "coffee", "delivery")
### 3. Export Named Icons (Optional)
- Click "Export Named Icons" to download a CSV of only the named icons
- This helps you review your naming before finalizing
### 4. Update the Main CSV
- Edit `populate_icon_names.js`
- Replace the placeholder names in the `iconNames` array with your actual names
- Run the script: `node populate_icon_names.js`
## Example Icon Names:
- Food items: "pizza", "burger", "pasta", "salad"
- Drinks: "coffee", "soda", "juice", "beer"
- Categories: "fast_food", "italian", "asian", "desserts"
- Actions: "delivery", "pickup", "favorite", "cart"
## Tips:
- Use consistent naming conventions (snake_case or camelCase)
- Keep names short but descriptive
- Group similar icons with consistent prefixes if applicable
- Review all names before running the populate script
The final CSV will have all original columns plus the new "icon_name" column with your custom names.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
#!/bin/bash
# Script to dump icon and icon_groups tables from PostgreSQL database
# Usage: ./dump-icons.sh [output_file]
set -e
# Load environment variables if .env file exists
if [ -f .env ]; then
set -a
# Source .env file, handling comments and empty lines
while IFS= read -r line || [ -n "$line" ]; do
# Remove leading/trailing whitespace
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Skip empty lines and lines starting with #
[[ -z "$line" ]] && continue
[[ "$line" =~ ^# ]] && continue
# Remove inline comments (everything after # that's not in quotes)
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
# Skip if line is empty after removing comments
[[ -z "$line" ]] && continue
# Export the variable (only if it looks like KEY=VALUE)
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
export "$line" 2>/dev/null || true
fi
done < .env
set +a
fi
# Get database connection details from environment variables
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-dmenu}"
DB_USER="${DB_USER:-postgres}"
# Output file name (default: icons_dump_YYYYMMDD_HHMMSS.sql)
if [ -z "$1" ]; then
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="icons_dump_${TIMESTAMP}.sql"
else
OUTPUT_FILE="$1"
fi
echo "Dumping icon_groups and icons tables..."
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
echo "Output file: ${OUTPUT_FILE}"
# Dump icon_groups table first (parent table)
echo "Dumping icon_groups table..."
PGPASSWORD="${DB_PASS}" pg_dump \
-h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "${DB_USER}" \
-d "${DB_NAME}" \
-t icon_groups \
--data-only \
--column-inserts \
--no-owner \
--no-privileges \
> "${OUTPUT_FILE}"
# Append icons table to the dump file
echo "Dumping icons table..."
PGPASSWORD="${DB_PASS}" pg_dump \
-h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "${DB_USER}" \
-d "${DB_NAME}" \
-t icons \
--data-only \
--column-inserts \
--no-owner \
--no-privileges \
>> "${OUTPUT_FILE}"
echo "Dump completed successfully!"
echo "File saved to: ${OUTPUT_FILE}"
echo ""
echo "To restore, run: ./restore-icons.sh ${OUTPUT_FILE}"
+312
View File
@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Viewer - Name the Icons</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.header {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
padding: 20px;
}
.icon-item {
background: white;
border-radius: 8px;
padding: 15px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.icon-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.icon-img {
width: 48px;
height: 48px;
margin-bottom: 10px;
}
.icon-id {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.icon-name {
font-weight: bold;
color: #333;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.input-field {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 5px;
}
.save-btn {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
}
.save-btn:hover {
background-color: #0056b3;
}
.progress {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="header">
<h1>Icon Name Editor</h1>
<p>Review each icon below and enter appropriate names. Names will be saved back to the CSV file.</p>
<div class="progress">
<span id="completed-count">0</span> of <span id="total-count">138</span> icons named
</div>
<button class="save-btn" onclick="saveToCSV()">Save Names to CSV</button>
<button class="save-btn" onclick="exportNamedIcons()" style="margin-left: 10px; background-color: #28a745;">Export Named Icons</button>
</div>
<div class="icon-grid" id="iconGrid">
<!-- Icons will be loaded here -->
</div>
<script>
const icons = [
"https://storage.danakcorp.com/images/1766554365832-411750172.svg",
"https://storage.danakcorp.com/images/1766554379149-104970938.svg",
"https://storage.danakcorp.com/images/1766554389090-593863597.svg",
"https://storage.danakcorp.com/images/1766554404968-905984091.svg",
"https://storage.danakcorp.com/images/1766554415325-873800591.svg",
"https://storage.danakcorp.com/images/1766554428814-77027818.svg",
"https://storage.danakcorp.com/images/1766554438632-45072052.svg",
"https://storage.danakcorp.com/images/1766554446340-463157109.svg",
"https://storage.danakcorp.com/images/1766554456605-280166176.svg",
"https://storage.danakcorp.com/images/1766554471887-315640495.svg",
"https://storage.danakcorp.com/images/1766554485634-242317100.svg",
"https://storage.danakcorp.com/images/1766554504553-157481346.svg",
"https://storage.danakcorp.com/images/1766554528198-919877647.svg",
"https://storage.danakcorp.com/images/1766554544140-886744157.svg",
"https://storage.danakcorp.com/images/1766554565614-684869010.svg",
"https://storage.danakcorp.com/images/1766554578855-318583145.svg",
"https://storage.danakcorp.com/images/1766554593272-362758712.svg",
"https://storage.danakcorp.com/images/1766554606166-877198688.svg",
"https://storage.danakcorp.com/images/1766554624133-21439996.svg",
"https://storage.danakcorp.com/images/1766554641691-535696200.svg",
"https://storage.danakcorp.com/images/1766554658481-114730426.svg",
"https://storage.danakcorp.com/images/1766554671760-634916570.svg",
"https://storage.danakcorp.com/images/1766554967381-660612838.svg",
"https://storage.danakcorp.com/images/1766555106925-267924006.svg",
"https://storage.danakcorp.com/images/1766555231674-387319393.svg",
"https://storage.danakcorp.com/images/1766555290519-155637622.svg",
"https://storage.danakcorp.com/images/1766555308301-230359494.svg",
"https://storage.danakcorp.com/images/1766555420851-186440015.svg",
"https://storage.danakcorp.com/images/1766555590828-644527730.svg",
"https://storage.danakcorp.com/images/1766555606997-571682462.svg",
"https://storage.danakcorp.com/images/1766555663659-433052581.svg",
"https://storage.danakcorp.com/images/1766555713098-988242030.svg",
"https://storage.danakcorp.com/images/1766555726158-680948882.svg",
"https://storage.danakcorp.com/images/1766555746650-426516324.svg",
"https://storage.danakcorp.com/images/1766555765798-127083646.svg",
"https://storage.danakcorp.com/images/1766555784837-243715698.svg",
"https://storage.danakcorp.com/images/1766555799191-594035927.svg",
"https://storage.danakcorp.com/images/1766555894624-524350777.svg",
"https://storage.danakcorp.com/images/1766555932114-485954688.svg",
"https://storage.danakcorp.com/images/1766555949650-909996041.svg",
"https://storage.danakcorp.com/images/1766555966861-75517122.svg",
"https://storage.danakcorp.com/images/1766556006875-239689681.svg",
"https://storage.danakcorp.com/images/1766556257163-459958715.svg",
"https://storage.danakcorp.com/images/1766556293812-665835248.svg",
"https://storage.danakcorp.com/images/1766556307082-162280793.svg",
"https://storage.danakcorp.com/images/1766556340402-744632392.svg",
"https://storage.danakcorp.com/images/1766556458335-857124841.svg",
"https://storage.danakcorp.com/images/1766556479530-161860621.svg",
"https://storage.danakcorp.com/images/1766556498215-91175544.svg",
"https://storage.danakcorp.com/images/1766556677355-108854669.svg",
"https://storage.danakcorp.com/images/1766556736283-691587656.svg",
"https://storage.danakcorp.com/images/1766556759367-58066921.svg",
"https://storage.danakcorp.com/images/1766556777353-490756125.svg",
"https://storage.danakcorp.com/images/1766556870740-118553581.svg",
"https://storage.danakcorp.com/images/1766556890334-962678132.svg",
"https://storage.danakcorp.com/images/1766556921029-865260745.svg",
"https://storage.danakcorp.com/images/1766556946363-320833501.svg",
"https://storage.danakcorp.com/images/1766556973771-832565827.svg",
"https://storage.danakcorp.com/images/1766557056358-727862983.svg",
"https://storage.danakcorp.com/images/1766557199259-72955190.svg",
"https://storage.danakcorp.com/images/1766557252452-988016525.svg",
"https://storage.danakcorp.com/images/1766557274778-439028456.svg",
"https://storage.danakcorp.com/images/1766557289094-415104854.svg",
"https://storage.danakcorp.com/images/1766557300923-302277606.svg",
"https://storage.danakcorp.com/images/1766557317091-602116315.svg",
"https://storage.danakcorp.com/images/1766557647999-47333939.svg",
"https://storage.danakcorp.com/images/1766557669282-172512141.svg",
"https://storage.danakcorp.com/images/1766557705283-692546793.svg",
"https://storage.danakcorp.com/images/1766557729175-762576743.svg",
"https://storage.danakcorp.com/images/1766557756487-665300947.svg",
"https://storage.danakcorp.com/images/1766557776067-804149228.svg",
"https://storage.danakcorp.com/images/1766557797509-101726352.svg",
"https://storage.danakcorp.com/images/1766557816484-357699340.svg",
"https://storage.danakcorp.com/images/1766557837292-326521286.svg",
"https://storage.danakcorp.com/images/1766559083575-564721059.svg",
"https://storage.danakcorp.com/images/1766559099561-720966516.svg",
"https://storage.danakcorp.com/images/1766559116496-834964990.svg",
"https://storage.danakcorp.com/images/1766563021426-600579018.svg",
"https://storage.danakcorp.com/images/1766563038525-727531190.svg",
"https://storage.danakcorp.com/images/1766563051485-824602001.svg",
"https://storage.danakcorp.com/images/1766563066017-372210647.svg",
"https://storage.danakcorp.com/images/1766563124296-331607800.svg",
"https://storage.danakcorp.com/images/1766563142133-359920423.svg",
"https://storage.danakcorp.com/images/1766563164270-350101986.svg",
"https://storage.danakcorp.com/images/1766563181587-86630830.svg",
"https://storage.danakcorp.com/images/1766563204966-651499380.svg",
"https://storage.danakcorp.com/images/1766563220596-103699525.svg",
"https://storage.danakcorp.com/images/1766563265634-373953449.svg",
"https://storage.danakcorp.com/images/1766563275517-958045207.svg",
"https://storage.danakcorp.com/images/1766563286526-622849562.svg",
"https://storage.danakcorp.com/images/1766564695339-504553420.svg",
"https://storage.danakcorp.com/images/1766564706032-159367016.svg",
"https://storage.danakcorp.com/images/1766564720683-887818252.svg",
"https://storage.danakcorp.com/images/1766564734432-483528219.svg",
"https://storage.danakcorp.com/images/1766564750099-854146988.svg",
"https://storage.danakcorp.com/images/1766564758573-696060786.svg",
"https://storage.danakcorp.com/images/1766564805823-829938380.svg",
"https://storage.danakcorp.com/images/1766564891137-468827500.svg",
"https://storage.danakcorp.com/images/1766564907215-921380792.svg",
"https://storage.danakcorp.com/images/1766564926872-672612697.svg",
"https://storage.danakcorp.com/images/1766565459604-638026702.svg",
"https://storage.danakcorp.com/images/1766565476771-42527917.svg",
"https://storage.danakcorp.com/images/1766565491264-946731491.svg",
"https://storage.danakcorp.com/images/1766565502475-288189136.svg",
"https://storage.danakcorp.com/images/1766565530628-800474580.svg",
"https://storage.danakcorp.com/images/1766565547983-878658938.svg",
"https://storage.danakcorp.com/images/1766565563263-891688321.svg",
"https://storage.danakcorp.com/images/1766565600547-704332792.svg",
"https://storage.danakcorp.com/images/1766565625437-528383713.svg",
"https://storage.danakcorp.com/images/1766565641620-53176909.svg",
"https://storage.danakcorp.com/images/1766565664427-433669317.svg",
"https://storage.danakcorp.com/images/1766565971515-159903323.svg",
"https://storage.danakcorp.com/images/1766565995424-288101561.svg",
"https://storage.danakcorp.com/images/1766566026942-868807643.svg",
"https://storage.danakcorp.com/images/1766566041525-973019175.svg",
"https://storage.danakcorp.com/images/1766566064741-271844433.svg",
"https://storage.danakcorp.com/images/1766566084368-438871956.svg",
"https://storage.danakcorp.com/images/1766566106954-47179607.svg",
"https://storage.danakcorp.com/images/1766566122836-486871124.svg",
"https://storage.danakcorp.com/images/1766566143171-668309076.svg",
"https://storage.danakcorp.com/images/1766566161551-844376214.svg",
"https://storage.danakcorp.com/images/1766566176755-30806596.svg",
"https://storage.danakcorp.com/images/1766566193720-339416210.svg",
"https://storage.danakcorp.com/images/1766566293736-981836756.svg",
"https://storage.danakcorp.com/images/1766566304318-193301989.svg",
"https://storage.danakcorp.com/images/1766566327768-2675508.svg",
"https://storage.danakcorp.com/images/1766566341717-462491426.svg",
"https://storage.danakcorp.com/images/1766566357028-432111036.svg",
"https://storage.danakcorp.com/images/1766566371354-279596850.svg",
"https://storage.danakcorp.com/images/1766566385674-569493499.svg",
"https://storage.danakcorp.com/images/1766566413058-975642584.svg",
"https://storage.danakcorp.com/images/1766566427440-982841441.svg",
"https://storage.danakcorp.com/images/1766566442808-761588425.svg",
"https://storage.danakcorp.com/images/1766566468108-519151895.svg",
"https://storage.danakcorp.com/images/1766566482278-661272876.svg",
"https://storage.danakcorp.com/images/1766566494050-408184442.svg",
"https://storage.danakcorp.com/images/1766566504760-709260995.svg"
];
const iconNames = new Array(icons.length).fill('');
function loadIcons() {
const grid = document.getElementById('iconGrid');
icons.forEach((url, index) => {
const item = document.createElement('div');
item.className = 'icon-item';
item.innerHTML = `
<div class="icon-id">Icon ${index + 1}</div>
<img src="${url}" alt="Icon ${index + 1}" class="icon-img" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiBmaWxsPSIjZGRkIi8+Cjx0ZXh0IHg9IjI0IiB5PSIyNCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zNWVtIj5ObyBJY29uPC90ZXh0Pgo8L3N2Zz4='">
<input type="text" class="input-field" placeholder="Enter icon name..." oninput="updateName(${index}, this.value)">
`;
grid.appendChild(item);
});
}
function updateName(index, name) {
iconNames[index] = name.trim();
updateProgress();
}
function updateProgress() {
const completed = iconNames.filter(name => name.length > 0).length;
document.getElementById('completed-count').textContent = completed;
}
function saveToCSV() {
// This will create a downloadable CSV with the names
let csvContent = '"id","created_at","updated_at","deleted_at","url","group_id","icon_name"\n';
// You'll need to load the original CSV data here and add the names
// For now, this is a placeholder - you'd need to combine with original data
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'icons_with_names.csv';
a.click();
window.URL.revokeObjectURL(url);
alert('CSV template downloaded. You\'ll need to manually add the icon names to the original CSV file.');
}
function exportNamedIcons() {
const namedIcons = [];
iconNames.forEach((name, index) => {
if (name.trim()) {
namedIcons.push({
index: index + 1,
name: name.trim(),
url: icons[index]
});
}
});
const csvContent = 'Index,Name,URL\n' +
namedIcons.map(icon => `${icon.index},"${icon.name}","${icon.url}"`).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'named_icons.csv';
a.click();
window.URL.revokeObjectURL(url);
}
// Load icons when page loads
window.onload = loadIcons;
</script>
</body>
</html>
+138
View File
@@ -0,0 +1,138 @@
"id","created_at","updated_at","deleted_at","url","group_id","icon_name"
"01KD7DGWH6TDB47V4710FDT7SR","2025-12-24 05:32:46.503+00","2025-12-24 05:32:46.503+00",NULL,"https://storage.danakcorp.com/images/1766554365832-411750172.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DH9F6BN4TEM96TA2QT4GJ","2025-12-24 05:32:59.751+00","2025-12-24 05:32:59.751+00",NULL,"https://storage.danakcorp.com/images/1766554379149-104970938.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DHK6ZTEGPRBNXJPV49TE6","2025-12-24 05:33:09.727+00","2025-12-24 05:33:09.727+00",NULL,"https://storage.danakcorp.com/images/1766554389090-593863597.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DJ2NVEHVQMDCAM31VKKGK","2025-12-24 05:33:25.564+00","2025-12-24 05:33:25.564+00",NULL,"https://storage.danakcorp.com/images/1766554404968-905984091.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DJCQY1V84DAVXPV097HE6","2025-12-24 05:33:35.871+00","2025-12-24 05:33:35.871+00",NULL,"https://storage.danakcorp.com/images/1766554415325-873800591.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DJSZ2XVX971FGNFRRDQV6","2025-12-24 05:33:49.411+00","2025-12-24 05:33:49.411+00",NULL,"https://storage.danakcorp.com/images/1766554428814-77027818.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DK3HJAD9SMDD8CM1RPNQS","2025-12-24 05:33:59.218+00","2025-12-24 05:33:59.218+00",NULL,"https://storage.danakcorp.com/images/1766554438632-45072052.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DKB23B6XWJ8CMAKQ5R8HS","2025-12-24 05:34:06.915+00","2025-12-24 05:34:06.915+00",NULL,"https://storage.danakcorp.com/images/1766554446340-463157109.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DKN32VCDQDF2D407MGKF0","2025-12-24 05:34:17.186+00","2025-12-24 05:34:17.186+00",NULL,"https://storage.danakcorp.com/images/1766554456605-280166176.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DM4AEQ2FCJGZM390N7YVJ","2025-12-24 05:34:32.782+00","2025-12-24 05:34:32.782+00",NULL,"https://storage.danakcorp.com/images/1766554471887-315640495.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DMHKWJSHX6BR41FTDYVHE","2025-12-24 05:34:46.396+00","2025-12-24 05:34:46.396+00",NULL,"https://storage.danakcorp.com/images/1766554485634-242317100.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DN3XNDW2JQ0RGM711R9Q5","2025-12-24 05:35:05.141+00","2025-12-24 05:35:05.141+00",NULL,"https://storage.danakcorp.com/images/1766554504553-157481346.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DNV0F9CPMKQ4R482MTVKJ","2025-12-24 05:35:28.783+00","2025-12-24 05:35:28.783+00",NULL,"https://storage.danakcorp.com/images/1766554528198-919877647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DPAKCPXD2GK0FCZ7N3XXP","2025-12-24 05:35:44.749+00","2025-12-24 05:35:44.749+00",NULL,"https://storage.danakcorp.com/images/1766554544140-886744157.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DPZGZFZPS5AJ8RNM3W16F","2025-12-24 05:36:06.175+00","2025-12-24 05:36:06.175+00",NULL,"https://storage.danakcorp.com/images/1766554565614-684869010.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DQCMAD39T5QB5728M6PFP","2025-12-24 05:36:19.594+00","2025-12-24 05:36:19.594+00",NULL,"https://storage.danakcorp.com/images/1766554578855-318583145.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DQTQHGWP5J77V327Y4GHZ","2025-12-24 05:36:34.034+00","2025-12-24 05:36:34.034+00",NULL,"https://storage.danakcorp.com/images/1766554593272-362758712.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DR75VSDY0B28AFTB6QP9M","2025-12-24 05:36:46.779+00","2025-12-24 05:36:46.779+00",NULL,"https://storage.danakcorp.com/images/1766554606166-877198688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DRRT3B7BAA714MMYVVHN4","2025-12-24 05:37:04.835+00","2025-12-24 05:37:04.835+00",NULL,"https://storage.danakcorp.com/images/1766554624133-21439996.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DS9TQY1KWPM2HY7VC7THR","2025-12-24 05:37:22.263+00","2025-12-24 05:37:22.263+00",NULL,"https://storage.danakcorp.com/images/1766554641691-535696200.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DST85ZZ0SKSVDYJWN67VQ","2025-12-24 05:37:39.078+00","2025-12-24 05:37:39.078+00",NULL,"https://storage.danakcorp.com/images/1766554658481-114730426.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7DT75Y84N1ECNV2WW6VQ37","2025-12-24 05:37:52.318+00","2025-12-24 05:37:52.318+00",NULL,"https://storage.danakcorp.com/images/1766554671760-634916570.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7E3837FJ9TDDKPF8S6PHK6","2025-12-24 05:42:48.167+00","2025-12-24 05:42:48.167+00",NULL,"https://storage.danakcorp.com/images/1766554967381-660612838.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7E7G89J5KH2ZT45F0MT5X3","2025-12-24 05:45:07.593+00","2025-12-24 05:45:07.593+00",NULL,"https://storage.danakcorp.com/images/1766555106925-267924006.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EBA6ATBBFVMGWQHACZ1G7","2025-12-24 05:47:12.459+00","2025-12-24 05:47:12.459+00",NULL,"https://storage.danakcorp.com/images/1766555231674-387319393.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7ED3NRKEX3Z8G6CTHK5QAB","2025-12-24 05:48:11.32+00","2025-12-24 05:48:11.32+00",NULL,"https://storage.danakcorp.com/images/1766555290519-155637622.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EDN07W94SGG04BRNRNRWT","2025-12-24 05:48:29.063+00","2025-12-24 05:48:29.063+00",NULL,"https://storage.danakcorp.com/images/1766555308301-230359494.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EH2W75B2H8JGMHR9P76ZE","2025-12-24 05:50:21.576+00","2025-12-24 05:50:21.576+00",NULL,"https://storage.danakcorp.com/images/1766555420851-186440015.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EP8V3KQYMDCDMXYP1QKPK","2025-12-24 05:53:11.523+00","2025-12-24 05:53:11.523+00",NULL,"https://storage.danakcorp.com/images/1766555590828-644527730.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EPRJWQAQX00F0J02FV05J","2025-12-24 05:53:27.644+00","2025-12-24 05:53:27.644+00",NULL,"https://storage.danakcorp.com/images/1766555606997-571682462.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7ERGH1ABTPGSJN1WFTESP6","2025-12-24 05:54:24.929+00","2025-12-24 05:54:24.929+00",NULL,"https://storage.danakcorp.com/images/1766555663659-433052581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7ET0394XN31KFT3340R04H","2025-12-24 05:55:13.641+00","2025-12-24 05:55:13.641+00",NULL,"https://storage.danakcorp.com/images/1766555713098-988242030.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7ETCZBFV8ZZF1GFDKP7DXA","2025-12-24 05:55:26.827+00","2025-12-24 05:55:26.827+00",NULL,"https://storage.danakcorp.com/images/1766555726158-680948882.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EV0VW1M18RGBB54C4YDAC","2025-12-24 05:55:47.196+00","2025-12-24 05:55:47.196+00",NULL,"https://storage.danakcorp.com/images/1766555746650-426516324.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EVMP392QF71V6QFGK9XQA","2025-12-24 05:56:07.491+00","2025-12-24 05:56:07.491+00",NULL,"https://storage.danakcorp.com/images/1766555765798-127083646.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EW6MQX0VFPS21TJPSEBAH","2025-12-24 05:56:25.879+00","2025-12-24 05:56:25.879+00",NULL,"https://storage.danakcorp.com/images/1766555784837-243715698.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EWMGEHYQRKBM6XBS9EKTF","2025-12-24 05:56:40.078+00","2025-12-24 05:56:40.078+00",NULL,"https://storage.danakcorp.com/images/1766555799191-594035927.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7EZHSEJ4G27VT3HS7JZ8YN","2025-12-24 05:58:15.599+00","2025-12-24 05:58:15.599+00",NULL,"https://storage.danakcorp.com/images/1766555894624-524350777.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7F0PCEADF1HWJ47V8RP1ZP","2025-12-24 05:58:53.07+00","2025-12-24 05:58:53.07+00",NULL,"https://storage.danakcorp.com/images/1766555932114-485954688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7F184CHB129A900K2V6N94","2025-12-24 05:59:11.244+00","2025-12-24 05:59:11.244+00",NULL,"https://storage.danakcorp.com/images/1766555949650-909996041.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7F1R0JNNRP8QW8NHANZB01","2025-12-24 05:59:27.506+00","2025-12-24 05:59:27.506+00",NULL,"https://storage.danakcorp.com/images/1766555966861-75517122.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7F2Z1V0T1S47R7VWBEPY66","2025-12-24 06:00:07.483+00","2025-12-24 06:00:07.483+00",NULL,"https://storage.danakcorp.com/images/1766556006875-239689681.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FAP3S04D5CW51W98YZRHE","2025-12-24 06:04:20.473+00","2025-12-24 06:04:20.473+00",NULL,"https://storage.danakcorp.com/images/1766556257163-459958715.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FBQJAV9HJMG4EVRKPSAC5","2025-12-24 06:04:54.73+00","2025-12-24 06:04:54.73+00",NULL,"https://storage.danakcorp.com/images/1766556293812-665835248.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FC48FRARBP7ZXN7SPGP8K","2025-12-24 06:05:07.727+00","2025-12-24 06:05:07.727+00",NULL,"https://storage.danakcorp.com/images/1766556307082-162280793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FD4SW2G73QX0YY6KNK7V4","2025-12-24 06:05:41.052+00","2025-12-24 06:05:41.052+00",NULL,"https://storage.danakcorp.com/images/1766556340402-744632392.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FGRC85J6JXEYNBGCPY0S4","2025-12-24 06:07:39.4+00","2025-12-24 06:07:39.4+00",NULL,"https://storage.danakcorp.com/images/1766556458335-857124841.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FHCW86SPVBSC8N62YEY5R","2025-12-24 06:08:00.393+00","2025-12-24 06:08:00.393+00",NULL,"https://storage.danakcorp.com/images/1766556479530-161860621.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FHYWZ73T36XKTDQ5FEN7G","2025-12-24 06:08:18.847+00","2025-12-24 06:08:18.847+00",NULL,"https://storage.danakcorp.com/images/1766556498215-91175544.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FQDYPGWD05HHATWBC499Z","2025-12-24 06:11:18.103+00","2025-12-24 06:11:18.103+00",NULL,"https://storage.danakcorp.com/images/1766556677355-108854669.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FS7QGJJ3YRSM10Y5YWD6C","2025-12-24 06:12:17.264+00","2025-12-24 06:12:17.264+00",NULL,"https://storage.danakcorp.com/images/1766556736283-691587656.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FSY2XKBREKJ9HCNJN924C","2025-12-24 06:12:40.157+00","2025-12-24 06:12:40.157+00",NULL,"https://storage.danakcorp.com/images/1766556759367-58066921.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FTFZ25MHQATRC41Y3EPQS","2025-12-24 06:12:58.468+00","2025-12-24 06:12:58.468+00",NULL,"https://storage.danakcorp.com/images/1766556777353-490756125.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FXARH84SJYESXVAG0AH3Q","2025-12-24 06:14:31.441+00","2025-12-24 06:14:31.441+00",NULL,"https://storage.danakcorp.com/images/1766556870740-118553581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FXXV4DP74NGWJC0J43MDP","2025-12-24 06:14:50.98+00","2025-12-24 06:14:50.98+00",NULL,"https://storage.danakcorp.com/images/1766556890334-962678132.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FYVRNQAAA0DQZ8NZ02F2R","2025-12-24 06:15:21.621+00","2025-12-24 06:15:21.621+00",NULL,"https://storage.danakcorp.com/images/1766556921029-865260745.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7FZMJ4QGTG6NZEDKWP7V0W","2025-12-24 06:15:47.013+00","2025-12-24 06:15:47.013+00",NULL,"https://storage.danakcorp.com/images/1766556946363-320833501.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7G0F8GTY7KZ530Y9XF8YXF","2025-12-24 06:16:14.352+00","2025-12-24 06:16:14.352+00",NULL,"https://storage.danakcorp.com/images/1766556973771-832565827.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7G300731YZQDT3FVAP0KHH","2025-12-24 06:17:37.031+00","2025-12-24 06:17:37.031+00",NULL,"https://storage.danakcorp.com/images/1766557056358-727862983.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7G7BHM8WEXFHHKD09MG10J","2025-12-24 06:19:59.925+00","2025-12-24 06:19:59.925+00",NULL,"https://storage.danakcorp.com/images/1766557199259-72955190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7G8ZM2FPZ9H5BVGD983CTS","2025-12-24 06:20:53.25+00","2025-12-24 06:20:53.25+00",NULL,"https://storage.danakcorp.com/images/1766557252452-988016525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7G9N7J647RH94K5WQB1Q3B","2025-12-24 06:21:15.378+00","2025-12-24 06:21:15.378+00",NULL,"https://storage.danakcorp.com/images/1766557274778-439028456.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GA36DDJBQJ15XT4D0E2D6","2025-12-24 06:21:29.678+00","2025-12-24 06:21:29.678+00",NULL,"https://storage.danakcorp.com/images/1766557289094-415104854.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GAEZKQKRC80EP7F03K22K","2025-12-24 06:21:41.747+00","2025-12-24 06:21:41.747+00",NULL,"https://storage.danakcorp.com/images/1766557300923-302277606.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GAYJAHXAZV84PN3X2N4FY","2025-12-24 06:21:57.707+00","2025-12-24 06:21:57.707+00",NULL,"https://storage.danakcorp.com/images/1766557317091-602116315.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GN1W5Q0WZWCJR8V62AD88","2025-12-24 06:27:28.774+00","2025-12-24 06:27:28.774+00",NULL,"https://storage.danakcorp.com/images/1766557647999-47333939.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GNPEX4CMZHRVYB4AZYSK3","2025-12-24 06:27:49.853+00","2025-12-24 06:27:49.853+00",NULL,"https://storage.danakcorp.com/images/1766557669282-172512141.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GPSY27QGH5XSPXWHD8VJ5","2025-12-24 06:28:26.179+00","2025-12-24 06:28:26.179+00",NULL,"https://storage.danakcorp.com/images/1766557705283-692546793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GQGZK52YM87E283013JS4","2025-12-24 06:28:49.779+00","2025-12-24 06:28:49.779+00",NULL,"https://storage.danakcorp.com/images/1766557729175-762576743.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GRBRZTFFHDS1A6MH2CMJ4","2025-12-24 06:29:17.215+00","2025-12-24 06:29:17.215+00",NULL,"https://storage.danakcorp.com/images/1766557756487-665300947.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GRYXHE220H9C3VH4VAC8E","2025-12-24 06:29:36.817+00","2025-12-24 06:29:36.817+00",NULL,"https://storage.danakcorp.com/images/1766557776067-804149228.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GSKMZH8WAH5VYAS4GH02C","2025-12-24 06:29:58.048+00","2025-12-24 06:29:58.048+00",NULL,"https://storage.danakcorp.com/images/1766557797509-101726352.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GT6A83SX8NKPVHYXAKJ3H","2025-12-24 06:30:17.16+00","2025-12-24 06:30:17.16+00",NULL,"https://storage.danakcorp.com/images/1766557816484-357699340.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7GTTKT3H3EDFTRCY8P2AE9","2025-12-24 06:30:37.946+00","2025-12-24 06:30:37.946+00",NULL,"https://storage.danakcorp.com/images/1766557837292-326521286.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7J0VP01FCP0TEHYG0MD191","2025-12-24 06:51:24.224+00","2025-12-24 06:51:24.224+00",NULL,"https://storage.danakcorp.com/images/1766559083575-564721059.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7J1BMTMBMV009EYC7N4G7D","2025-12-24 06:51:40.57+00","2025-12-24 06:51:40.57+00",NULL,"https://storage.danakcorp.com/images/1766559099561-720966516.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7J1W90CASGWZKBFMN75STB","2025-12-24 06:51:57.601+00","2025-12-24 06:51:57.601+00",NULL,"https://storage.danakcorp.com/images/1766559116496-834964990.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NS1HJ2C707G3KXRRZCN7Y","2025-12-24 07:57:02.387+00","2025-12-24 07:57:02.387+00",NULL,"https://storage.danakcorp.com/images/1766563021426-600579018.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NSHYZ6XESEJR7MCKS7CY0","2025-12-24 07:57:19.199+00","2025-12-24 07:57:19.199+00",NULL,"https://storage.danakcorp.com/images/1766563038525-727531190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NSYX5W066SDN0RYBKXHQ3","2025-12-24 07:57:32.454+00","2025-12-24 07:57:32.454+00",NULL,"https://storage.danakcorp.com/images/1766563051485-824602001.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NTCZK4NYE3KCYDM1247GP","2025-12-24 07:57:46.867+00","2025-12-24 07:57:46.867+00",NULL,"https://storage.danakcorp.com/images/1766563066017-372210647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NW5KZEW4M2ZJX93T53G41","2025-12-24 07:58:44.863+00","2025-12-24 07:58:44.863+00",NULL,"https://storage.danakcorp.com/images/1766563124296-331607800.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NWQ7YFJ7HDNWRB5F1V47Q","2025-12-24 07:59:02.91+00","2025-12-24 07:59:02.91+00",NULL,"https://storage.danakcorp.com/images/1766563142133-359920423.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NXCZSEN7M2TN0DT0XVVR9","2025-12-24 07:59:25.177+00","2025-12-24 07:59:25.177+00",NULL,"https://storage.danakcorp.com/images/1766563164270-350101986.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NXXMYZFMAWMRS23GRZSKP","2025-12-24 07:59:42.238+00","2025-12-24 07:59:42.238+00",NULL,"https://storage.danakcorp.com/images/1766563181587-86630830.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NYP6M0HNC3A4XCXHK4W4M","2025-12-24 08:00:07.38+00","2025-12-24 08:00:07.38+00",NULL,"https://storage.danakcorp.com/images/1766563204966-651499380.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7NZ3PDZX4WTVV3KC5MTD8K","2025-12-24 08:00:21.198+00","2025-12-24 08:00:21.198+00",NULL,"https://storage.danakcorp.com/images/1766563220596-103699525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7P0FWPK3HTYVX14XMGCHTC","2025-12-24 08:01:06.455+00","2025-12-24 08:01:06.455+00",NULL,"https://storage.danakcorp.com/images/1766563265634-373953449.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7P0SAN0PA6KAGE3ABAXSWF","2025-12-24 08:01:16.117+00","2025-12-24 08:01:16.117+00",NULL,"https://storage.danakcorp.com/images/1766563275517-958045207.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7P1429PQT8NFDFRZEDNH1P","2025-12-24 08:01:27.113+00","2025-12-24 08:01:27.113+00",NULL,"https://storage.danakcorp.com/images/1766563286526-622849562.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7QC4M3D2SF26ZH0S0SDWBD","2025-12-24 08:24:56.708+00","2025-12-24 08:24:56.708+00",NULL,"https://storage.danakcorp.com/images/1766564695339-504553420.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QCEDA0T3CGZ01NGBKWZ9S","2025-12-24 08:25:06.73+00","2025-12-24 08:25:06.73+00",NULL,"https://storage.danakcorp.com/images/1766564706032-159367016.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QCWQ00TVCK7BAAHSWA09H","2025-12-24 08:25:21.376+00","2025-12-24 08:25:21.376+00",NULL,"https://storage.danakcorp.com/images/1766564720683-887818252.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QDA1R9WTTV2JWQ2Y3WGST","2025-12-24 08:25:35.032+00","2025-12-24 08:25:35.032+00",NULL,"https://storage.danakcorp.com/images/1766564734432-483528219.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QDSCQNW2JC4SAQCK0QBNW","2025-12-24 08:25:50.743+00","2025-12-24 08:25:50.743+00",NULL,"https://storage.danakcorp.com/images/1766564750099-854146988.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QE1NXGG08JQ5F2BC35NFC","2025-12-24 08:25:59.23+00","2025-12-24 08:25:59.23+00",NULL,"https://storage.danakcorp.com/images/1766564758573-696060786.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QFFXDVTSB5VAXB640HR7N","2025-12-24 08:26:46.574+00","2025-12-24 08:26:46.574+00",NULL,"https://storage.danakcorp.com/images/1766564805823-829938380.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QJ37DT4NCD6QY9MNGCZXZ","2025-12-24 08:28:11.885+00","2025-12-24 08:28:11.885+00",NULL,"https://storage.danakcorp.com/images/1766564891137-468827500.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QJJZX6Y7CR8R60W3TNKRS","2025-12-24 08:28:28.029+00","2025-12-24 08:28:28.029+00",NULL,"https://storage.danakcorp.com/images/1766564907215-921380792.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7QK69ZH6B4S602YKZCBVMB","2025-12-24 08:28:47.807+00","2025-12-24 08:28:47.807+00",NULL,"https://storage.danakcorp.com/images/1766564926872-672612697.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R3EM739KW6PV431V6P19S","2025-12-24 08:37:40.616+00","2025-12-24 08:37:40.616+00",NULL,"https://storage.danakcorp.com/images/1766565459604-638026702.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R3YZ6SAQWZ4CMC4GCJ1V4","2025-12-24 08:37:57.351+00","2025-12-24 08:37:57.351+00",NULL,"https://storage.danakcorp.com/images/1766565476771-42527917.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R4D5BVGPWKXE6A7FNVNRA","2025-12-24 08:38:11.883+00","2025-12-24 08:38:11.883+00",NULL,"https://storage.danakcorp.com/images/1766565491264-946731491.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R4RFYFFQ4GV5TQ3DH6BW6","2025-12-24 08:38:23.486+00","2025-12-24 08:38:23.486+00",NULL,"https://storage.danakcorp.com/images/1766565502475-288189136.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R5KMSAGCXZCTQPTVYDTA7","2025-12-24 08:38:51.289+00","2025-12-24 08:38:51.289+00",NULL,"https://storage.danakcorp.com/images/1766565530628-800474580.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R64HYQD37W8ASB2Q817MW","2025-12-24 08:39:08.606+00","2025-12-24 08:39:08.606+00",NULL,"https://storage.danakcorp.com/images/1766565547983-878658938.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R6KF9YWZBZ3WPAKXQS2CH","2025-12-24 08:39:23.881+00","2025-12-24 08:39:23.881+00",NULL,"https://storage.danakcorp.com/images/1766565563263-891688321.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R7RM02R8FZ4DN65WMJVE8","2025-12-24 08:40:01.92+00","2025-12-24 08:40:01.92+00",NULL,"https://storage.danakcorp.com/images/1766565600547-704332792.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
"01KD7R8G53XBC6AG6KAVSBW94G","2025-12-24 08:40:26.019+00","2025-12-24 08:40:26.019+00",NULL,"https://storage.danakcorp.com/images/1766565625437-528383713.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R901TPNAX9YYRG6W0GWHF","2025-12-24 08:40:42.298+00","2025-12-24 08:40:42.298+00",NULL,"https://storage.danakcorp.com/images/1766565641620-53176909.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7R9PKF9G1DD1HMS2QQQN9V","2025-12-24 08:41:05.391+00","2025-12-24 08:41:05.391+00",NULL,"https://storage.danakcorp.com/images/1766565664427-433669317.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RK2G6E5JKEDK9P95W79PR","2025-12-24 08:46:12.487+00","2025-12-24 08:46:12.487+00",NULL,"https://storage.danakcorp.com/images/1766565971515-159903323.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RKSM9QD6RA2H6WGE23KEC","2025-12-24 08:46:36.169+00","2025-12-24 08:46:36.169+00",NULL,"https://storage.danakcorp.com/images/1766565995424-288101561.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RMR9DRA1TG2BJ9Q8QAZ15","2025-12-24 08:47:07.565+00","2025-12-24 08:47:07.565+00",NULL,"https://storage.danakcorp.com/images/1766566026942-868807643.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RN6FH0TX69RZTKJKM6MEN","2025-12-24 08:47:22.097+00","2025-12-24 08:47:22.097+00",NULL,"https://storage.danakcorp.com/images/1766566041525-973019175.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RNX4EE92QR2C24577BJCW","2025-12-24 08:47:45.294+00","2025-12-24 08:47:45.294+00",NULL,"https://storage.danakcorp.com/images/1766566064741-271844433.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RPHHCKNXNQH29WSG84D2Z","2025-12-24 08:48:06.189+00","2025-12-24 08:48:06.189+00",NULL,"https://storage.danakcorp.com/images/1766566084368-438871956.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RQ6MFA5CDQ4J55NG3N7PX","2025-12-24 08:48:27.791+00","2025-12-24 08:48:27.791+00",NULL,"https://storage.danakcorp.com/images/1766566106954-47179607.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RQNXJNRXS8MXV57R50PKN","2025-12-24 08:48:43.443+00","2025-12-24 08:48:43.443+00",NULL,"https://storage.danakcorp.com/images/1766566122836-486871124.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RR9W6B9BTWJ0N2H11TWM1","2025-12-24 08:49:03.879+00","2025-12-24 08:49:03.879+00",NULL,"https://storage.danakcorp.com/images/1766566143171-668309076.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RRVQWYHSHN3M7R25GNGHS","2025-12-24 08:49:22.172+00","2025-12-24 08:49:22.172+00",NULL,"https://storage.danakcorp.com/images/1766566161551-844376214.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RSAKAP1TJ59GN11DEAXD9","2025-12-24 08:49:37.386+00","2025-12-24 08:49:37.386+00",NULL,"https://storage.danakcorp.com/images/1766566176755-30806596.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RSV35PY1A6193H4GDW1ZW","2025-12-24 08:49:54.277+00","2025-12-24 08:49:54.277+00",NULL,"https://storage.danakcorp.com/images/1766566193720-339416210.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RWX3D4ZZE35W345HDK35S","2025-12-24 08:51:34.638+00","2025-12-24 08:51:34.638+00",NULL,"https://storage.danakcorp.com/images/1766566293736-981836756.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RX79BVR30WGKXAZQ3D0CM","2025-12-24 08:51:45.068+00","2025-12-24 08:51:45.068+00",NULL,"https://storage.danakcorp.com/images/1766566304318-193301989.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RXY99C8PV6SAR0AJ00F16","2025-12-24 08:52:08.618+00","2025-12-24 08:52:08.618+00",NULL,"https://storage.danakcorp.com/images/1766566327768-2675508.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RYBPV273KCR350ESK2FN1","2025-12-24 08:52:22.364+00","2025-12-24 08:52:22.364+00",NULL,"https://storage.danakcorp.com/images/1766566341717-462491426.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RYTNTE20YS29X2XJT7GY3","2025-12-24 08:52:37.69+00","2025-12-24 08:52:37.69+00",NULL,"https://storage.danakcorp.com/images/1766566357028-432111036.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RZ8K86GCJSW1E0EC5T4B2","2025-12-24 08:52:51.944+00","2025-12-24 08:52:51.944+00",NULL,"https://storage.danakcorp.com/images/1766566371354-279596850.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7RZPNGASPYFP9XKMSY208B","2025-12-24 08:53:06.352+00","2025-12-24 08:53:06.352+00",NULL,"https://storage.danakcorp.com/images/1766566385674-569493499.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S0HEQZ4AY5TTFKF36HS6F","2025-12-24 08:53:33.783+00","2025-12-24 08:53:33.783+00",NULL,"https://storage.danakcorp.com/images/1766566413058-975642584.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S0ZEQ6BH163HCX0MQZHBJ","2025-12-24 08:53:48.119+00","2025-12-24 08:53:48.119+00",NULL,"https://storage.danakcorp.com/images/1766566427440-982841441.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S1EF6R6J6AMMC6HP5J0AA","2025-12-24 08:54:03.494+00","2025-12-24 08:54:03.494+00",NULL,"https://storage.danakcorp.com/images/1766566442808-761588425.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S27EFH2CDEK71GVQC839G","2025-12-24 08:54:29.074+00","2025-12-24 08:54:29.074+00",NULL,"https://storage.danakcorp.com/images/1766566468108-519151895.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S2N705Z754YS3YFQW4AGQ","2025-12-24 08:54:43.169+00","2025-12-24 08:54:43.169+00",NULL,"https://storage.danakcorp.com/images/1766566482278-661272876.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S31HNKPAAK58CX77BNJCA","2025-12-24 08:54:55.798+00","2025-12-24 08:54:55.798+00",NULL,"https://storage.danakcorp.com/images/1766566494050-408184442.svg","01KD7DGBDFX76VKXMKQKR51RJK"
"01KD7S3AX9VW5B830MYJYGXQ0B","2025-12-24 08:55:05.385+00","2025-12-24 08:55:05.385+00",NULL,"https://storage.danakcorp.com/images/1766566504760-709260995.svg","01KD7DGBDFX76VKXMKQKR51RJK"
1 id created_at updated_at deleted_at url group_id icon_name
2 01KD7DGWH6TDB47V4710FDT7SR 2025-12-24 05:32:46.503+00 2025-12-24 05:32:46.503+00 NULL https://storage.danakcorp.com/images/1766554365832-411750172.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
3 01KD7DH9F6BN4TEM96TA2QT4GJ 2025-12-24 05:32:59.751+00 2025-12-24 05:32:59.751+00 NULL https://storage.danakcorp.com/images/1766554379149-104970938.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
4 01KD7DHK6ZTEGPRBNXJPV49TE6 2025-12-24 05:33:09.727+00 2025-12-24 05:33:09.727+00 NULL https://storage.danakcorp.com/images/1766554389090-593863597.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
5 01KD7DJ2NVEHVQMDCAM31VKKGK 2025-12-24 05:33:25.564+00 2025-12-24 05:33:25.564+00 NULL https://storage.danakcorp.com/images/1766554404968-905984091.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
6 01KD7DJCQY1V84DAVXPV097HE6 2025-12-24 05:33:35.871+00 2025-12-24 05:33:35.871+00 NULL https://storage.danakcorp.com/images/1766554415325-873800591.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
7 01KD7DJSZ2XVX971FGNFRRDQV6 2025-12-24 05:33:49.411+00 2025-12-24 05:33:49.411+00 NULL https://storage.danakcorp.com/images/1766554428814-77027818.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
8 01KD7DK3HJAD9SMDD8CM1RPNQS 2025-12-24 05:33:59.218+00 2025-12-24 05:33:59.218+00 NULL https://storage.danakcorp.com/images/1766554438632-45072052.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
9 01KD7DKB23B6XWJ8CMAKQ5R8HS 2025-12-24 05:34:06.915+00 2025-12-24 05:34:06.915+00 NULL https://storage.danakcorp.com/images/1766554446340-463157109.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
10 01KD7DKN32VCDQDF2D407MGKF0 2025-12-24 05:34:17.186+00 2025-12-24 05:34:17.186+00 NULL https://storage.danakcorp.com/images/1766554456605-280166176.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
11 01KD7DM4AEQ2FCJGZM390N7YVJ 2025-12-24 05:34:32.782+00 2025-12-24 05:34:32.782+00 NULL https://storage.danakcorp.com/images/1766554471887-315640495.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
12 01KD7DMHKWJSHX6BR41FTDYVHE 2025-12-24 05:34:46.396+00 2025-12-24 05:34:46.396+00 NULL https://storage.danakcorp.com/images/1766554485634-242317100.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
13 01KD7DN3XNDW2JQ0RGM711R9Q5 2025-12-24 05:35:05.141+00 2025-12-24 05:35:05.141+00 NULL https://storage.danakcorp.com/images/1766554504553-157481346.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
14 01KD7DNV0F9CPMKQ4R482MTVKJ 2025-12-24 05:35:28.783+00 2025-12-24 05:35:28.783+00 NULL https://storage.danakcorp.com/images/1766554528198-919877647.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
15 01KD7DPAKCPXD2GK0FCZ7N3XXP 2025-12-24 05:35:44.749+00 2025-12-24 05:35:44.749+00 NULL https://storage.danakcorp.com/images/1766554544140-886744157.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
16 01KD7DPZGZFZPS5AJ8RNM3W16F 2025-12-24 05:36:06.175+00 2025-12-24 05:36:06.175+00 NULL https://storage.danakcorp.com/images/1766554565614-684869010.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
17 01KD7DQCMAD39T5QB5728M6PFP 2025-12-24 05:36:19.594+00 2025-12-24 05:36:19.594+00 NULL https://storage.danakcorp.com/images/1766554578855-318583145.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
18 01KD7DQTQHGWP5J77V327Y4GHZ 2025-12-24 05:36:34.034+00 2025-12-24 05:36:34.034+00 NULL https://storage.danakcorp.com/images/1766554593272-362758712.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
19 01KD7DR75VSDY0B28AFTB6QP9M 2025-12-24 05:36:46.779+00 2025-12-24 05:36:46.779+00 NULL https://storage.danakcorp.com/images/1766554606166-877198688.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
20 01KD7DRRT3B7BAA714MMYVVHN4 2025-12-24 05:37:04.835+00 2025-12-24 05:37:04.835+00 NULL https://storage.danakcorp.com/images/1766554624133-21439996.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
21 01KD7DS9TQY1KWPM2HY7VC7THR 2025-12-24 05:37:22.263+00 2025-12-24 05:37:22.263+00 NULL https://storage.danakcorp.com/images/1766554641691-535696200.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
22 01KD7DST85ZZ0SKSVDYJWN67VQ 2025-12-24 05:37:39.078+00 2025-12-24 05:37:39.078+00 NULL https://storage.danakcorp.com/images/1766554658481-114730426.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
23 01KD7DT75Y84N1ECNV2WW6VQ37 2025-12-24 05:37:52.318+00 2025-12-24 05:37:52.318+00 NULL https://storage.danakcorp.com/images/1766554671760-634916570.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
24 01KD7E3837FJ9TDDKPF8S6PHK6 2025-12-24 05:42:48.167+00 2025-12-24 05:42:48.167+00 NULL https://storage.danakcorp.com/images/1766554967381-660612838.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
25 01KD7E7G89J5KH2ZT45F0MT5X3 2025-12-24 05:45:07.593+00 2025-12-24 05:45:07.593+00 NULL https://storage.danakcorp.com/images/1766555106925-267924006.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
26 01KD7EBA6ATBBFVMGWQHACZ1G7 2025-12-24 05:47:12.459+00 2025-12-24 05:47:12.459+00 NULL https://storage.danakcorp.com/images/1766555231674-387319393.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
27 01KD7ED3NRKEX3Z8G6CTHK5QAB 2025-12-24 05:48:11.32+00 2025-12-24 05:48:11.32+00 NULL https://storage.danakcorp.com/images/1766555290519-155637622.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
28 01KD7EDN07W94SGG04BRNRNRWT 2025-12-24 05:48:29.063+00 2025-12-24 05:48:29.063+00 NULL https://storage.danakcorp.com/images/1766555308301-230359494.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
29 01KD7EH2W75B2H8JGMHR9P76ZE 2025-12-24 05:50:21.576+00 2025-12-24 05:50:21.576+00 NULL https://storage.danakcorp.com/images/1766555420851-186440015.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
30 01KD7EP8V3KQYMDCDMXYP1QKPK 2025-12-24 05:53:11.523+00 2025-12-24 05:53:11.523+00 NULL https://storage.danakcorp.com/images/1766555590828-644527730.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
31 01KD7EPRJWQAQX00F0J02FV05J 2025-12-24 05:53:27.644+00 2025-12-24 05:53:27.644+00 NULL https://storage.danakcorp.com/images/1766555606997-571682462.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
32 01KD7ERGH1ABTPGSJN1WFTESP6 2025-12-24 05:54:24.929+00 2025-12-24 05:54:24.929+00 NULL https://storage.danakcorp.com/images/1766555663659-433052581.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
33 01KD7ET0394XN31KFT3340R04H 2025-12-24 05:55:13.641+00 2025-12-24 05:55:13.641+00 NULL https://storage.danakcorp.com/images/1766555713098-988242030.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
34 01KD7ETCZBFV8ZZF1GFDKP7DXA 2025-12-24 05:55:26.827+00 2025-12-24 05:55:26.827+00 NULL https://storage.danakcorp.com/images/1766555726158-680948882.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
35 01KD7EV0VW1M18RGBB54C4YDAC 2025-12-24 05:55:47.196+00 2025-12-24 05:55:47.196+00 NULL https://storage.danakcorp.com/images/1766555746650-426516324.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
36 01KD7EVMP392QF71V6QFGK9XQA 2025-12-24 05:56:07.491+00 2025-12-24 05:56:07.491+00 NULL https://storage.danakcorp.com/images/1766555765798-127083646.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
37 01KD7EW6MQX0VFPS21TJPSEBAH 2025-12-24 05:56:25.879+00 2025-12-24 05:56:25.879+00 NULL https://storage.danakcorp.com/images/1766555784837-243715698.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
38 01KD7EWMGEHYQRKBM6XBS9EKTF 2025-12-24 05:56:40.078+00 2025-12-24 05:56:40.078+00 NULL https://storage.danakcorp.com/images/1766555799191-594035927.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
39 01KD7EZHSEJ4G27VT3HS7JZ8YN 2025-12-24 05:58:15.599+00 2025-12-24 05:58:15.599+00 NULL https://storage.danakcorp.com/images/1766555894624-524350777.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
40 01KD7F0PCEADF1HWJ47V8RP1ZP 2025-12-24 05:58:53.07+00 2025-12-24 05:58:53.07+00 NULL https://storage.danakcorp.com/images/1766555932114-485954688.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
41 01KD7F184CHB129A900K2V6N94 2025-12-24 05:59:11.244+00 2025-12-24 05:59:11.244+00 NULL https://storage.danakcorp.com/images/1766555949650-909996041.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
42 01KD7F1R0JNNRP8QW8NHANZB01 2025-12-24 05:59:27.506+00 2025-12-24 05:59:27.506+00 NULL https://storage.danakcorp.com/images/1766555966861-75517122.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
43 01KD7F2Z1V0T1S47R7VWBEPY66 2025-12-24 06:00:07.483+00 2025-12-24 06:00:07.483+00 NULL https://storage.danakcorp.com/images/1766556006875-239689681.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
44 01KD7FAP3S04D5CW51W98YZRHE 2025-12-24 06:04:20.473+00 2025-12-24 06:04:20.473+00 NULL https://storage.danakcorp.com/images/1766556257163-459958715.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
45 01KD7FBQJAV9HJMG4EVRKPSAC5 2025-12-24 06:04:54.73+00 2025-12-24 06:04:54.73+00 NULL https://storage.danakcorp.com/images/1766556293812-665835248.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
46 01KD7FC48FRARBP7ZXN7SPGP8K 2025-12-24 06:05:07.727+00 2025-12-24 06:05:07.727+00 NULL https://storage.danakcorp.com/images/1766556307082-162280793.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
47 01KD7FD4SW2G73QX0YY6KNK7V4 2025-12-24 06:05:41.052+00 2025-12-24 06:05:41.052+00 NULL https://storage.danakcorp.com/images/1766556340402-744632392.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
48 01KD7FGRC85J6JXEYNBGCPY0S4 2025-12-24 06:07:39.4+00 2025-12-24 06:07:39.4+00 NULL https://storage.danakcorp.com/images/1766556458335-857124841.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
49 01KD7FHCW86SPVBSC8N62YEY5R 2025-12-24 06:08:00.393+00 2025-12-24 06:08:00.393+00 NULL https://storage.danakcorp.com/images/1766556479530-161860621.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
50 01KD7FHYWZ73T36XKTDQ5FEN7G 2025-12-24 06:08:18.847+00 2025-12-24 06:08:18.847+00 NULL https://storage.danakcorp.com/images/1766556498215-91175544.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
51 01KD7FQDYPGWD05HHATWBC499Z 2025-12-24 06:11:18.103+00 2025-12-24 06:11:18.103+00 NULL https://storage.danakcorp.com/images/1766556677355-108854669.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
52 01KD7FS7QGJJ3YRSM10Y5YWD6C 2025-12-24 06:12:17.264+00 2025-12-24 06:12:17.264+00 NULL https://storage.danakcorp.com/images/1766556736283-691587656.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
53 01KD7FSY2XKBREKJ9HCNJN924C 2025-12-24 06:12:40.157+00 2025-12-24 06:12:40.157+00 NULL https://storage.danakcorp.com/images/1766556759367-58066921.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
54 01KD7FTFZ25MHQATRC41Y3EPQS 2025-12-24 06:12:58.468+00 2025-12-24 06:12:58.468+00 NULL https://storage.danakcorp.com/images/1766556777353-490756125.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
55 01KD7FXARH84SJYESXVAG0AH3Q 2025-12-24 06:14:31.441+00 2025-12-24 06:14:31.441+00 NULL https://storage.danakcorp.com/images/1766556870740-118553581.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
56 01KD7FXXV4DP74NGWJC0J43MDP 2025-12-24 06:14:50.98+00 2025-12-24 06:14:50.98+00 NULL https://storage.danakcorp.com/images/1766556890334-962678132.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
57 01KD7FYVRNQAAA0DQZ8NZ02F2R 2025-12-24 06:15:21.621+00 2025-12-24 06:15:21.621+00 NULL https://storage.danakcorp.com/images/1766556921029-865260745.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
58 01KD7FZMJ4QGTG6NZEDKWP7V0W 2025-12-24 06:15:47.013+00 2025-12-24 06:15:47.013+00 NULL https://storage.danakcorp.com/images/1766556946363-320833501.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
59 01KD7G0F8GTY7KZ530Y9XF8YXF 2025-12-24 06:16:14.352+00 2025-12-24 06:16:14.352+00 NULL https://storage.danakcorp.com/images/1766556973771-832565827.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
60 01KD7G300731YZQDT3FVAP0KHH 2025-12-24 06:17:37.031+00 2025-12-24 06:17:37.031+00 NULL https://storage.danakcorp.com/images/1766557056358-727862983.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
61 01KD7G7BHM8WEXFHHKD09MG10J 2025-12-24 06:19:59.925+00 2025-12-24 06:19:59.925+00 NULL https://storage.danakcorp.com/images/1766557199259-72955190.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
62 01KD7G8ZM2FPZ9H5BVGD983CTS 2025-12-24 06:20:53.25+00 2025-12-24 06:20:53.25+00 NULL https://storage.danakcorp.com/images/1766557252452-988016525.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
63 01KD7G9N7J647RH94K5WQB1Q3B 2025-12-24 06:21:15.378+00 2025-12-24 06:21:15.378+00 NULL https://storage.danakcorp.com/images/1766557274778-439028456.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
64 01KD7GA36DDJBQJ15XT4D0E2D6 2025-12-24 06:21:29.678+00 2025-12-24 06:21:29.678+00 NULL https://storage.danakcorp.com/images/1766557289094-415104854.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
65 01KD7GAEZKQKRC80EP7F03K22K 2025-12-24 06:21:41.747+00 2025-12-24 06:21:41.747+00 NULL https://storage.danakcorp.com/images/1766557300923-302277606.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
66 01KD7GAYJAHXAZV84PN3X2N4FY 2025-12-24 06:21:57.707+00 2025-12-24 06:21:57.707+00 NULL https://storage.danakcorp.com/images/1766557317091-602116315.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
67 01KD7GN1W5Q0WZWCJR8V62AD88 2025-12-24 06:27:28.774+00 2025-12-24 06:27:28.774+00 NULL https://storage.danakcorp.com/images/1766557647999-47333939.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
68 01KD7GNPEX4CMZHRVYB4AZYSK3 2025-12-24 06:27:49.853+00 2025-12-24 06:27:49.853+00 NULL https://storage.danakcorp.com/images/1766557669282-172512141.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
69 01KD7GPSY27QGH5XSPXWHD8VJ5 2025-12-24 06:28:26.179+00 2025-12-24 06:28:26.179+00 NULL https://storage.danakcorp.com/images/1766557705283-692546793.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
70 01KD7GQGZK52YM87E283013JS4 2025-12-24 06:28:49.779+00 2025-12-24 06:28:49.779+00 NULL https://storage.danakcorp.com/images/1766557729175-762576743.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
71 01KD7GRBRZTFFHDS1A6MH2CMJ4 2025-12-24 06:29:17.215+00 2025-12-24 06:29:17.215+00 NULL https://storage.danakcorp.com/images/1766557756487-665300947.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
72 01KD7GRYXHE220H9C3VH4VAC8E 2025-12-24 06:29:36.817+00 2025-12-24 06:29:36.817+00 NULL https://storage.danakcorp.com/images/1766557776067-804149228.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
73 01KD7GSKMZH8WAH5VYAS4GH02C 2025-12-24 06:29:58.048+00 2025-12-24 06:29:58.048+00 NULL https://storage.danakcorp.com/images/1766557797509-101726352.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
74 01KD7GT6A83SX8NKPVHYXAKJ3H 2025-12-24 06:30:17.16+00 2025-12-24 06:30:17.16+00 NULL https://storage.danakcorp.com/images/1766557816484-357699340.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
75 01KD7GTTKT3H3EDFTRCY8P2AE9 2025-12-24 06:30:37.946+00 2025-12-24 06:30:37.946+00 NULL https://storage.danakcorp.com/images/1766557837292-326521286.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
76 01KD7J0VP01FCP0TEHYG0MD191 2025-12-24 06:51:24.224+00 2025-12-24 06:51:24.224+00 NULL https://storage.danakcorp.com/images/1766559083575-564721059.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
77 01KD7J1BMTMBMV009EYC7N4G7D 2025-12-24 06:51:40.57+00 2025-12-24 06:51:40.57+00 NULL https://storage.danakcorp.com/images/1766559099561-720966516.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
78 01KD7J1W90CASGWZKBFMN75STB 2025-12-24 06:51:57.601+00 2025-12-24 06:51:57.601+00 NULL https://storage.danakcorp.com/images/1766559116496-834964990.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
79 01KD7NS1HJ2C707G3KXRRZCN7Y 2025-12-24 07:57:02.387+00 2025-12-24 07:57:02.387+00 NULL https://storage.danakcorp.com/images/1766563021426-600579018.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
80 01KD7NSHYZ6XESEJR7MCKS7CY0 2025-12-24 07:57:19.199+00 2025-12-24 07:57:19.199+00 NULL https://storage.danakcorp.com/images/1766563038525-727531190.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
81 01KD7NSYX5W066SDN0RYBKXHQ3 2025-12-24 07:57:32.454+00 2025-12-24 07:57:32.454+00 NULL https://storage.danakcorp.com/images/1766563051485-824602001.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
82 01KD7NTCZK4NYE3KCYDM1247GP 2025-12-24 07:57:46.867+00 2025-12-24 07:57:46.867+00 NULL https://storage.danakcorp.com/images/1766563066017-372210647.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
83 01KD7NW5KZEW4M2ZJX93T53G41 2025-12-24 07:58:44.863+00 2025-12-24 07:58:44.863+00 NULL https://storage.danakcorp.com/images/1766563124296-331607800.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
84 01KD7NWQ7YFJ7HDNWRB5F1V47Q 2025-12-24 07:59:02.91+00 2025-12-24 07:59:02.91+00 NULL https://storage.danakcorp.com/images/1766563142133-359920423.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
85 01KD7NXCZSEN7M2TN0DT0XVVR9 2025-12-24 07:59:25.177+00 2025-12-24 07:59:25.177+00 NULL https://storage.danakcorp.com/images/1766563164270-350101986.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
86 01KD7NXXMYZFMAWMRS23GRZSKP 2025-12-24 07:59:42.238+00 2025-12-24 07:59:42.238+00 NULL https://storage.danakcorp.com/images/1766563181587-86630830.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
87 01KD7NYP6M0HNC3A4XCXHK4W4M 2025-12-24 08:00:07.38+00 2025-12-24 08:00:07.38+00 NULL https://storage.danakcorp.com/images/1766563204966-651499380.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
88 01KD7NZ3PDZX4WTVV3KC5MTD8K 2025-12-24 08:00:21.198+00 2025-12-24 08:00:21.198+00 NULL https://storage.danakcorp.com/images/1766563220596-103699525.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
89 01KD7P0FWPK3HTYVX14XMGCHTC 2025-12-24 08:01:06.455+00 2025-12-24 08:01:06.455+00 NULL https://storage.danakcorp.com/images/1766563265634-373953449.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
90 01KD7P0SAN0PA6KAGE3ABAXSWF 2025-12-24 08:01:16.117+00 2025-12-24 08:01:16.117+00 NULL https://storage.danakcorp.com/images/1766563275517-958045207.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
91 01KD7P1429PQT8NFDFRZEDNH1P 2025-12-24 08:01:27.113+00 2025-12-24 08:01:27.113+00 NULL https://storage.danakcorp.com/images/1766563286526-622849562.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
92 01KD7QC4M3D2SF26ZH0S0SDWBD 2025-12-24 08:24:56.708+00 2025-12-24 08:24:56.708+00 NULL https://storage.danakcorp.com/images/1766564695339-504553420.svg 01KD7DGBDFX76VKXMKQKR51RJK
93 01KD7QCEDA0T3CGZ01NGBKWZ9S 2025-12-24 08:25:06.73+00 2025-12-24 08:25:06.73+00 NULL https://storage.danakcorp.com/images/1766564706032-159367016.svg 01KD7DGBDFX76VKXMKQKR51RJK
94 01KD7QCWQ00TVCK7BAAHSWA09H 2025-12-24 08:25:21.376+00 2025-12-24 08:25:21.376+00 NULL https://storage.danakcorp.com/images/1766564720683-887818252.svg 01KD7DGBDFX76VKXMKQKR51RJK
95 01KD7QDA1R9WTTV2JWQ2Y3WGST 2025-12-24 08:25:35.032+00 2025-12-24 08:25:35.032+00 NULL https://storage.danakcorp.com/images/1766564734432-483528219.svg 01KD7DGBDFX76VKXMKQKR51RJK
96 01KD7QDSCQNW2JC4SAQCK0QBNW 2025-12-24 08:25:50.743+00 2025-12-24 08:25:50.743+00 NULL https://storage.danakcorp.com/images/1766564750099-854146988.svg 01KD7DGBDFX76VKXMKQKR51RJK
97 01KD7QE1NXGG08JQ5F2BC35NFC 2025-12-24 08:25:59.23+00 2025-12-24 08:25:59.23+00 NULL https://storage.danakcorp.com/images/1766564758573-696060786.svg 01KD7DGBDFX76VKXMKQKR51RJK
98 01KD7QFFXDVTSB5VAXB640HR7N 2025-12-24 08:26:46.574+00 2025-12-24 08:26:46.574+00 NULL https://storage.danakcorp.com/images/1766564805823-829938380.svg 01KD7DGBDFX76VKXMKQKR51RJK
99 01KD7QJ37DT4NCD6QY9MNGCZXZ 2025-12-24 08:28:11.885+00 2025-12-24 08:28:11.885+00 NULL https://storage.danakcorp.com/images/1766564891137-468827500.svg 01KD7DGBDFX76VKXMKQKR51RJK
100 01KD7QJJZX6Y7CR8R60W3TNKRS 2025-12-24 08:28:28.029+00 2025-12-24 08:28:28.029+00 NULL https://storage.danakcorp.com/images/1766564907215-921380792.svg 01KD7DGBDFX76VKXMKQKR51RJK
101 01KD7QK69ZH6B4S602YKZCBVMB 2025-12-24 08:28:47.807+00 2025-12-24 08:28:47.807+00 NULL https://storage.danakcorp.com/images/1766564926872-672612697.svg 01KD7DGBDFX76VKXMKQKR51RJK
102 01KD7R3EM739KW6PV431V6P19S 2025-12-24 08:37:40.616+00 2025-12-24 08:37:40.616+00 NULL https://storage.danakcorp.com/images/1766565459604-638026702.svg 01KD7DGBDFX76VKXMKQKR51RJK
103 01KD7R3YZ6SAQWZ4CMC4GCJ1V4 2025-12-24 08:37:57.351+00 2025-12-24 08:37:57.351+00 NULL https://storage.danakcorp.com/images/1766565476771-42527917.svg 01KD7DGBDFX76VKXMKQKR51RJK
104 01KD7R4D5BVGPWKXE6A7FNVNRA 2025-12-24 08:38:11.883+00 2025-12-24 08:38:11.883+00 NULL https://storage.danakcorp.com/images/1766565491264-946731491.svg 01KD7DGBDFX76VKXMKQKR51RJK
105 01KD7R4RFYFFQ4GV5TQ3DH6BW6 2025-12-24 08:38:23.486+00 2025-12-24 08:38:23.486+00 NULL https://storage.danakcorp.com/images/1766565502475-288189136.svg 01KD7DGBDFX76VKXMKQKR51RJK
106 01KD7R5KMSAGCXZCTQPTVYDTA7 2025-12-24 08:38:51.289+00 2025-12-24 08:38:51.289+00 NULL https://storage.danakcorp.com/images/1766565530628-800474580.svg 01KD7DGBDFX76VKXMKQKR51RJK
107 01KD7R64HYQD37W8ASB2Q817MW 2025-12-24 08:39:08.606+00 2025-12-24 08:39:08.606+00 NULL https://storage.danakcorp.com/images/1766565547983-878658938.svg 01KD7DGBDFX76VKXMKQKR51RJK
108 01KD7R6KF9YWZBZ3WPAKXQS2CH 2025-12-24 08:39:23.881+00 2025-12-24 08:39:23.881+00 NULL https://storage.danakcorp.com/images/1766565563263-891688321.svg 01KD7DGBDFX76VKXMKQKR51RJK
109 01KD7R7RM02R8FZ4DN65WMJVE8 2025-12-24 08:40:01.92+00 2025-12-24 08:40:01.92+00 NULL https://storage.danakcorp.com/images/1766565600547-704332792.svg 01KD7DG4VCTWZRSA54M3PEJ6NW
110 01KD7R8G53XBC6AG6KAVSBW94G 2025-12-24 08:40:26.019+00 2025-12-24 08:40:26.019+00 NULL https://storage.danakcorp.com/images/1766565625437-528383713.svg 01KD7DGBDFX76VKXMKQKR51RJK
111 01KD7R901TPNAX9YYRG6W0GWHF 2025-12-24 08:40:42.298+00 2025-12-24 08:40:42.298+00 NULL https://storage.danakcorp.com/images/1766565641620-53176909.svg 01KD7DGBDFX76VKXMKQKR51RJK
112 01KD7R9PKF9G1DD1HMS2QQQN9V 2025-12-24 08:41:05.391+00 2025-12-24 08:41:05.391+00 NULL https://storage.danakcorp.com/images/1766565664427-433669317.svg 01KD7DGBDFX76VKXMKQKR51RJK
113 01KD7RK2G6E5JKEDK9P95W79PR 2025-12-24 08:46:12.487+00 2025-12-24 08:46:12.487+00 NULL https://storage.danakcorp.com/images/1766565971515-159903323.svg 01KD7DGBDFX76VKXMKQKR51RJK
114 01KD7RKSM9QD6RA2H6WGE23KEC 2025-12-24 08:46:36.169+00 2025-12-24 08:46:36.169+00 NULL https://storage.danakcorp.com/images/1766565995424-288101561.svg 01KD7DGBDFX76VKXMKQKR51RJK
115 01KD7RMR9DRA1TG2BJ9Q8QAZ15 2025-12-24 08:47:07.565+00 2025-12-24 08:47:07.565+00 NULL https://storage.danakcorp.com/images/1766566026942-868807643.svg 01KD7DGBDFX76VKXMKQKR51RJK
116 01KD7RN6FH0TX69RZTKJKM6MEN 2025-12-24 08:47:22.097+00 2025-12-24 08:47:22.097+00 NULL https://storage.danakcorp.com/images/1766566041525-973019175.svg 01KD7DGBDFX76VKXMKQKR51RJK
117 01KD7RNX4EE92QR2C24577BJCW 2025-12-24 08:47:45.294+00 2025-12-24 08:47:45.294+00 NULL https://storage.danakcorp.com/images/1766566064741-271844433.svg 01KD7DGBDFX76VKXMKQKR51RJK
118 01KD7RPHHCKNXNQH29WSG84D2Z 2025-12-24 08:48:06.189+00 2025-12-24 08:48:06.189+00 NULL https://storage.danakcorp.com/images/1766566084368-438871956.svg 01KD7DGBDFX76VKXMKQKR51RJK
119 01KD7RQ6MFA5CDQ4J55NG3N7PX 2025-12-24 08:48:27.791+00 2025-12-24 08:48:27.791+00 NULL https://storage.danakcorp.com/images/1766566106954-47179607.svg 01KD7DGBDFX76VKXMKQKR51RJK
120 01KD7RQNXJNRXS8MXV57R50PKN 2025-12-24 08:48:43.443+00 2025-12-24 08:48:43.443+00 NULL https://storage.danakcorp.com/images/1766566122836-486871124.svg 01KD7DGBDFX76VKXMKQKR51RJK
121 01KD7RR9W6B9BTWJ0N2H11TWM1 2025-12-24 08:49:03.879+00 2025-12-24 08:49:03.879+00 NULL https://storage.danakcorp.com/images/1766566143171-668309076.svg 01KD7DGBDFX76VKXMKQKR51RJK
122 01KD7RRVQWYHSHN3M7R25GNGHS 2025-12-24 08:49:22.172+00 2025-12-24 08:49:22.172+00 NULL https://storage.danakcorp.com/images/1766566161551-844376214.svg 01KD7DGBDFX76VKXMKQKR51RJK
123 01KD7RSAKAP1TJ59GN11DEAXD9 2025-12-24 08:49:37.386+00 2025-12-24 08:49:37.386+00 NULL https://storage.danakcorp.com/images/1766566176755-30806596.svg 01KD7DGBDFX76VKXMKQKR51RJK
124 01KD7RSV35PY1A6193H4GDW1ZW 2025-12-24 08:49:54.277+00 2025-12-24 08:49:54.277+00 NULL https://storage.danakcorp.com/images/1766566193720-339416210.svg 01KD7DGBDFX76VKXMKQKR51RJK
125 01KD7RWX3D4ZZE35W345HDK35S 2025-12-24 08:51:34.638+00 2025-12-24 08:51:34.638+00 NULL https://storage.danakcorp.com/images/1766566293736-981836756.svg 01KD7DGBDFX76VKXMKQKR51RJK
126 01KD7RX79BVR30WGKXAZQ3D0CM 2025-12-24 08:51:45.068+00 2025-12-24 08:51:45.068+00 NULL https://storage.danakcorp.com/images/1766566304318-193301989.svg 01KD7DGBDFX76VKXMKQKR51RJK
127 01KD7RXY99C8PV6SAR0AJ00F16 2025-12-24 08:52:08.618+00 2025-12-24 08:52:08.618+00 NULL https://storage.danakcorp.com/images/1766566327768-2675508.svg 01KD7DGBDFX76VKXMKQKR51RJK
128 01KD7RYBPV273KCR350ESK2FN1 2025-12-24 08:52:22.364+00 2025-12-24 08:52:22.364+00 NULL https://storage.danakcorp.com/images/1766566341717-462491426.svg 01KD7DGBDFX76VKXMKQKR51RJK
129 01KD7RYTNTE20YS29X2XJT7GY3 2025-12-24 08:52:37.69+00 2025-12-24 08:52:37.69+00 NULL https://storage.danakcorp.com/images/1766566357028-432111036.svg 01KD7DGBDFX76VKXMKQKR51RJK
130 01KD7RZ8K86GCJSW1E0EC5T4B2 2025-12-24 08:52:51.944+00 2025-12-24 08:52:51.944+00 NULL https://storage.danakcorp.com/images/1766566371354-279596850.svg 01KD7DGBDFX76VKXMKQKR51RJK
131 01KD7RZPNGASPYFP9XKMSY208B 2025-12-24 08:53:06.352+00 2025-12-24 08:53:06.352+00 NULL https://storage.danakcorp.com/images/1766566385674-569493499.svg 01KD7DGBDFX76VKXMKQKR51RJK
132 01KD7S0HEQZ4AY5TTFKF36HS6F 2025-12-24 08:53:33.783+00 2025-12-24 08:53:33.783+00 NULL https://storage.danakcorp.com/images/1766566413058-975642584.svg 01KD7DGBDFX76VKXMKQKR51RJK
133 01KD7S0ZEQ6BH163HCX0MQZHBJ 2025-12-24 08:53:48.119+00 2025-12-24 08:53:48.119+00 NULL https://storage.danakcorp.com/images/1766566427440-982841441.svg 01KD7DGBDFX76VKXMKQKR51RJK
134 01KD7S1EF6R6J6AMMC6HP5J0AA 2025-12-24 08:54:03.494+00 2025-12-24 08:54:03.494+00 NULL https://storage.danakcorp.com/images/1766566442808-761588425.svg 01KD7DGBDFX76VKXMKQKR51RJK
135 01KD7S27EFH2CDEK71GVQC839G 2025-12-24 08:54:29.074+00 2025-12-24 08:54:29.074+00 NULL https://storage.danakcorp.com/images/1766566468108-519151895.svg 01KD7DGBDFX76VKXMKQKR51RJK
136 01KD7S2N705Z754YS3YFQW4AGQ 2025-12-24 08:54:43.169+00 2025-12-24 08:54:43.169+00 NULL https://storage.danakcorp.com/images/1766566482278-661272876.svg 01KD7DGBDFX76VKXMKQKR51RJK
137 01KD7S31HNKPAAK58CX77BNJCA 2025-12-24 08:54:55.798+00 2025-12-24 08:54:55.798+00 NULL https://storage.danakcorp.com/images/1766566494050-408184442.svg 01KD7DGBDFX76VKXMKQKR51RJK
138 01KD7S3AX9VW5B830MYJYGXQ0B 2025-12-24 08:55:05.385+00 2025-12-24 08:55:05.385+00 NULL https://storage.danakcorp.com/images/1766566504760-709260995.svg 01KD7DGBDFX76VKXMKQKR51RJK
+205
View File
@@ -0,0 +1,205 @@
--
-- PostgreSQL database dump
--
\restrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: icon_groups; Type: TABLE DATA; Schema: public; Owner: -
--
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DG4VCTWZRSA54M3PEJ6NW', '2025-12-24 05:32:22.252+00', '2025-12-24 05:32:22.252+00', NULL, 'پک آیکون 1');
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DGBDFX76VKXMKQKR51RJK', '2025-12-24 05:32:28.976+00', '2025-12-24 05:32:28.976+00', NULL, 'پک آیکون 2');
--
-- PostgreSQL database dump complete
--
\unrestrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
--
-- PostgreSQL database dump
--
\restrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: icons; Type: TABLE DATA; Schema: public; Owner: -
--
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DGWH6TDB47V4710FDT7SR', '2025-12-24 05:32:46.503+00', '2025-12-24 05:32:46.503+00', NULL, 'https://storage.danakcorp.com/images/1766554365832-411750172.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DH9F6BN4TEM96TA2QT4GJ', '2025-12-24 05:32:59.751+00', '2025-12-24 05:32:59.751+00', NULL, 'https://storage.danakcorp.com/images/1766554379149-104970938.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DHK6ZTEGPRBNXJPV49TE6', '2025-12-24 05:33:09.727+00', '2025-12-24 05:33:09.727+00', NULL, 'https://storage.danakcorp.com/images/1766554389090-593863597.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJ2NVEHVQMDCAM31VKKGK', '2025-12-24 05:33:25.564+00', '2025-12-24 05:33:25.564+00', NULL, 'https://storage.danakcorp.com/images/1766554404968-905984091.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJCQY1V84DAVXPV097HE6', '2025-12-24 05:33:35.871+00', '2025-12-24 05:33:35.871+00', NULL, 'https://storage.danakcorp.com/images/1766554415325-873800591.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJSZ2XVX971FGNFRRDQV6', '2025-12-24 05:33:49.411+00', '2025-12-24 05:33:49.411+00', NULL, 'https://storage.danakcorp.com/images/1766554428814-77027818.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DK3HJAD9SMDD8CM1RPNQS', '2025-12-24 05:33:59.218+00', '2025-12-24 05:33:59.218+00', NULL, 'https://storage.danakcorp.com/images/1766554438632-45072052.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKB23B6XWJ8CMAKQ5R8HS', '2025-12-24 05:34:06.915+00', '2025-12-24 05:34:06.915+00', NULL, 'https://storage.danakcorp.com/images/1766554446340-463157109.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKN32VCDQDF2D407MGKF0', '2025-12-24 05:34:17.186+00', '2025-12-24 05:34:17.186+00', NULL, 'https://storage.danakcorp.com/images/1766554456605-280166176.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DM4AEQ2FCJGZM390N7YVJ', '2025-12-24 05:34:32.782+00', '2025-12-24 05:34:32.782+00', NULL, 'https://storage.danakcorp.com/images/1766554471887-315640495.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DMHKWJSHX6BR41FTDYVHE', '2025-12-24 05:34:46.396+00', '2025-12-24 05:34:46.396+00', NULL, 'https://storage.danakcorp.com/images/1766554485634-242317100.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DN3XNDW2JQ0RGM711R9Q5', '2025-12-24 05:35:05.141+00', '2025-12-24 05:35:05.141+00', NULL, 'https://storage.danakcorp.com/images/1766554504553-157481346.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DNV0F9CPMKQ4R482MTVKJ', '2025-12-24 05:35:28.783+00', '2025-12-24 05:35:28.783+00', NULL, 'https://storage.danakcorp.com/images/1766554528198-919877647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPAKCPXD2GK0FCZ7N3XXP', '2025-12-24 05:35:44.749+00', '2025-12-24 05:35:44.749+00', NULL, 'https://storage.danakcorp.com/images/1766554544140-886744157.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPZGZFZPS5AJ8RNM3W16F', '2025-12-24 05:36:06.175+00', '2025-12-24 05:36:06.175+00', NULL, 'https://storage.danakcorp.com/images/1766554565614-684869010.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQCMAD39T5QB5728M6PFP', '2025-12-24 05:36:19.594+00', '2025-12-24 05:36:19.594+00', NULL, 'https://storage.danakcorp.com/images/1766554578855-318583145.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQTQHGWP5J77V327Y4GHZ', '2025-12-24 05:36:34.034+00', '2025-12-24 05:36:34.034+00', NULL, 'https://storage.danakcorp.com/images/1766554593272-362758712.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DR75VSDY0B28AFTB6QP9M', '2025-12-24 05:36:46.779+00', '2025-12-24 05:36:46.779+00', NULL, 'https://storage.danakcorp.com/images/1766554606166-877198688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DRRT3B7BAA714MMYVVHN4', '2025-12-24 05:37:04.835+00', '2025-12-24 05:37:04.835+00', NULL, 'https://storage.danakcorp.com/images/1766554624133-21439996.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DS9TQY1KWPM2HY7VC7THR', '2025-12-24 05:37:22.263+00', '2025-12-24 05:37:22.263+00', NULL, 'https://storage.danakcorp.com/images/1766554641691-535696200.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DST85ZZ0SKSVDYJWN67VQ', '2025-12-24 05:37:39.078+00', '2025-12-24 05:37:39.078+00', NULL, 'https://storage.danakcorp.com/images/1766554658481-114730426.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DT75Y84N1ECNV2WW6VQ37', '2025-12-24 05:37:52.318+00', '2025-12-24 05:37:52.318+00', NULL, 'https://storage.danakcorp.com/images/1766554671760-634916570.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E3837FJ9TDDKPF8S6PHK6', '2025-12-24 05:42:48.167+00', '2025-12-24 05:42:48.167+00', NULL, 'https://storage.danakcorp.com/images/1766554967381-660612838.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E7G89J5KH2ZT45F0MT5X3', '2025-12-24 05:45:07.593+00', '2025-12-24 05:45:07.593+00', NULL, 'https://storage.danakcorp.com/images/1766555106925-267924006.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EBA6ATBBFVMGWQHACZ1G7', '2025-12-24 05:47:12.459+00', '2025-12-24 05:47:12.459+00', NULL, 'https://storage.danakcorp.com/images/1766555231674-387319393.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ED3NRKEX3Z8G6CTHK5QAB', '2025-12-24 05:48:11.32+00', '2025-12-24 05:48:11.32+00', NULL, 'https://storage.danakcorp.com/images/1766555290519-155637622.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EDN07W94SGG04BRNRNRWT', '2025-12-24 05:48:29.063+00', '2025-12-24 05:48:29.063+00', NULL, 'https://storage.danakcorp.com/images/1766555308301-230359494.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EH2W75B2H8JGMHR9P76ZE', '2025-12-24 05:50:21.576+00', '2025-12-24 05:50:21.576+00', NULL, 'https://storage.danakcorp.com/images/1766555420851-186440015.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EP8V3KQYMDCDMXYP1QKPK', '2025-12-24 05:53:11.523+00', '2025-12-24 05:53:11.523+00', NULL, 'https://storage.danakcorp.com/images/1766555590828-644527730.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EPRJWQAQX00F0J02FV05J', '2025-12-24 05:53:27.644+00', '2025-12-24 05:53:27.644+00', NULL, 'https://storage.danakcorp.com/images/1766555606997-571682462.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ERGH1ABTPGSJN1WFTESP6', '2025-12-24 05:54:24.929+00', '2025-12-24 05:54:24.929+00', NULL, 'https://storage.danakcorp.com/images/1766555663659-433052581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ET0394XN31KFT3340R04H', '2025-12-24 05:55:13.641+00', '2025-12-24 05:55:13.641+00', NULL, 'https://storage.danakcorp.com/images/1766555713098-988242030.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ETCZBFV8ZZF1GFDKP7DXA', '2025-12-24 05:55:26.827+00', '2025-12-24 05:55:26.827+00', NULL, 'https://storage.danakcorp.com/images/1766555726158-680948882.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EV0VW1M18RGBB54C4YDAC', '2025-12-24 05:55:47.196+00', '2025-12-24 05:55:47.196+00', NULL, 'https://storage.danakcorp.com/images/1766555746650-426516324.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EVMP392QF71V6QFGK9XQA', '2025-12-24 05:56:07.491+00', '2025-12-24 05:56:07.491+00', NULL, 'https://storage.danakcorp.com/images/1766555765798-127083646.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EW6MQX0VFPS21TJPSEBAH', '2025-12-24 05:56:25.879+00', '2025-12-24 05:56:25.879+00', NULL, 'https://storage.danakcorp.com/images/1766555784837-243715698.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EWMGEHYQRKBM6XBS9EKTF', '2025-12-24 05:56:40.078+00', '2025-12-24 05:56:40.078+00', NULL, 'https://storage.danakcorp.com/images/1766555799191-594035927.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EZHSEJ4G27VT3HS7JZ8YN', '2025-12-24 05:58:15.599+00', '2025-12-24 05:58:15.599+00', NULL, 'https://storage.danakcorp.com/images/1766555894624-524350777.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F0PCEADF1HWJ47V8RP1ZP', '2025-12-24 05:58:53.07+00', '2025-12-24 05:58:53.07+00', NULL, 'https://storage.danakcorp.com/images/1766555932114-485954688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F184CHB129A900K2V6N94', '2025-12-24 05:59:11.244+00', '2025-12-24 05:59:11.244+00', NULL, 'https://storage.danakcorp.com/images/1766555949650-909996041.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F1R0JNNRP8QW8NHANZB01', '2025-12-24 05:59:27.506+00', '2025-12-24 05:59:27.506+00', NULL, 'https://storage.danakcorp.com/images/1766555966861-75517122.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F2Z1V0T1S47R7VWBEPY66', '2025-12-24 06:00:07.483+00', '2025-12-24 06:00:07.483+00', NULL, 'https://storage.danakcorp.com/images/1766556006875-239689681.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FAP3S04D5CW51W98YZRHE', '2025-12-24 06:04:20.473+00', '2025-12-24 06:04:20.473+00', NULL, 'https://storage.danakcorp.com/images/1766556257163-459958715.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FBQJAV9HJMG4EVRKPSAC5', '2025-12-24 06:04:54.73+00', '2025-12-24 06:04:54.73+00', NULL, 'https://storage.danakcorp.com/images/1766556293812-665835248.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FC48FRARBP7ZXN7SPGP8K', '2025-12-24 06:05:07.727+00', '2025-12-24 06:05:07.727+00', NULL, 'https://storage.danakcorp.com/images/1766556307082-162280793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FD4SW2G73QX0YY6KNK7V4', '2025-12-24 06:05:41.052+00', '2025-12-24 06:05:41.052+00', NULL, 'https://storage.danakcorp.com/images/1766556340402-744632392.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FGRC85J6JXEYNBGCPY0S4', '2025-12-24 06:07:39.4+00', '2025-12-24 06:07:39.4+00', NULL, 'https://storage.danakcorp.com/images/1766556458335-857124841.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHCW86SPVBSC8N62YEY5R', '2025-12-24 06:08:00.393+00', '2025-12-24 06:08:00.393+00', NULL, 'https://storage.danakcorp.com/images/1766556479530-161860621.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHYWZ73T36XKTDQ5FEN7G', '2025-12-24 06:08:18.847+00', '2025-12-24 06:08:18.847+00', NULL, 'https://storage.danakcorp.com/images/1766556498215-91175544.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FQDYPGWD05HHATWBC499Z', '2025-12-24 06:11:18.103+00', '2025-12-24 06:11:18.103+00', NULL, 'https://storage.danakcorp.com/images/1766556677355-108854669.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FS7QGJJ3YRSM10Y5YWD6C', '2025-12-24 06:12:17.264+00', '2025-12-24 06:12:17.264+00', NULL, 'https://storage.danakcorp.com/images/1766556736283-691587656.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FSY2XKBREKJ9HCNJN924C', '2025-12-24 06:12:40.157+00', '2025-12-24 06:12:40.157+00', NULL, 'https://storage.danakcorp.com/images/1766556759367-58066921.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FTFZ25MHQATRC41Y3EPQS', '2025-12-24 06:12:58.468+00', '2025-12-24 06:12:58.468+00', NULL, 'https://storage.danakcorp.com/images/1766556777353-490756125.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXARH84SJYESXVAG0AH3Q', '2025-12-24 06:14:31.441+00', '2025-12-24 06:14:31.441+00', NULL, 'https://storage.danakcorp.com/images/1766556870740-118553581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXXV4DP74NGWJC0J43MDP', '2025-12-24 06:14:50.98+00', '2025-12-24 06:14:50.98+00', NULL, 'https://storage.danakcorp.com/images/1766556890334-962678132.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FYVRNQAAA0DQZ8NZ02F2R', '2025-12-24 06:15:21.621+00', '2025-12-24 06:15:21.621+00', NULL, 'https://storage.danakcorp.com/images/1766556921029-865260745.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FZMJ4QGTG6NZEDKWP7V0W', '2025-12-24 06:15:47.013+00', '2025-12-24 06:15:47.013+00', NULL, 'https://storage.danakcorp.com/images/1766556946363-320833501.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G0F8GTY7KZ530Y9XF8YXF', '2025-12-24 06:16:14.352+00', '2025-12-24 06:16:14.352+00', NULL, 'https://storage.danakcorp.com/images/1766556973771-832565827.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G300731YZQDT3FVAP0KHH', '2025-12-24 06:17:37.031+00', '2025-12-24 06:17:37.031+00', NULL, 'https://storage.danakcorp.com/images/1766557056358-727862983.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G7BHM8WEXFHHKD09MG10J', '2025-12-24 06:19:59.925+00', '2025-12-24 06:19:59.925+00', NULL, 'https://storage.danakcorp.com/images/1766557199259-72955190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G8ZM2FPZ9H5BVGD983CTS', '2025-12-24 06:20:53.25+00', '2025-12-24 06:20:53.25+00', NULL, 'https://storage.danakcorp.com/images/1766557252452-988016525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G9N7J647RH94K5WQB1Q3B', '2025-12-24 06:21:15.378+00', '2025-12-24 06:21:15.378+00', NULL, 'https://storage.danakcorp.com/images/1766557274778-439028456.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GA36DDJBQJ15XT4D0E2D6', '2025-12-24 06:21:29.678+00', '2025-12-24 06:21:29.678+00', NULL, 'https://storage.danakcorp.com/images/1766557289094-415104854.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAEZKQKRC80EP7F03K22K', '2025-12-24 06:21:41.747+00', '2025-12-24 06:21:41.747+00', NULL, 'https://storage.danakcorp.com/images/1766557300923-302277606.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAYJAHXAZV84PN3X2N4FY', '2025-12-24 06:21:57.707+00', '2025-12-24 06:21:57.707+00', NULL, 'https://storage.danakcorp.com/images/1766557317091-602116315.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GN1W5Q0WZWCJR8V62AD88', '2025-12-24 06:27:28.774+00', '2025-12-24 06:27:28.774+00', NULL, 'https://storage.danakcorp.com/images/1766557647999-47333939.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GNPEX4CMZHRVYB4AZYSK3', '2025-12-24 06:27:49.853+00', '2025-12-24 06:27:49.853+00', NULL, 'https://storage.danakcorp.com/images/1766557669282-172512141.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GPSY27QGH5XSPXWHD8VJ5', '2025-12-24 06:28:26.179+00', '2025-12-24 06:28:26.179+00', NULL, 'https://storage.danakcorp.com/images/1766557705283-692546793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GQGZK52YM87E283013JS4', '2025-12-24 06:28:49.779+00', '2025-12-24 06:28:49.779+00', NULL, 'https://storage.danakcorp.com/images/1766557729175-762576743.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRBRZTFFHDS1A6MH2CMJ4', '2025-12-24 06:29:17.215+00', '2025-12-24 06:29:17.215+00', NULL, 'https://storage.danakcorp.com/images/1766557756487-665300947.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRYXHE220H9C3VH4VAC8E', '2025-12-24 06:29:36.817+00', '2025-12-24 06:29:36.817+00', NULL, 'https://storage.danakcorp.com/images/1766557776067-804149228.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GSKMZH8WAH5VYAS4GH02C', '2025-12-24 06:29:58.048+00', '2025-12-24 06:29:58.048+00', NULL, 'https://storage.danakcorp.com/images/1766557797509-101726352.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GT6A83SX8NKPVHYXAKJ3H', '2025-12-24 06:30:17.16+00', '2025-12-24 06:30:17.16+00', NULL, 'https://storage.danakcorp.com/images/1766557816484-357699340.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GTTKT3H3EDFTRCY8P2AE9', '2025-12-24 06:30:37.946+00', '2025-12-24 06:30:37.946+00', NULL, 'https://storage.danakcorp.com/images/1766557837292-326521286.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J0VP01FCP0TEHYG0MD191', '2025-12-24 06:51:24.224+00', '2025-12-24 06:51:24.224+00', NULL, 'https://storage.danakcorp.com/images/1766559083575-564721059.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1BMTMBMV009EYC7N4G7D', '2025-12-24 06:51:40.57+00', '2025-12-24 06:51:40.57+00', NULL, 'https://storage.danakcorp.com/images/1766559099561-720966516.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1W90CASGWZKBFMN75STB', '2025-12-24 06:51:57.601+00', '2025-12-24 06:51:57.601+00', NULL, 'https://storage.danakcorp.com/images/1766559116496-834964990.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NS1HJ2C707G3KXRRZCN7Y', '2025-12-24 07:57:02.387+00', '2025-12-24 07:57:02.387+00', NULL, 'https://storage.danakcorp.com/images/1766563021426-600579018.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSHYZ6XESEJR7MCKS7CY0', '2025-12-24 07:57:19.199+00', '2025-12-24 07:57:19.199+00', NULL, 'https://storage.danakcorp.com/images/1766563038525-727531190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSYX5W066SDN0RYBKXHQ3', '2025-12-24 07:57:32.454+00', '2025-12-24 07:57:32.454+00', NULL, 'https://storage.danakcorp.com/images/1766563051485-824602001.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NTCZK4NYE3KCYDM1247GP', '2025-12-24 07:57:46.867+00', '2025-12-24 07:57:46.867+00', NULL, 'https://storage.danakcorp.com/images/1766563066017-372210647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NW5KZEW4M2ZJX93T53G41', '2025-12-24 07:58:44.863+00', '2025-12-24 07:58:44.863+00', NULL, 'https://storage.danakcorp.com/images/1766563124296-331607800.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NWQ7YFJ7HDNWRB5F1V47Q', '2025-12-24 07:59:02.91+00', '2025-12-24 07:59:02.91+00', NULL, 'https://storage.danakcorp.com/images/1766563142133-359920423.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXCZSEN7M2TN0DT0XVVR9', '2025-12-24 07:59:25.177+00', '2025-12-24 07:59:25.177+00', NULL, 'https://storage.danakcorp.com/images/1766563164270-350101986.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXXMYZFMAWMRS23GRZSKP', '2025-12-24 07:59:42.238+00', '2025-12-24 07:59:42.238+00', NULL, 'https://storage.danakcorp.com/images/1766563181587-86630830.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NYP6M0HNC3A4XCXHK4W4M', '2025-12-24 08:00:07.38+00', '2025-12-24 08:00:07.38+00', NULL, 'https://storage.danakcorp.com/images/1766563204966-651499380.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NZ3PDZX4WTVV3KC5MTD8K', '2025-12-24 08:00:21.198+00', '2025-12-24 08:00:21.198+00', NULL, 'https://storage.danakcorp.com/images/1766563220596-103699525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0FWPK3HTYVX14XMGCHTC', '2025-12-24 08:01:06.455+00', '2025-12-24 08:01:06.455+00', NULL, 'https://storage.danakcorp.com/images/1766563265634-373953449.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0SAN0PA6KAGE3ABAXSWF', '2025-12-24 08:01:16.117+00', '2025-12-24 08:01:16.117+00', NULL, 'https://storage.danakcorp.com/images/1766563275517-958045207.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P1429PQT8NFDFRZEDNH1P', '2025-12-24 08:01:27.113+00', '2025-12-24 08:01:27.113+00', NULL, 'https://storage.danakcorp.com/images/1766563286526-622849562.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QC4M3D2SF26ZH0S0SDWBD', '2025-12-24 08:24:56.708+00', '2025-12-24 08:24:56.708+00', NULL, 'https://storage.danakcorp.com/images/1766564695339-504553420.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCEDA0T3CGZ01NGBKWZ9S', '2025-12-24 08:25:06.73+00', '2025-12-24 08:25:06.73+00', NULL, 'https://storage.danakcorp.com/images/1766564706032-159367016.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCWQ00TVCK7BAAHSWA09H', '2025-12-24 08:25:21.376+00', '2025-12-24 08:25:21.376+00', NULL, 'https://storage.danakcorp.com/images/1766564720683-887818252.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDA1R9WTTV2JWQ2Y3WGST', '2025-12-24 08:25:35.032+00', '2025-12-24 08:25:35.032+00', NULL, 'https://storage.danakcorp.com/images/1766564734432-483528219.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDSCQNW2JC4SAQCK0QBNW', '2025-12-24 08:25:50.743+00', '2025-12-24 08:25:50.743+00', NULL, 'https://storage.danakcorp.com/images/1766564750099-854146988.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QE1NXGG08JQ5F2BC35NFC', '2025-12-24 08:25:59.23+00', '2025-12-24 08:25:59.23+00', NULL, 'https://storage.danakcorp.com/images/1766564758573-696060786.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QFFXDVTSB5VAXB640HR7N', '2025-12-24 08:26:46.574+00', '2025-12-24 08:26:46.574+00', NULL, 'https://storage.danakcorp.com/images/1766564805823-829938380.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJ37DT4NCD6QY9MNGCZXZ', '2025-12-24 08:28:11.885+00', '2025-12-24 08:28:11.885+00', NULL, 'https://storage.danakcorp.com/images/1766564891137-468827500.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJJZX6Y7CR8R60W3TNKRS', '2025-12-24 08:28:28.029+00', '2025-12-24 08:28:28.029+00', NULL, 'https://storage.danakcorp.com/images/1766564907215-921380792.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QK69ZH6B4S602YKZCBVMB', '2025-12-24 08:28:47.807+00', '2025-12-24 08:28:47.807+00', NULL, 'https://storage.danakcorp.com/images/1766564926872-672612697.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3EM739KW6PV431V6P19S', '2025-12-24 08:37:40.616+00', '2025-12-24 08:37:40.616+00', NULL, 'https://storage.danakcorp.com/images/1766565459604-638026702.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3YZ6SAQWZ4CMC4GCJ1V4', '2025-12-24 08:37:57.351+00', '2025-12-24 08:37:57.351+00', NULL, 'https://storage.danakcorp.com/images/1766565476771-42527917.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4D5BVGPWKXE6A7FNVNRA', '2025-12-24 08:38:11.883+00', '2025-12-24 08:38:11.883+00', NULL, 'https://storage.danakcorp.com/images/1766565491264-946731491.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4RFYFFQ4GV5TQ3DH6BW6', '2025-12-24 08:38:23.486+00', '2025-12-24 08:38:23.486+00', NULL, 'https://storage.danakcorp.com/images/1766565502475-288189136.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R5KMSAGCXZCTQPTVYDTA7', '2025-12-24 08:38:51.289+00', '2025-12-24 08:38:51.289+00', NULL, 'https://storage.danakcorp.com/images/1766565530628-800474580.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R64HYQD37W8ASB2Q817MW', '2025-12-24 08:39:08.606+00', '2025-12-24 08:39:08.606+00', NULL, 'https://storage.danakcorp.com/images/1766565547983-878658938.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R6KF9YWZBZ3WPAKXQS2CH', '2025-12-24 08:39:23.881+00', '2025-12-24 08:39:23.881+00', NULL, 'https://storage.danakcorp.com/images/1766565563263-891688321.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R7RM02R8FZ4DN65WMJVE8', '2025-12-24 08:40:01.92+00', '2025-12-24 08:40:01.92+00', NULL, 'https://storage.danakcorp.com/images/1766565600547-704332792.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R8G53XBC6AG6KAVSBW94G', '2025-12-24 08:40:26.019+00', '2025-12-24 08:40:26.019+00', NULL, 'https://storage.danakcorp.com/images/1766565625437-528383713.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R901TPNAX9YYRG6W0GWHF', '2025-12-24 08:40:42.298+00', '2025-12-24 08:40:42.298+00', NULL, 'https://storage.danakcorp.com/images/1766565641620-53176909.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R9PKF9G1DD1HMS2QQQN9V', '2025-12-24 08:41:05.391+00', '2025-12-24 08:41:05.391+00', NULL, 'https://storage.danakcorp.com/images/1766565664427-433669317.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RK2G6E5JKEDK9P95W79PR', '2025-12-24 08:46:12.487+00', '2025-12-24 08:46:12.487+00', NULL, 'https://storage.danakcorp.com/images/1766565971515-159903323.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RKSM9QD6RA2H6WGE23KEC', '2025-12-24 08:46:36.169+00', '2025-12-24 08:46:36.169+00', NULL, 'https://storage.danakcorp.com/images/1766565995424-288101561.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RMR9DRA1TG2BJ9Q8QAZ15', '2025-12-24 08:47:07.565+00', '2025-12-24 08:47:07.565+00', NULL, 'https://storage.danakcorp.com/images/1766566026942-868807643.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RN6FH0TX69RZTKJKM6MEN', '2025-12-24 08:47:22.097+00', '2025-12-24 08:47:22.097+00', NULL, 'https://storage.danakcorp.com/images/1766566041525-973019175.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RNX4EE92QR2C24577BJCW', '2025-12-24 08:47:45.294+00', '2025-12-24 08:47:45.294+00', NULL, 'https://storage.danakcorp.com/images/1766566064741-271844433.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RPHHCKNXNQH29WSG84D2Z', '2025-12-24 08:48:06.189+00', '2025-12-24 08:48:06.189+00', NULL, 'https://storage.danakcorp.com/images/1766566084368-438871956.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQ6MFA5CDQ4J55NG3N7PX', '2025-12-24 08:48:27.791+00', '2025-12-24 08:48:27.791+00', NULL, 'https://storage.danakcorp.com/images/1766566106954-47179607.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQNXJNRXS8MXV57R50PKN', '2025-12-24 08:48:43.443+00', '2025-12-24 08:48:43.443+00', NULL, 'https://storage.danakcorp.com/images/1766566122836-486871124.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RR9W6B9BTWJ0N2H11TWM1', '2025-12-24 08:49:03.879+00', '2025-12-24 08:49:03.879+00', NULL, 'https://storage.danakcorp.com/images/1766566143171-668309076.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RRVQWYHSHN3M7R25GNGHS', '2025-12-24 08:49:22.172+00', '2025-12-24 08:49:22.172+00', NULL, 'https://storage.danakcorp.com/images/1766566161551-844376214.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSAKAP1TJ59GN11DEAXD9', '2025-12-24 08:49:37.386+00', '2025-12-24 08:49:37.386+00', NULL, 'https://storage.danakcorp.com/images/1766566176755-30806596.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSV35PY1A6193H4GDW1ZW', '2025-12-24 08:49:54.277+00', '2025-12-24 08:49:54.277+00', NULL, 'https://storage.danakcorp.com/images/1766566193720-339416210.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RWX3D4ZZE35W345HDK35S', '2025-12-24 08:51:34.638+00', '2025-12-24 08:51:34.638+00', NULL, 'https://storage.danakcorp.com/images/1766566293736-981836756.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RX79BVR30WGKXAZQ3D0CM', '2025-12-24 08:51:45.068+00', '2025-12-24 08:51:45.068+00', NULL, 'https://storage.danakcorp.com/images/1766566304318-193301989.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RXY99C8PV6SAR0AJ00F16', '2025-12-24 08:52:08.618+00', '2025-12-24 08:52:08.618+00', NULL, 'https://storage.danakcorp.com/images/1766566327768-2675508.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYBPV273KCR350ESK2FN1', '2025-12-24 08:52:22.364+00', '2025-12-24 08:52:22.364+00', NULL, 'https://storage.danakcorp.com/images/1766566341717-462491426.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYTNTE20YS29X2XJT7GY3', '2025-12-24 08:52:37.69+00', '2025-12-24 08:52:37.69+00', NULL, 'https://storage.danakcorp.com/images/1766566357028-432111036.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZ8K86GCJSW1E0EC5T4B2', '2025-12-24 08:52:51.944+00', '2025-12-24 08:52:51.944+00', NULL, 'https://storage.danakcorp.com/images/1766566371354-279596850.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZPNGASPYFP9XKMSY208B', '2025-12-24 08:53:06.352+00', '2025-12-24 08:53:06.352+00', NULL, 'https://storage.danakcorp.com/images/1766566385674-569493499.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0HEQZ4AY5TTFKF36HS6F', '2025-12-24 08:53:33.783+00', '2025-12-24 08:53:33.783+00', NULL, 'https://storage.danakcorp.com/images/1766566413058-975642584.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0ZEQ6BH163HCX0MQZHBJ', '2025-12-24 08:53:48.119+00', '2025-12-24 08:53:48.119+00', NULL, 'https://storage.danakcorp.com/images/1766566427440-982841441.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S1EF6R6J6AMMC6HP5J0AA', '2025-12-24 08:54:03.494+00', '2025-12-24 08:54:03.494+00', NULL, 'https://storage.danakcorp.com/images/1766566442808-761588425.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S27EFH2CDEK71GVQC839G', '2025-12-24 08:54:29.074+00', '2025-12-24 08:54:29.074+00', NULL, 'https://storage.danakcorp.com/images/1766566468108-519151895.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S2N705Z754YS3YFQW4AGQ', '2025-12-24 08:54:43.169+00', '2025-12-24 08:54:43.169+00', NULL, 'https://storage.danakcorp.com/images/1766566482278-661272876.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S31HNKPAAK58CX77BNJCA', '2025-12-24 08:54:55.798+00', '2025-12-24 08:54:55.798+00', NULL, 'https://storage.danakcorp.com/images/1766566494050-408184442.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S3AX9VW5B830MYJYGXQ0B', '2025-12-24 08:55:05.385+00', '2025-12-24 08:55:05.385+00', NULL, 'https://storage.danakcorp.com/images/1766566504760-709260995.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
--
-- PostgreSQL database dump complete
--
\unrestrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
+138
View File
@@ -0,0 +1,138 @@
Index,Name,URL
1,bread,https://storage.danakcorp.com/images/1766554365832-411750172.svg
2,avocado,https://storage.danakcorp.com/images/1766554379149-104970938.svg
3,knife,https://storage.danakcorp.com/images/1766554389090-593863597.svg
4,baby bottle,https://storage.danakcorp.com/images/1766554404968-905984091.svg
5,baby bottle,https://storage.danakcorp.com/images/1766554415325-873800591.svg
6,baby pacifier,https://storage.danakcorp.com/images/1766554428814-77027818.svg
7,banana,https://storage.danakcorp.com/images/1766554438632-45072052.svg
8,barbecue,https://storage.danakcorp.com/images/1766554446340-463157109.svg
9,barbeque2,https://storage.danakcorp.com/images/1766554456605-280166176.svg
10,drink,https://storage.danakcorp.com/images/1766554471887-315640495.svg
11,hanburger,https://storage.danakcorp.com/images/1766554485634-242317100.svg
12,drink2,https://storage.danakcorp.com/images/1766554504553-157481346.svg
13,bottle1,https://storage.danakcorp.com/images/1766554528198-919877647.svg
14,bottle2,https://storage.danakcorp.com/images/1766554544140-886744157.svg
15,broccoli,https://storage.danakcorp.com/images/1766554565614-684869010.svg
16,cake1,https://storage.danakcorp.com/images/1766554578855-318583145.svg
17,cake2,https://storage.danakcorp.com/images/1766554593272-362758712.svg
18,cake piece,https://storage.danakcorp.com/images/1766554606166-877198688.svg
19,cup cake,https://storage.danakcorp.com/images/1766554624133-21439996.svg
20,caupcake2,https://storage.danakcorp.com/images/1766554641691-535696200.svg
21,candle,https://storage.danakcorp.com/images/1766554658481-114730426.svg
22,candy,https://storage.danakcorp.com/images/1766554671760-634916570.svg
23,carrot,https://storage.danakcorp.com/images/1766554967381-660612838.svg
24,cheese,https://storage.danakcorp.com/images/1766555106925-267924006.svg
25,cheese2,https://storage.danakcorp.com/images/1766555231674-387319393.svg
26,hat robe,https://storage.danakcorp.com/images/1766555290519-155637622.svg
27,hat robe 2,https://storage.danakcorp.com/images/1766555308301-230359494.svg
28,cherry,https://storage.danakcorp.com/images/1766555420851-186440015.svg
29,chicken,https://storage.danakcorp.com/images/1766555590828-644527730.svg
30,baken,https://storage.danakcorp.com/images/1766555606997-571682462.svg
31,lollipop,https://storage.danakcorp.com/images/1766555663659-433052581.svg
32,cookie1,https://storage.danakcorp.com/images/1766555713098-988242030.svg
33,cookie2,https://storage.danakcorp.com/images/1766555726158-680948882.svg
34,crosson,https://storage.danakcorp.com/images/1766555746650-426516324.svg
35,juice,https://storage.danakcorp.com/images/1766555765798-127083646.svg
36,juice2,https://storage.danakcorp.com/images/1766555784837-243715698.svg
37,juice3,https://storage.danakcorp.com/images/1766555799191-594035927.svg
38,spoon and fork 1,https://storage.danakcorp.com/images/1766555894624-524350777.svg
39,spoon and fork 2,https://storage.danakcorp.com/images/1766555932114-485954688.svg
40,spoon and knife,https://storage.danakcorp.com/images/1766555949650-909996041.svg
41,fork and knife,https://storage.danakcorp.com/images/1766555966861-75517122.svg
42,hot dog,https://storage.danakcorp.com/images/1766556006875-239689681.svg
43,soap,https://storage.danakcorp.com/images/1766556257163-459958715.svg
44,dish,https://storage.danakcorp.com/images/1766556293812-665835248.svg
45,donuts,https://storage.danakcorp.com/images/1766556307082-162280793.svg
46,egg plant,https://storage.danakcorp.com/images/1766556340402-744632392.svg
47,egg,https://storage.danakcorp.com/images/1766556458335-857124841.svg
48,fast-food,https://storage.danakcorp.com/images/1766556479530-161860621.svg
49,taco,https://storage.danakcorp.com/images/1766556498215-91175544.svg
50,fire,https://storage.danakcorp.com/images/1766556677355-108854669.svg
51,fish,https://storage.danakcorp.com/images/1766556736283-691587656.svg
52,fish,https://storage.danakcorp.com/images/1766556759367-58066921.svg
53,french fries,https://storage.danakcorp.com/images/1766556777353-490756125.svg
54,fried egg,https://storage.danakcorp.com/images/1766556870740-118553581.svg
55,refrigerator,https://storage.danakcorp.com/images/1766556890334-962678132.svg
56,grape,https://storage.danakcorp.com/images/1766556921029-865260745.svg
57,hat robe,https://storage.danakcorp.com/images/1766556946363-320833501.svg
58,hot dog,https://storage.danakcorp.com/images/1766556973771-832565827.svg
59,ice cream,https://storage.danakcorp.com/images/1766557056358-727862983.svg
60,ice cream,https://storage.danakcorp.com/images/1766557199259-72955190.svg
61,ice cream 3,https://storage.danakcorp.com/images/1766557252452-988016525.svg
62,jar,https://storage.danakcorp.com/images/1766557274778-439028456.svg
63,jelly,https://storage.danakcorp.com/images/1766557289094-415104854.svg
64,juice1,https://storage.danakcorp.com/images/1766557300923-302277606.svg
65,juice 2,https://storage.danakcorp.com/images/1766557317091-602116315.svg
66,kitchen stove,https://storage.danakcorp.com/images/1766557647999-47333939.svg
67,orange slash,https://storage.danakcorp.com/images/1766557669282-172512141.svg
68,loaf of bread,https://storage.danakcorp.com/images/1766557705283-692546793.svg
69,loaf of bread 2,https://storage.danakcorp.com/images/1766557729175-762576743.svg
70,meat,https://storage.danakcorp.com/images/1766557756487-665300947.svg
71,meat 2,https://storage.danakcorp.com/images/1766557776067-804149228.svg
72,mixer,https://storage.danakcorp.com/images/1766557797509-101726352.svg
73,mortar,https://storage.danakcorp.com/images/1766557816484-357699340.svg
74,noodle,https://storage.danakcorp.com/images/1766557837292-326521286.svg
75,nuts,https://storage.danakcorp.com/images/1766559083575-564721059.svg
76,olive,https://storage.danakcorp.com/images/1766559099561-720966516.svg
77,onion,https://storage.danakcorp.com/images/1766559116496-834964990.svg
78,pizza,https://storage.danakcorp.com/images/1766563021426-600579018.svg
79,rolling pin,https://storage.danakcorp.com/images/1766563038525-727531190.svg
80,solt,https://storage.danakcorp.com/images/1766563051485-824602001.svg
81,scales,https://storage.danakcorp.com/images/1766563066017-372210647.svg
82,basket,https://storage.danakcorp.com/images/1766563124296-331607800.svg
83,skewer,https://storage.danakcorp.com/images/1766563142133-359920423.svg
84,strawberry,https://storage.danakcorp.com/images/1766563164270-350101986.svg
85,toast,https://storage.danakcorp.com/images/1766563181587-86630830.svg
86,watermelon,https://storage.danakcorp.com/images/1766563204966-651499380.svg
87,whisk,https://storage.danakcorp.com/images/1766563220596-103699525.svg
88,wine glass,https://storage.danakcorp.com/images/1766563265634-373953449.svg
89,wine glass 2,https://storage.danakcorp.com/images/1766563275517-958045207.svg
90,wine glass 3,https://storage.danakcorp.com/images/1766563286526-622849562.svg
91,colored bakeries,https://storage.danakcorp.com/images/1766564695339-504553420.svg
92,colored bio,https://storage.danakcorp.com/images/1766564706032-159367016.svg
93,colored bibimbap,https://storage.danakcorp.com/images/1766564720683-887818252.svg
94,colored boba,https://storage.danakcorp.com/images/1766564734432-483528219.svg
95,colored breakfast,https://storage.danakcorp.com/images/1766564750099-854146988.svg
96,colored hamburger,https://storage.danakcorp.com/images/1766564758573-696060786.svg
97,colored hamburger 2,https://storage.danakcorp.com/images/1766564805823-829938380.svg
98,colored coffee,https://storage.danakcorp.com/images/1766564891137-468827500.svg
99,colored coca,https://storage.danakcorp.com/images/1766564907215-921380792.svg
100,colored cookies,https://storage.danakcorp.com/images/1766564926872-672612697.svg
101,colored noodle,https://storage.danakcorp.com/images/1766565459604-638026702.svg
102,colored crossan,https://storage.danakcorp.com/images/1766565476771-42527917.svg
103,colored sweet,https://storage.danakcorp.com/images/1766565491264-946731491.svg
104,colored dumpling,https://storage.danakcorp.com/images/1766565502475-288189136.svg
105,colored fish and chips,https://storage.danakcorp.com/images/1766565530628-800474580.svg
106,colored focaccia,https://storage.danakcorp.com/images/1766565547983-878658938.svg
107,colored french fries,https://storage.danakcorp.com/images/1766565563263-891688321.svg
108,colored fried chicken,https://storage.danakcorp.com/images/1766565600547-704332792.svg
109,colored fried rice,https://storage.danakcorp.com/images/1766565625437-528383713.svg
110,colored fries,https://storage.danakcorp.com/images/1766565641620-53176909.svg
111,hat robe,https://storage.danakcorp.com/images/1766565664427-433669317.svg
112,colored hot dog,https://storage.danakcorp.com/images/1766565971515-159903323.svg
113,colored hot dog,https://storage.danakcorp.com/images/1766565995424-288101561.svg
114,colored hot pot,https://storage.danakcorp.com/images/1766566026942-868807643.svg
115,colored ice cream,https://storage.danakcorp.com/images/1766566041525-973019175.svg
116,colored ice tea,https://storage.danakcorp.com/images/1766566064741-271844433.svg
117,colored instant noodle cup,https://storage.danakcorp.com/images/1766566084368-438871956.svg
118,lemon,https://storage.danakcorp.com/images/1766566106954-47179607.svg
119,colored martini,https://storage.danakcorp.com/images/1766566122836-486871124.svg
120,colored match,https://storage.danakcorp.com/images/1766566143171-668309076.svg
121,colored mochi donuts,https://storage.danakcorp.com/images/1766566161551-844376214.svg
122,colored mochi,https://storage.danakcorp.com/images/1766566176755-30806596.svg
123,colored nachos,https://storage.danakcorp.com/images/1766566193720-339416210.svg
124,colored origini,https://storage.danakcorp.com/images/1766566293736-981836756.svg
125,colored pan cake,https://storage.danakcorp.com/images/1766566304318-193301989.svg
126,colored pepperoni pizza,https://storage.danakcorp.com/images/1766566327768-2675508.svg
127,colored pizza 4,https://storage.danakcorp.com/images/1766566341717-462491426.svg
128,colored podding,https://storage.danakcorp.com/images/1766566357028-432111036.svg
129,quesadilla,https://storage.danakcorp.com/images/1766566371354-279596850.svg
130,salad,https://storage.danakcorp.com/images/1766566385674-569493499.svg
131,colored sandwich,https://storage.danakcorp.com/images/1766566413058-975642584.svg
132,colored stak,https://storage.danakcorp.com/images/1766566427440-982841441.svg
133,colored soshi,https://storage.danakcorp.com/images/1766566442808-761588425.svg
134,colored taco,https://storage.danakcorp.com/images/1766566468108-519151895.svg
135,colored tempura,https://storage.danakcorp.com/images/1766566482278-661272876.svg
136,colored tafu,https://storage.danakcorp.com/images/1766566494050-408184442.svg
137,whisk,https://storage.danakcorp.com/images/1766566504760-709260995.svg
1 Index Name URL
2 1 bread https://storage.danakcorp.com/images/1766554365832-411750172.svg
3 2 avocado https://storage.danakcorp.com/images/1766554379149-104970938.svg
4 3 knife https://storage.danakcorp.com/images/1766554389090-593863597.svg
5 4 baby bottle https://storage.danakcorp.com/images/1766554404968-905984091.svg
6 5 baby bottle https://storage.danakcorp.com/images/1766554415325-873800591.svg
7 6 baby pacifier https://storage.danakcorp.com/images/1766554428814-77027818.svg
8 7 banana https://storage.danakcorp.com/images/1766554438632-45072052.svg
9 8 barbecue https://storage.danakcorp.com/images/1766554446340-463157109.svg
10 9 barbeque2 https://storage.danakcorp.com/images/1766554456605-280166176.svg
11 10 drink https://storage.danakcorp.com/images/1766554471887-315640495.svg
12 11 hanburger https://storage.danakcorp.com/images/1766554485634-242317100.svg
13 12 drink2 https://storage.danakcorp.com/images/1766554504553-157481346.svg
14 13 bottle1 https://storage.danakcorp.com/images/1766554528198-919877647.svg
15 14 bottle2 https://storage.danakcorp.com/images/1766554544140-886744157.svg
16 15 broccoli https://storage.danakcorp.com/images/1766554565614-684869010.svg
17 16 cake1 https://storage.danakcorp.com/images/1766554578855-318583145.svg
18 17 cake2 https://storage.danakcorp.com/images/1766554593272-362758712.svg
19 18 cake piece https://storage.danakcorp.com/images/1766554606166-877198688.svg
20 19 cup cake https://storage.danakcorp.com/images/1766554624133-21439996.svg
21 20 caupcake2 https://storage.danakcorp.com/images/1766554641691-535696200.svg
22 21 candle https://storage.danakcorp.com/images/1766554658481-114730426.svg
23 22 candy https://storage.danakcorp.com/images/1766554671760-634916570.svg
24 23 carrot https://storage.danakcorp.com/images/1766554967381-660612838.svg
25 24 cheese https://storage.danakcorp.com/images/1766555106925-267924006.svg
26 25 cheese2 https://storage.danakcorp.com/images/1766555231674-387319393.svg
27 26 hat robe https://storage.danakcorp.com/images/1766555290519-155637622.svg
28 27 hat robe 2 https://storage.danakcorp.com/images/1766555308301-230359494.svg
29 28 cherry https://storage.danakcorp.com/images/1766555420851-186440015.svg
30 29 chicken https://storage.danakcorp.com/images/1766555590828-644527730.svg
31 30 baken https://storage.danakcorp.com/images/1766555606997-571682462.svg
32 31 lollipop https://storage.danakcorp.com/images/1766555663659-433052581.svg
33 32 cookie1 https://storage.danakcorp.com/images/1766555713098-988242030.svg
34 33 cookie2 https://storage.danakcorp.com/images/1766555726158-680948882.svg
35 34 crosson https://storage.danakcorp.com/images/1766555746650-426516324.svg
36 35 juice https://storage.danakcorp.com/images/1766555765798-127083646.svg
37 36 juice2 https://storage.danakcorp.com/images/1766555784837-243715698.svg
38 37 juice3 https://storage.danakcorp.com/images/1766555799191-594035927.svg
39 38 spoon and fork 1 https://storage.danakcorp.com/images/1766555894624-524350777.svg
40 39 spoon and fork 2 https://storage.danakcorp.com/images/1766555932114-485954688.svg
41 40 spoon and knife https://storage.danakcorp.com/images/1766555949650-909996041.svg
42 41 fork and knife https://storage.danakcorp.com/images/1766555966861-75517122.svg
43 42 hot dog https://storage.danakcorp.com/images/1766556006875-239689681.svg
44 43 soap https://storage.danakcorp.com/images/1766556257163-459958715.svg
45 44 dish https://storage.danakcorp.com/images/1766556293812-665835248.svg
46 45 donuts https://storage.danakcorp.com/images/1766556307082-162280793.svg
47 46 egg plant https://storage.danakcorp.com/images/1766556340402-744632392.svg
48 47 egg https://storage.danakcorp.com/images/1766556458335-857124841.svg
49 48 fast-food https://storage.danakcorp.com/images/1766556479530-161860621.svg
50 49 taco https://storage.danakcorp.com/images/1766556498215-91175544.svg
51 50 fire https://storage.danakcorp.com/images/1766556677355-108854669.svg
52 51 fish https://storage.danakcorp.com/images/1766556736283-691587656.svg
53 52 fish https://storage.danakcorp.com/images/1766556759367-58066921.svg
54 53 french fries https://storage.danakcorp.com/images/1766556777353-490756125.svg
55 54 fried egg https://storage.danakcorp.com/images/1766556870740-118553581.svg
56 55 refrigerator https://storage.danakcorp.com/images/1766556890334-962678132.svg
57 56 grape https://storage.danakcorp.com/images/1766556921029-865260745.svg
58 57 hat robe https://storage.danakcorp.com/images/1766556946363-320833501.svg
59 58 hot dog https://storage.danakcorp.com/images/1766556973771-832565827.svg
60 59 ice cream https://storage.danakcorp.com/images/1766557056358-727862983.svg
61 60 ice cream https://storage.danakcorp.com/images/1766557199259-72955190.svg
62 61 ice cream 3 https://storage.danakcorp.com/images/1766557252452-988016525.svg
63 62 jar https://storage.danakcorp.com/images/1766557274778-439028456.svg
64 63 jelly https://storage.danakcorp.com/images/1766557289094-415104854.svg
65 64 juice1 https://storage.danakcorp.com/images/1766557300923-302277606.svg
66 65 juice 2 https://storage.danakcorp.com/images/1766557317091-602116315.svg
67 66 kitchen stove https://storage.danakcorp.com/images/1766557647999-47333939.svg
68 67 orange slash https://storage.danakcorp.com/images/1766557669282-172512141.svg
69 68 loaf of bread https://storage.danakcorp.com/images/1766557705283-692546793.svg
70 69 loaf of bread 2 https://storage.danakcorp.com/images/1766557729175-762576743.svg
71 70 meat https://storage.danakcorp.com/images/1766557756487-665300947.svg
72 71 meat 2 https://storage.danakcorp.com/images/1766557776067-804149228.svg
73 72 mixer https://storage.danakcorp.com/images/1766557797509-101726352.svg
74 73 mortar https://storage.danakcorp.com/images/1766557816484-357699340.svg
75 74 noodle https://storage.danakcorp.com/images/1766557837292-326521286.svg
76 75 nuts https://storage.danakcorp.com/images/1766559083575-564721059.svg
77 76 olive https://storage.danakcorp.com/images/1766559099561-720966516.svg
78 77 onion https://storage.danakcorp.com/images/1766559116496-834964990.svg
79 78 pizza https://storage.danakcorp.com/images/1766563021426-600579018.svg
80 79 rolling pin https://storage.danakcorp.com/images/1766563038525-727531190.svg
81 80 solt https://storage.danakcorp.com/images/1766563051485-824602001.svg
82 81 scales https://storage.danakcorp.com/images/1766563066017-372210647.svg
83 82 basket https://storage.danakcorp.com/images/1766563124296-331607800.svg
84 83 skewer https://storage.danakcorp.com/images/1766563142133-359920423.svg
85 84 strawberry https://storage.danakcorp.com/images/1766563164270-350101986.svg
86 85 toast https://storage.danakcorp.com/images/1766563181587-86630830.svg
87 86 watermelon https://storage.danakcorp.com/images/1766563204966-651499380.svg
88 87 whisk https://storage.danakcorp.com/images/1766563220596-103699525.svg
89 88 wine glass https://storage.danakcorp.com/images/1766563265634-373953449.svg
90 89 wine glass 2 https://storage.danakcorp.com/images/1766563275517-958045207.svg
91 90 wine glass 3 https://storage.danakcorp.com/images/1766563286526-622849562.svg
92 91 colored bakeries https://storage.danakcorp.com/images/1766564695339-504553420.svg
93 92 colored bio https://storage.danakcorp.com/images/1766564706032-159367016.svg
94 93 colored bibimbap https://storage.danakcorp.com/images/1766564720683-887818252.svg
95 94 colored boba https://storage.danakcorp.com/images/1766564734432-483528219.svg
96 95 colored breakfast https://storage.danakcorp.com/images/1766564750099-854146988.svg
97 96 colored hamburger https://storage.danakcorp.com/images/1766564758573-696060786.svg
98 97 colored hamburger 2 https://storage.danakcorp.com/images/1766564805823-829938380.svg
99 98 colored coffee https://storage.danakcorp.com/images/1766564891137-468827500.svg
100 99 colored coca https://storage.danakcorp.com/images/1766564907215-921380792.svg
101 100 colored cookies https://storage.danakcorp.com/images/1766564926872-672612697.svg
102 101 colored noodle https://storage.danakcorp.com/images/1766565459604-638026702.svg
103 102 colored crossan https://storage.danakcorp.com/images/1766565476771-42527917.svg
104 103 colored sweet https://storage.danakcorp.com/images/1766565491264-946731491.svg
105 104 colored dumpling https://storage.danakcorp.com/images/1766565502475-288189136.svg
106 105 colored fish and chips https://storage.danakcorp.com/images/1766565530628-800474580.svg
107 106 colored focaccia https://storage.danakcorp.com/images/1766565547983-878658938.svg
108 107 colored french fries https://storage.danakcorp.com/images/1766565563263-891688321.svg
109 108 colored fried chicken https://storage.danakcorp.com/images/1766565600547-704332792.svg
110 109 colored fried rice https://storage.danakcorp.com/images/1766565625437-528383713.svg
111 110 colored fries https://storage.danakcorp.com/images/1766565641620-53176909.svg
112 111 hat robe https://storage.danakcorp.com/images/1766565664427-433669317.svg
113 112 colored hot dog https://storage.danakcorp.com/images/1766565971515-159903323.svg
114 113 colored hot dog https://storage.danakcorp.com/images/1766565995424-288101561.svg
115 114 colored hot pot https://storage.danakcorp.com/images/1766566026942-868807643.svg
116 115 colored ice cream https://storage.danakcorp.com/images/1766566041525-973019175.svg
117 116 colored ice tea https://storage.danakcorp.com/images/1766566064741-271844433.svg
118 117 colored instant noodle cup https://storage.danakcorp.com/images/1766566084368-438871956.svg
119 118 lemon https://storage.danakcorp.com/images/1766566106954-47179607.svg
120 119 colored martini https://storage.danakcorp.com/images/1766566122836-486871124.svg
121 120 colored match https://storage.danakcorp.com/images/1766566143171-668309076.svg
122 121 colored mochi donuts https://storage.danakcorp.com/images/1766566161551-844376214.svg
123 122 colored mochi https://storage.danakcorp.com/images/1766566176755-30806596.svg
124 123 colored nachos https://storage.danakcorp.com/images/1766566193720-339416210.svg
125 124 colored origini https://storage.danakcorp.com/images/1766566293736-981836756.svg
126 125 colored pan cake https://storage.danakcorp.com/images/1766566304318-193301989.svg
127 126 colored pepperoni pizza https://storage.danakcorp.com/images/1766566327768-2675508.svg
128 127 colored pizza 4 https://storage.danakcorp.com/images/1766566341717-462491426.svg
129 128 colored podding https://storage.danakcorp.com/images/1766566357028-432111036.svg
130 129 quesadilla https://storage.danakcorp.com/images/1766566371354-279596850.svg
131 130 salad https://storage.danakcorp.com/images/1766566385674-569493499.svg
132 131 colored sandwich https://storage.danakcorp.com/images/1766566413058-975642584.svg
133 132 colored stak https://storage.danakcorp.com/images/1766566427440-982841441.svg
134 133 colored soshi https://storage.danakcorp.com/images/1766566442808-761588425.svg
135 134 colored taco https://storage.danakcorp.com/images/1766566468108-519151895.svg
136 135 colored tempura https://storage.danakcorp.com/images/1766566482278-661272876.svg
137 136 colored tafu https://storage.danakcorp.com/images/1766566494050-408184442.svg
138 137 whisk https://storage.danakcorp.com/images/1766566504760-709260995.svg
+58
View File
@@ -0,0 +1,58 @@
const fs = require('fs');
const path = require('path');
// Read the CSV file
const csvPath = path.join(__dirname, 'icons.csv');
const csvContent = fs.readFileSync(csvPath, 'utf8');
// Parse CSV lines
const lines = csvContent.split('\n').filter(line => line.trim());
const header = lines[0];
const dataLines = lines.slice(1);
// Icon names - replace these with your actual icon names after reviewing them
const iconNames = [
// Add your icon names here, one for each row
// Example: "hamburger", "pizza", "drink", etc.
// For now, placeholders:
"icon_1", "icon_2", "icon_3", "icon_4", "icon_5", "icon_6", "icon_7", "icon_8", "icon_9", "icon_10",
"icon_11", "icon_12", "icon_13", "icon_14", "icon_15", "icon_16", "icon_17", "icon_18", "icon_19", "icon_20",
"icon_21", "icon_22", "icon_23", "icon_24", "icon_25", "icon_26", "icon_27", "icon_28", "icon_29", "icon_30",
"icon_31", "icon_32", "icon_33", "icon_34", "icon_35", "icon_36", "icon_37", "icon_38", "icon_39", "icon_40",
"icon_41", "icon_42", "icon_43", "icon_44", "icon_45", "icon_46", "icon_47", "icon_48", "icon_49", "icon_50",
"icon_51", "icon_52", "icon_53", "icon_54", "icon_55", "icon_56", "icon_57", "icon_58", "icon_59", "icon_60",
"icon_61", "icon_62", "icon_63", "icon_64", "icon_65", "icon_66", "icon_67", "icon_68", "icon_69", "icon_70",
"icon_71", "icon_72", "icon_73", "icon_74", "icon_75", "icon_76", "icon_77", "icon_78", "icon_79", "icon_80",
"icon_81", "icon_82", "icon_83", "icon_84", "icon_85", "icon_86", "icon_87", "icon_88", "icon_89", "icon_90",
"icon_91", "icon_92", "icon_93", "icon_94", "icon_95", "icon_96", "icon_97", "icon_98", "icon_99", "icon_100",
"icon_101", "icon_102", "icon_103", "icon_104", "icon_105", "icon_106", "icon_107", "icon_108", "icon_109", "icon_110",
"icon_111", "icon_112", "icon_113", "icon_114", "icon_115", "icon_116", "icon_117", "icon_118", "icon_119", "icon_120",
"icon_121", "icon_122", "icon_123", "icon_124", "icon_125", "icon_126", "icon_127", "icon_128", "icon_129", "icon_130",
"icon_131", "icon_132", "icon_133", "icon_134", "icon_135", "icon_136", "icon_137", "icon_138"
];
// Validate that we have the right number of names
if (iconNames.length !== dataLines.length) {
console.error(`Error: You provided ${iconNames.length} icon names but there are ${dataLines.length} data rows in the CSV.`);
process.exit(1);
}
// Create new CSV content with icon names
const newLines = [header];
dataLines.forEach((line, index) => {
// Remove trailing comma if present and add the icon name
const cleanLine = line.replace(/,$/, '');
newLines.push(`${cleanLine},"${iconNames[index]}"`);
});
// Write the updated CSV
const newCsvContent = newLines.join('\n');
fs.writeFileSync(csvPath, newCsvContent, 'utf8');
console.log(`✅ Successfully added ${iconNames.length} icon names to icons.csv`);
console.log('📁 File saved:', csvPath);
// Show summary
const namedCount = iconNames.filter(name => name && !name.startsWith('icon_')).length;
console.log(`📊 ${namedCount} custom names, ${iconNames.length - namedCount} placeholder names`);
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Script to restore icon and icon_groups tables to PostgreSQL database
# Usage: ./restore-icons.sh [dump_file]
set -e
# Load environment variables if .env file exists
if [ -f .env ]; then
set -a
# Source .env file, handling comments and empty lines
while IFS= read -r line || [ -n "$line" ]; do
# Remove leading/trailing whitespace
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Skip empty lines and lines starting with #
[[ -z "$line" ]] && continue
[[ "$line" =~ ^# ]] && continue
# Remove inline comments (everything after # that's not in quotes)
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
# Skip if line is empty after removing comments
[[ -z "$line" ]] && continue
# Export the variable (only if it looks like KEY=VALUE)
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
export "$line" 2>/dev/null || true
fi
done < .env
set +a
fi
# Get database connection details from environment variables
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-dmenu}"
DB_USER="${DB_USER:-postgres}"
# Input file name
if [ -z "$1" ]; then
echo "Error: Please provide a dump file to restore"
echo "Usage: ./restore-icons.sh <dump_file>"
echo "Example: ./restore-icons.sh icons_dump_20240101_120000.sql"
exit 1
fi
DUMP_FILE="$1"
# Check if dump file exists
if [ ! -f "${DUMP_FILE}" ]; then
echo "Error: Dump file '${DUMP_FILE}' not found!"
exit 1
fi
echo "Restoring icon_groups and icons tables..."
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
echo "Dump file: ${DUMP_FILE}"
echo ""
# Ask for confirmation
read -p "This will replace existing data in icon_groups and icons tables. Continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Restore cancelled."
exit 0
fi
# Disable foreign key checks temporarily and truncate tables
echo "Clearing existing data..."
PGPASSWORD="${DB_PASS}" psql \
-h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "${DB_USER}" \
-d "${DB_NAME}" \
-c "TRUNCATE TABLE icons CASCADE;" \
-c "TRUNCATE TABLE icon_groups CASCADE;"
# Restore data from dump file
echo "Restoring data from ${DUMP_FILE}..."
PGPASSWORD="${DB_PASS}" psql \
-h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "${DB_USER}" \
-d "${DB_NAME}" \
-f "${DUMP_FILE}"
echo ""
echo "Restore completed successfully!"
echo ""
# Verify the restore
echo "Verifying restore..."
ICON_GROUPS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icon_groups;")
ICONS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icons;")
echo "icon_groups records: ${ICON_GROUPS_COUNT}"
echo "icons records: ${ICONS_COUNT}"
+36
View File
@@ -0,0 +1,36 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-unused-vars': 'error',
},
},
);
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+17122
View File
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
{
"name": "base-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts",
"db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts ",
"db:drop": "npx mikro-orm schema:drop --run --config ./src/config/mikro-orm.config.dev.ts",
"db:seed": "npx mikro-orm seeder:run --config ./src/config/mikro-orm.config.dev.ts",
"db:reset": "npm run db:drop && npm run db:create && npm run db:seed",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start dist/main",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node --max-old-space-size=2048 dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:create": "npx mikro-orm migration:create --config ./src/config/mikro-orm.config.dev.ts",
"migration:up": "npx mikro-orm migration:up --config ./src/config/mikro-orm.config.dev.ts",
"migration:down": "npx mikro-orm migration:down --config ./src/config/mikro-orm.config.dev.ts",
"migration:list": "npx mikro-orm migration:list --config ./src/config/mikro-orm.config.dev.ts",
"migration:pending": "npx mikro-orm migration:pending --config ./src/config/mikro-orm.config.dev.ts"
},
"dependencies": {
"@apollo/server": "^5.1.0",
"@as-integrations/fastify": "^3.1.0",
"@aws-sdk/client-s3": "^3.922.0",
"@aws-sdk/s3-request-presigner": "^3.922.0",
"@fastify/cookie": "^11.0.2",
"@fastify/multipart": "^9.3.0",
"@fastify/static": "^8.3.0",
"@keyv/redis": "^5.1.3",
"@mikro-orm/core": "^6.5.8",
"@mikro-orm/nestjs": "^6.1.1",
"@mikro-orm/postgresql": "^6.5.8",
"@nest-lab/fastify-multer": "^1.3.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-fastify": "^11.1.8",
"@nestjs/platform-socket.io": "^11.1.9",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^11.2.1",
"@nestjs/throttler": "^6.4.0",
"@types/socket.io": "^3.0.1",
"axios": "^1.13.1",
"bullmq": "^5.65.1",
"cache-manager": "^7.2.4",
"cacheable": "^2.3.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.19",
"fastify": "^5.6.1",
"firebase-admin": "^13.6.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sanitize-html": "^2.17.0",
"slugify": "^1.6.6",
"socket.io": "^4.8.1",
"ulid": "^3.0.1",
"uuid": "^13.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@mikro-orm/cli": "^6.5.8",
"@mikro-orm/migrations": "^6.5.8",
"@mikro-orm/seeder": "^6.5.8",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/sanitize-html": "^2.16.0",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
+14002
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
up
+67
View File
@@ -0,0 +1,67 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import dataBaseConfig from './config/mikro-orm.config';
import { ConfigModule } from '@nestjs/config';
import { UserModule } from './modules/users/user.module';
import { UtilsModule } from './modules/utils/utils.module';
import { AuthModule } from './modules/auth/auth.module';
import { UploaderModule } from './modules/uploader/uploader.module';
import { AdminModule } from './modules/admin/admin.module';
import { ThrottlerModule } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule';
import { RestaurantsModule } from './modules/restaurants/restaurants.module';
import { FoodModule } from './modules/foods/food.module';
import { CartModule } from './modules/cart/cart.module';
import { RolesModule } from './modules/roles/roles.module';
import { PaymentsModule } from './modules/payments/payments.module';
import { DeliveryModule } from './modules/delivery/delivery.module';
import { OrdersModule } from './modules/orders/orders.module';
import { CouponModule } from './modules/coupons/coupon.module';
import { ReviewModule } from './modules/review/review.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { PagerModule } from './modules/pager/pager.module';
import { ContactModule } from './modules/contact/contact.module';
import { InventoryModule } from './modules/inventory/inventory.module';
import { IconsModule } from './modules/icons/icons.module';
import { CacheModule } from '@nestjs/cache-manager';
import { cacheConfig } from './config/cache.config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, cache: true }),
CacheModule.registerAsync(cacheConfig()),
MikroOrmModule.forRootAsync(dataBaseConfig),
UserModule,
UtilsModule,
AuthModule,
UploaderModule,
AdminModule,
ThrottlerModule.forRoot([
{
ttl: 60, // time window in seconds
limit: 5, // max requests per window
},
]),
ScheduleModule.forRoot(),
RestaurantsModule,
FoodModule,
CartModule,
RolesModule,
PaymentsModule,
DeliveryModule,
OrdersModule,
CouponModule,
ReviewModule,
NotificationsModule,
EventEmitterModule.forRoot(),
PagerModule,
ContactModule,
InventoryModule,
IconsModule,
],
controllers: [],
providers: [],
// exports: [CacheService],
})
export class AppModule {}
+20
View File
@@ -0,0 +1,20 @@
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
export const AI_CONFIG = 'AI_CONFIG';
export const MIKRO_ORM_QUERY_LOGGER = 'MIKRO_ORM_QUERY_LOGGER';
export const NAJVA_CONFIG = 'NAJVA_CONFIG';
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
export const AUTH_THROTTLE_LIMIT = 5;
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
//
export const CONSOLE_JWT_STRATEGY_NAME = 'console_jwt_strategy';
export const LOCAL_JWT_STRATEGY_NAME = 'local_jwt_strategy';
export const API_HEADER_SLUG = {
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
};
@@ -0,0 +1,18 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
/**
* Decorator to extract userId from the authenticated request.
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
*
* @example
* @Get('/profile')
* @UseGuards(AdminAuthGuard)
* getProfile(@UserId() userId: string) {
* return this.userService.findById(userId);
* }
*/
export const AdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request & { adminId?: string }>();
return request.adminId || '';
});
+3
View File
@@ -0,0 +1,3 @@
export { UserId } from './user-id.decorator';
export { RestId } from './rest-id.decorator';
export { RateLimit } from './rate-limit.decorator';
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
@@ -0,0 +1,18 @@
import { UseGuards, applyDecorators } from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import {
AUTH_THROTTLE_LIMIT,
AUTH_THROTTLE_TTL,
AUTH__REFRESH_THROTTLE_LIMIT,
AUTH__REFRESH_THROTTLE_TTL,
} from '../constants';
export const RateLimit = (limit: number, ttl: number) =>
applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard));
// Predefined rate limits for common scenarios
export const StrictRateLimit = () => RateLimit(AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL); // 5 requests per minute
export const StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute
export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails per minute
export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute
@@ -0,0 +1,18 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
/**
* Decorator to extract restId from the authenticated request.
* Must be used after AdminAuthGuard or AuthGuard that sets request.restId.
*
* @example
* @Get('/restaurants')
* @UseGuards(AdminAuthGuard)
* getRestaurants(@RestId() restId: string) {
* return this.restaurantService.findById(restId);
* }
*/
export const RestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request & { restId?: string }>();
return request.restId || '';
});
@@ -0,0 +1,7 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
export const RestSlug = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request & { slug?: string }>();
return request.slug || '';
});
@@ -0,0 +1,18 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
/**
* Decorator to extract userId from the authenticated request.
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
*
* @example
* @Get('/profile')
* @UseGuards(AdminAuthGuard)
* getProfile(@UserId() userId: string) {
* return this.userService.findById(userId);
* }
*/
export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request & { userId?: string }>();
return request.userId || '';
});
+25
View File
@@ -0,0 +1,25 @@
import { Index, PrimaryKey, Property, Filter, OptionalProps } from '@mikro-orm/core';
import { ulid } from 'ulid';
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
@Index({ properties: ['deletedAt'] })
@Index({ properties: ['createdAt'] })
export abstract class BaseEntity {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid();
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
createdAt: Date = new Date();
@Property({
onUpdate: () => new Date(),
defaultRaw: 'now()',
columnType: 'timestamptz',
})
updatedAt: Date = new Date();
@Property({ nullable: true })
deletedAt?: Date;
}
+781
View File
@@ -0,0 +1,781 @@
export const enum AuthMessage {
UNAUTHORIZED_ACCESS = 'شما دسترسی لازم برای این عملیات را ندارید.',
PHONE_REGISTERED = 'شماره ثبت شد',
INVALID_PHONE_FORMAT = 'فرمت شماره تلفن صحیح نیست',
OTP_SENT = 'کد یکبار مصرف به شماره شما ارسال شد.',
INVALID_PHONE = 'شماره موبایل اشتباه می باشد',
INVALID_CREDENTIAL = 'شماره موبایل یا رمز عبور اشتباه می باشد',
OTP_FAILED = 'کد یا شماره موبایل اشتباه است',
LOGIN_SUCCESS = 'ورود با موفقیت انجام شد',
PASSWORD_LOGIN_SUCCESS = 'با موفقیت وارد شدید',
FORGOT_PASSWORD_SUCCESS = 'درخواست فراموشی رمز عبور با موفقیت ثبت شد',
PASSWORD_SET_SUCCESS = 'پسورد با موفقیت تنظیم شد',
PASSWORD_UPDATE_SUCCESS = 'رمز عبور شما تغییر کرد',
INVALID_OTP = 'کد صحیح نمی‌باشد یا منقضی شده است',
PASSWORD_MISMATCH = 'رمز عبور یکسان نیست',
INVALID_PASSWORD = 'ایمیل یا رمز عبور اشتباه است',
INVALID_REPEAT_PASSWORD = 'تکرار رمز عبور اشتباه است',
EMAIL_NOT_EMPTY = 'ایمیل نمیتواند خالی باشد.',
INVALID_EMAIL_FORMAT = 'فرمت ایمیل صحیح نیست',
PasswordNotEmpty = 'پسورد نمیتواند خالی باشد.',
USER_NOT_FOUND = 'کاربری با این شماره وجود ندارد',
ADMIN_NOT_FOUND = 'ادمینی با این شماره وجود ندارد',
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
INVALID_USER = 'کاربری با این مشخصات یافت نشد',
PHONE_NOT_FOUND = 'کاربری با این شماره یافت نشد',
TOKEN_EXPIRED = 'توکن منقضی شده است',
TOKEN_INVALID = 'توکن نامعتبر است',
BANNED = 'در حال حاضر شما امکان دسترسی به این سرویس را ندارید',
PHONE_EXISTS = 'شماره تلفن قبلا ثبت شده است',
ADMIN_CREATED = 'ادمین با موفقیت ایجاد شد',
PERM_NOT_FOUND = 'دسترسی یافت نشد',
INVALID_PASS_FORMAT = 'فرمت رمز عبور صحیح نیست',
PHONE_NOT_EMPTY = 'شماره تلفن نمیتواند خالی باشد',
PASSWORD_FORMAT_INVALID = 'رمز عبور باید به صورت رشته باشد',
OTP_FORMAT_INVALID = 'کد یکبار مصرف باید عددی و ۵ رقمی باشد',
PHONE_FORMAT_INVALID = 'شماره تلفن باید معتبر و به فرمت ایران باشد',
TOKEN_NOT_EMPTY = 'توکن نمی‌تواند خالی باشد',
PASSWORD_NOT_EMPTY = 'رمز عبور نمی‌تواند خالی باشد',
PASSWORD_CHANGED_SUCCESSFULLY = 'رمز عبور با موفقیت تغییر کرد',
PASSWORD_CONFIRMATION_NOT_EMPTY = 'تایید رمز عبور نمی‌تواند خالی باشد',
PASSWORD_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
OTP_NOT_EMPTY = 'کد یکبار مصرف نمی‌تواند خالی باشد',
LAST_PASSWORD_NOT_EMPTY = 'رمز عبور قبلی نمی‌تواند خالی باشد',
FIRST_NAME_NOT_EMPTY = 'نام نمی‌تواند خالی باشد',
LAST_NAME_NOT_EMPTY = 'نام خانوادگی نمی‌تواند خالی باشد',
FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام باید بین ۲ تا ۵۰ کاراکتر باشد',
LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام خانوادگی باید بین ۲ تا ۵۰ کاراکتر باشد',
BIRTH_DATE_NOT_EMPTY = 'تاریخ تولد نمی‌تواند خالی باشد',
NATIONAL_CODE_INCORRECT = 'کد ملی باید ۱۰ رقمی باشد',
NATIONAL_NOT_EMPTY = 'کد ملی نمی‌تواند خالی باشد',
OTP_ALREADY_SENT = 'کد یکبار مصرف قبلا ارسال شده است',
TOO_MANY_REQUESTS = 'تعداد درخواست های شما بیش از حد مجاز است',
NOT_ADMIN = 'شما دسترسی به بخش ادمین ندارید',
PHONE_SHOULD_BE_11_DIGIT = 'شماره تلفن باید ۱۱ رقم باشد',
TIMESTAMP_NOT_EMPTY = 'زمان نمی‌تواند خالی باشد',
ADMIN_CAN_NOT_LOGIN = 'ادمین نمی‌تواند وارد شود',
NATIONAL_CODE_INVALID = 'کد ملی نامعتبر است',
INVALID_REFRESH_TOKEN = 'توکن رفرش نامعتبر است',
REFRESH_TOKEN_EXPIRED = 'توکن رفرش منقضی شده است',
LOGOUT_SUCCESS = 'خروج با موفقیت انجام شد',
CURRENT_PASSWORD_INVALID = 'رمز عبور فعلی نامعتبر است',
TOKEN_EXPIRED_OR_INVALID = 'توکن منقضی شده یا نامعتبر است',
ACCESS_DENIED = 'دسترسی شما محدود شده است',
USER_ACCOUNT_INACTIVE = 'حساب کاربری شما غیر فعال است',
TOKEN_MISSED_OR_EXPIRED = 'توکن مفقود یا منقضی شده است',
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
SLUG_REQUIRED = 'اسلاگ الزامی است',
INVALID_TOKEN_PAYLOAD = 'محتویات توکن نامعتبر است',
INVALID_SLUG = 'اسلاگ نامعتبر است',
INVALID_OR_EXPIRED_TOKEN = 'توکن نامعتبر یا منقضی شده است',
}
export const enum FoodMessage {
NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
}
export const enum CategoryMessage {
NOT_FOUND = 'دسته‌بندی مورد نظر یافت نشد',
NOT_CREATED = 'ایجاد دسته‌بندی با خطا مواجه شد',
NOT_UPDATED = 'به‌روزرسانی دسته‌بندی با خطا مواجه شد',
NOT_DELETED = 'حذف دسته‌بندی با خطا مواجه شد',
}
export const enum UserMessage {
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
EMAIL_ADDRESS_ALREADY_EXISTS = 'ایمیل قبلا ثبت شده است',
EMAIL_USER_NOT_FOUND = 'ایمیل یافت نشد',
EMAIL_USER_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
FAILED_TO_DELETE_EMAIL_USER = 'خطا در حذف ایمیل',
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = 'ایمیل با موفقیت به روز رسانی شد',
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = 'خطا در به روز رسانی حجم ایمیل',
USERNAME_REQUIRED = 'نام کاربری الزامی است',
USERNAME_STRING = 'نام کاربری باید یک رشته باشد',
USERNAME_MIN_LENGTH = 'نام کاربری باید حداقل ۲ کاراکتر باشد',
USERNAME_MATCHES = 'نام کاربری فقط میتواند شامل حروف، اعداد، نقطه، خط زیر و خط فاصله باشد',
PASSWORD_MATCHES = 'رمز عبور باید حداقل ۸ کاراکتر باشد و شامل حروف بزرگ، حروف کوچک، اعداد و علائم خاص باشد',
PASSWORD_STRING = 'رمز عبور باید یک رشته باشد',
PASSWORD_REQUIRED = 'رمز عبور الزامی است',
PASSWORD_MIN_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
DOMAIN_ID_STRING = 'شناسه دامنه باید یک رشته باشد',
DOMAIN_ID_REQUIRED = 'شناسه دامنه الزامی است',
DOMAIN_ID_UUID = 'شناسه دامنه باید یک یو یو آی دی باشد',
DISPLAY_NAME_STRING = 'نام نمایشی باید یک رشته باشد',
DISPLAY_NAME_OPTIONAL = 'نام نمایشی اختیاری است',
QUOTA_NUMBER = 'حجم ایمیل باید یک عدد باشد',
QUOTA_OPTIONAL = 'حجم ایمیل اختیاری است',
QUOTA_NOT_EMPTY = 'حجم ایمیل نمیتواند خالی باشد',
ALIASES_ARRAY = 'الیاس ها باید یک آرایه باشد',
ALIASES_STRING = 'الیاس ها باید یک رشته باشد',
ALIASES_OPTIONAL = 'الیاس ها اختیاری است',
TITLE_STRING = 'عنوان باید یک رشته باشد',
TITLE_OPTIONAL = 'عنوان اختیاری است',
TITLE_NOT_EMPTY = 'عنوان نمیتواند خالی باشد',
EMAIL_USER_CREATED_SUCCESSFULLY = 'ایمیل با موفقیت ایجاد شد',
EMAIL_USER_UPDATED_SUCCESSFULLY = 'ایمیل کاربر با موفقیت به‌روزرسانی شد',
STATUS_UPDATED = 'وضعیت کاربر با موفقیت تغییر کرد',
TEMPLATE_UUID = 'شناسه قالب باید یک یو یو آی دی باشد',
TEMPLATE_NOT_EMPTY = 'شناسه قالب نمیتواند خالی باشد',
ALIASES_EMAIL = 'آدرس ایمیل الیاس باید معتبر باشد',
FORWARDERS_EMAIL_VALID = 'آدرس ایمیل فوروارد باید معتبر باشد',
FORWARDERS_ARRAY = 'آدرس های فوروارد باید یک آرایه باشد',
FORWARDERS_STRING = 'آدرس های فوروارد باید یک رشته باشد',
PROFILE_PICTURE_URL = 'آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد',
USER_UPDATED_SUCCESSFULLY = 'کاربر با موفقیت به روز رسانی شد',
PROFILE_PICTURE_NOT_EMPTY = 'تصویر پروفایل نمیتواند خالی باشد',
PUSH_TOKEN_NOT_FOUND = 'توکن پوش نامعتبر است',
PUSH_TOKEN_ADDED_SUCCESSFULLY = 'توکن پوش با موفقیت اضافه شد',
NO_PUSH_TOKENS_FOUND = 'توکن پوشی برای این کاربر یافت نشد',
PUSH_TOKEN_REMOVED_SUCCESSFULLY = 'توکن پوش با موفقیت حذف شد',
}
export const enum CommonMessage {
VALIDITY_TYPE_REQUIRED = 'نوع اعتبار سنجی مورد نیاز است',
THIS_FILED_IS_REQUIRED = 'این فیلد الزامی است',
VALID_FOR_CHOOSE = 'معتبر برای انتخاب',
UPDATE_SUCCESS = 'با موفقیت به روز رسانی شد',
CREATED = 'با موفقیت ایجاد شد',
DELETED = 'با موفقیت حذف شد',
ID_REQUIRED = 'شناسه مورد نیاز است',
ID_SHOULD_BE_UUID = 'شناسه باید یک یو یو آی دی باشد',
PaymentTypeQueryNotEmpty = 'نوع پرداخت نمی‌تواند خالی باشد',
SEARCH_QUERY_STRING = 'رشته جستجو باید یک رشته باشد',
UPDATED = 'با موفقیت آپدیت شد',
IS_ACTIVE_SHOULD_BE_1_0 = 'وضعیت باید یکی از مقادیر ۰ و ۱ باشد',
DATE_MUST_BE_DATE = 'تاریخ باید یک تاریخ معتبر به صورت رشته باشد',
}
export const enum UploaderMessage {
UPLOAD_FILE_INVALID = 'فایل معتبر نیست',
UPLOAD_FILE_TOO_LARGE = 'فایل بزرگتر از حد مجاز است . حداکثر حجم فایل [MAX_FILE_SIZE] مگابایت است',
UPLOAD_FILE_TYPE_NOT_SUPPORTED = 'نوع فایل معتبر نیست . مجاز است : [MIME_TYPES]',
UPLOAD_FILE_SUCCESS = 'فایل با موفقیت آپلود شد',
}
export const enum EmailMessage {
EMAIL_VERIFICATION = 'تایید ایمیل',
EMAIL_SENDING_FAILED = 'ارسال ایمیل با خطا مواجه شد',
LOGIN = 'ورود به سیستم',
INVOICE = 'فاکتور',
WALLET = 'اعلان کیف پول',
TICKET = 'تیکت پشتیبانی',
ANNOUNCEMENT = 'اعلان',
PAYMENT_REMINDER = 'یادآوری پرداخت',
PAYMENT_CANCELLATION = 'لغو پرداخت',
EMAIL_SENDING_SUCCESS = 'ارسال ایمیل با موفقیت انجام شد',
EMAIL_FORWARD_SUCCESS = 'فوروارد ایمیل با موفقیت انجام شد',
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = 'وضعیت ایمیل با موفقیت دریافت شد',
MESSAGES_SEARCHED_SUCCESSFULLY = 'جستجوی ایمیل با موفقیت انجام شد',
MESSAGES_LISTED_SUCCESSFULLY = 'لیست ایمیل ها با موفقیت دریافت شد',
MESSAGE_RETRIEVED_SUCCESSFULLY = 'ایمیل با موفقیت دریافت شد',
MESSAGE_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
USER_ID_REQUIRED = 'شناسه کاربری مورد نیاز است',
USER_ID_MUST_BE_STRING = 'شناسه کاربری باید یک رشته باشد',
MAILBOX_ID_REQUIRED = 'شناسه میل باکس ایمیل مورد نیاز است',
MAILBOX_ID_MUST_BE_STRING = 'شناسه میل باکس ایمیل باید یک رشته باشد',
QUEUE_ID_REQUIRED = 'شناسه پیش‌نویس ایمیل مورد نیاز است',
QUEUE_ID_MUST_BE_STRING = 'شناسه پیش‌نویس ایمیل باید یک رشته باشد',
MESSAGE_ID_MUST_BE_NUMBER = 'شناسه ایمیل باید یک عدد باشد',
FROM_ADDRESS_REQUIRED = 'آدرس از مورد نیاز است',
// Validation Messages for Email DTOs
FROM_ADDRESS_MUST_BE_EMAIL = 'آدرس فرستنده باید یک ایمیل معتبر باشد',
// Query validation messages
SEARCH_QUERY_PARAMETER_MUST_BE_STRING = 'پارامتر جستجو باید یک رشته باشد',
MESSAGE_IDS_MUST_BE_STRING = 'شناسه‌های پیام باید یک رشته باشد',
THREAD_ID_MUST_BE_STRING = 'شناسه رشته گفتگو باید یک رشته باشد',
OR_QUERY_MUST_BE_OBJECT = 'پارامتر OR باید یک شیء باشد',
DATE_START_MUST_BE_DATE_STRING = 'تاریخ شروع باید یک رشته تاریخ معتبر باشد',
DATE_END_MUST_BE_DATE_STRING = 'تاریخ پایان باید یک رشته تاریخ معتبر باشد',
TO_ADDRESS_MUST_BE_STRING = 'آدرس گیرنده باید یک رشته باشد',
MIN_SIZE_MUST_BE_NUMBER = 'حداقل اندازه باید یک عدد باشد',
MIN_SIZE_MUST_BE_NON_NEGATIVE = 'حداقل اندازه نمی‌تواند منفی باشد',
MAX_SIZE_MUST_BE_NUMBER = 'حداکثر اندازه باید یک عدد باشد',
MAX_SIZE_MUST_BE_NON_NEGATIVE = 'حداکثر اندازه نمی‌تواند منفی باشد',
ATTACHMENTS_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیوست‌ها باید بولین باشد',
FLAGGED_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیام‌های علامت‌دار باید بولین باشد',
UNSEEN_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیام‌های خوانده‌نشده باید بولین باشد',
INCLUDE_HEADERS_MUST_BE_STRING = 'لیست هدرها باید یک رشته باشد',
SEARCHABLE_FILTER_MUST_BE_BOOLEAN = 'فیلتر قابل جستجو باید بولین باشد',
THREAD_COUNTERS_MUST_BE_BOOLEAN = 'شمارنده رشته گفتگو باید بولین باشد',
ORDER_MUST_BE_VALID = 'ترتیب باید یکی از مقادیر asc یا desc باشد',
NEXT_CURSOR_MUST_BE_STRING = 'نشانگر صفحه بعد باید یک رشته باشد',
PREVIOUS_CURSOR_MUST_BE_STRING = 'نشانگر صفحه قبل باید یک رشته باشد',
RECIPIENT_NAME_MUST_BE_STRING = 'نام گیرنده باید یک رشته باشد',
RECIPIENT_ADDRESS_REQUIRED = 'آدرس گیرنده مورد نیاز است',
RECIPIENT_ADDRESS_MUST_BE_EMAIL = 'آدرس گیرنده باید یک ایمیل معتبر باشد',
HEADER_KEY_REQUIRED = 'کلید هدر مورد نیاز است',
HEADER_KEY_MUST_BE_STRING = 'کلید هدر باید یک رشته باشد',
HEADER_VALUE_REQUIRED = 'مقدار هدر مورد نیاز است',
HEADER_VALUE_MUST_BE_STRING = 'مقدار هدر باید یک رشته باشد',
ATTACHMENT_FILENAME_MUST_BE_STRING = 'نام فایل پیوست باید یک رشته باشد',
ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING = 'نوع محتوای پیوست باید یک رشته باشد',
ATTACHMENT_ENCODING_MUST_BE_STRING = 'کدگذاری پیوست باید یک رشته باشد',
ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING = 'کدگذاری انتقال محتوای پیوست باید یک رشته باشد',
ATTACHMENT_CONTENT_DISPOSITION_INVALID = 'نحوه نمایش پیوست باید inline یا attachment باشد',
ATTACHMENT_CONTENT_REQUIRED = 'محتوای پیوست مورد نیاز است',
ATTACHMENT_CONTENT_MUST_BE_STRING = 'محتوای پیوست باید یک رشته باشد',
ATTACHMENT_CID_MUST_BE_STRING = 'شناسه محتوای پیوست باید یک رشته باشد',
DRAFT_MAILBOX_REQUIRED = 'شناسه صندوق پیش‌نویس مورد نیاز است',
DRAFT_MAILBOX_MUST_BE_STRING = 'شناسه صندوق پیش‌نویس باید یک رشته باشد',
DRAFT_ID_REQUIRED = 'شناسه پیش‌نویس مورد نیاز است',
DRAFT_ID_MUST_BE_NUMBER = 'شناسه پیش‌نویس باید یک عدد باشد',
ENVELOPE_FROM_REQUIRED = 'آدرس فرستنده در envelope مورد نیاز است',
ENVELOPE_TO_REQUIRED = 'آدرس گیرنده در envelope مورد نیاز است',
ENVELOPE_TO_MUST_BE_ARRAY = 'آدرس گیرنده در envelope باید یک آرایه باشد',
MAILBOX_MUST_BE_STRING = 'شناسه صندوق باید یک رشته باشد',
REPLY_TO_MUST_BE_VALID = 'آدرس پاسخ باید معتبر باشد',
TO_ADDRESSES_REQUIRED = 'آدرس‌های گیرنده مورد نیاز است',
TO_ADDRESSES_MUST_BE_ARRAY = 'آدرس‌های گیرنده باید یک آرایه باشد',
CC_ADDRESSES_MUST_BE_ARRAY = 'آدرس‌های کپی کربن باید یک آرایه باشد',
BCC_ADDRESSES_MUST_BE_ARRAY = 'آدرس‌های کپی مخفی باید یک آرایه باشد',
HEADERS_MUST_BE_ARRAY = 'هدرهای سفارشی باید یک آرایه باشد',
SUBJECT_MUST_BE_STRING = 'موضوع ایمیل باید یک رشته باشد',
TEXT_CONTENT_MUST_BE_STRING = 'محتوای متنی باید یک رشته باشد',
HTML_CONTENT_MUST_BE_STRING = 'محتوای HTML باید یک رشته باشد',
ATTACHMENTS_MUST_BE_ARRAY = 'پیوست‌ها باید یک آرایه باشد',
SESSION_ID_MUST_BE_STRING = 'شناسه جلسه باید یک رشته باشد',
IP_ADDRESS_MUST_BE_STRING = 'آدرس IP باید یک رشته باشد',
IS_DRAFT_MUST_BE_BOOLEAN = 'وضعیت پیش‌نویس باید بولین باشد',
SEND_TIME_MUST_BE_DATE = 'زمان ارسال باید یک تاریخ معتبر باشد',
UPLOAD_ONLY_MUST_BE_BOOLEAN = 'حالت فقط آپلود باید بولین باشد',
ENVELOPE_MUST_BE_VALID = 'envelope باید معتبر باشد',
// Message operations
MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده شده علامت گذاری شد',
ALL_MESSAGES_MARKED_AS_READ_SUCCESSFULLY = 'تمام پیام ها با موفقیت به عنوان خوانده شده علامت گذاری شدند',
MESSAGE_MARK_AS_SEEN_FAILED = 'علامت گذاری پیام با خطا مواجه شد',
SENT_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های ارسالی با موفقیت دریافت شد',
SENT_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های ارسالی با خطا مواجه شد',
TRASH_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های حذف شده با موفقیت دریافت شد',
TRASH_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های حذف شده با خطا مواجه شد',
DRAFT_UPDATED_SUCCESSFULLY = 'پیش نویس با موفقیت به روز رسانی شد',
DRAFT_SENT_SUCCESSFULLY = 'پیش نویس با موفقیت ارسال شد',
DRAFT_DELETED_SUCCESSFULLY = 'پیش نویس با موفقیت حذف شد',
MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت به آرشیو منتقل شد',
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به علاقه مندی ها منتقل شد',
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = 'پیام با موفقیت به سطل زباله منتقل شد',
MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = 'پیام با موفقیت به جانک منتقل شد',
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد',
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد',
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = 'پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد',
MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY = 'پیام با موفقیت از جانک به صندوق ورودی منتقل شد',
MESSAGE_NOT_FOUND = 'پیام یافت نشد',
// Bulk action messages
BULK_ACTION_COMPLETED_SUCCESSFULLY = '[count] پیام با موفقیت [action] شد',
BULK_ACTION_PARTIALLY_COMPLETED = '[successful] از [total] پیام [action] شد. [failed] پیام با خطا مواجه شد',
BULK_ACTION_FAILED = 'هیچ پیامی [action] نشد. تمام [total] تلاش ناموفق بود',
// Bulk action display names
BULK_ACTION_SEEN = 'خوانده شده علامت‌گذاری',
BULK_ACTION_DELETE = 'حذف',
BULK_ACTION_ARCHIVE = 'آرشیو',
BULK_ACTION_UNARCHIVE = 'از آرشیو خارج',
BULK_ACTION_FAVORITE = 'به علاقه مندی ها منتقل',
BULK_ACTION_UNFAVORITE = 'از علاقه مندی ها حذف',
BULK_ACTION_JUNK = 'به جانک منتقل',
BULK_ACTION_NOTJUNK = 'از جانک از حذف',
BULK_ACTION_TRASH = 'به سطل زباله منتقل',
BULK_ACTION_RESTORE = 'از سطل زباله بازیابی',
BULK_ACTION_PROCESSED = 'پردازش',
USER_NOT_FOUND = 'کاربر یافت نشد',
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = 'اولویت باید یکی از مقادیر high, normal, low باشد',
SUBJECT_REQUIRED = 'موضوع ایمیل مورد نیاز است',
ALL_MESSAGES_DELETED_SUCCESSFULLY = 'همه پیام ها با موفقیت حذف شدند',
MAILBOX_ID_MUST_BE_MONGO_ID = 'شناسه میل باکس ایمیل باید یک آی دی معتبر باشد',
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد',
MESSAGE_MARK_AS_UNSEEN_FAILED = 'علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد',
ATTACHMENT_ID_MUST_BE_STRING = 'شناسه پیوست باید یک رشته باشد',
IS_FORWARD_MUST_BE_BOOLEAN = 'IS_FORWARD_MUST_BE_BOOLEAN',
// Favorite/Star messages (Gmail-like behavior)
MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به عنوان مورد علاقه علامت گذاری شد',
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از مورد علاقه حذف شد',
TRASH_EMPTYED_SUCCESSFULLY = 'سطل زباله با موفقیت خالی شد',
SPAM_EMPTYED_SUCCESSFULLY = 'همه پیام های اسپم با موفقیت حذف شدند',
SAVE_DRAFT_SUCCESS = 'پیش نویس با موفقیت ذخیره شد',
HTML_CONTENT_REQUIRED = 'محتوای HTML مورد نیاز است',
TEXT_CONTENT_REQUIRED = 'TEXT_CONTENT_REQUIRED',
}
export const enum WebSocketMessage {
// Connection Messages
WS_CONNECTION_ESTABLISHED = 'اتصال وب سوکت با موفقیت برقرار شد',
WS_CONNECTION_FAILED = 'اتصال وب سوکت ناموفق بود',
WS_AUTHENTICATION_REQUIRED = 'احراز هویت وب سوکت مورد نیاز است',
WS_AUTHENTICATION_FAILED = 'احراز هویت وب سوکت ناموفق بود',
WS_AUTHENTICATION_SUCCESS = 'احراز هویت وب سوکت با موفقیت انجام شد',
WS_INVALID_TOKEN = 'توکن وب سوکت نامعتبر است',
WS_TOKEN_EXPIRED = 'توکن وب سوکت منقضی شده است',
// Room Management Messages
JOINED_ROOM_SUCCESS = 'با موفقیت به اتاق متصل شدید',
LEFT_ROOM_SUCCESS = 'با موفقیت از اتاق خارج شدید',
ROOM_NOT_FOUND = 'اتاق یافت نشد',
UNAUTHORIZED_ROOM_ACCESS = 'دسترسی غیرمجاز به اتاق',
// Session Management Messages
WS_SESSION_CREATED = 'جلسه وب سوکت ایجاد شد',
WS_SESSION_JOINED = 'به جلسه وب سوکت پیوستید',
WS_SESSION_LEFT = 'از جلسه وب سوکت خارج شدید',
WS_SESSION_EXPIRED = 'جلسه وب سوکت منقضی شده است',
WS_SESSION_NOT_FOUND = 'جلسه وب سوکت یافت نشد',
// Error Messages
WS_CONNECTION_ERROR = 'خطا در اتصال وب سوکت',
INVALID_EVENT = 'رویداد نامعتبر',
INVALID_DATA = 'داده نامعتبر',
WS_RATE_LIMIT_EXCEEDED = 'تعداد درخواست‌های وب سوکت از حد مجاز بیشتر است',
INTERNAL_ERROR = 'خطای داخلی سرور',
// Notification Messages
NEW_MESSAGE_NOTIFICATION = 'پیام جدید دریافت شد',
MESSAGE_STATUS_UPDATED = 'وضعیت پیام به‌روزرسانی شد',
MESSAGE_MOVED_NOTIFICATION = 'پیام جابجا شد',
MESSAGE_DELETED_NOTIFICATION = 'پیام حذف شد',
// General Messages
OPERATION_SUCCESS = 'عملیات با موفقیت انجام شد',
OPERATION_FAILED = 'عملیات ناموفق بود',
PERMISSION_DENIED = 'مجوز دسترسی ندارید',
RESOURCE_NOT_FOUND = 'منبع یافت نشد',
// Authentication errors
AUTHENTICATION_REQUIRED = 'احراز هویت ضروری است',
INVALID_TOKEN = 'توکن احراز هویت نامعتبر است',
TOKEN_EXPIRED = 'توکن منقضی شده است',
UNAUTHORIZED_ACCESS = 'دسترسی غیرمجاز',
USER_NOT_AUTHENTICATED = 'کاربر احراز هویت نشده است',
// Connection errors
CONNECTION_FAILED = 'اتصال برقرار نشد',
WEBSOCKET_ERROR = 'خطا در ارتباط WebSocket',
CONNECTION_TIMEOUT = 'زمان اتصال به پایان رسید',
CONNECTION_REFUSED = 'اتصال رد شد',
// Session errors
SESSION_NOT_FOUND = 'جلسه چت یافت نشد',
SESSION_CREATION_FAILED = 'ایجاد جلسه چت با شکست مواجه شد',
SESSION_ACCESS_DENIED = 'دسترسی به جلسه چت مجاز نیست',
SESSION_EXPIRED = 'جلسه چت منقضی شده است',
SESSION_ALREADY_EXISTS = 'جلسه چت قبلاً وجود دارد',
// Message errors
MESSAGE_TOO_LONG = 'پیام از حداکثر طول مجاز تجاوز کرده است',
MESSAGE_EMPTY = 'پیام نمی‌تواند خالی باشد',
MESSAGE_SEND_FAILED = 'ارسال پیام با شکست مواجه شد',
INVALID_MESSAGE_FORMAT = 'فرمت پیام نامعتبر است',
// Rate limiting
RATE_LIMIT_EXCEEDED = 'تعداد پیام‌های ارسالی بیش از حد مجاز است',
TOO_MANY_CONNECTIONS = 'تعداد اتصالات بیش از حد مجاز است',
// Service errors
LLM_SERVICE_ERROR = 'سرویس هوش مصنوعی موقتاً در دسترس نیست',
SERVICE_UNAVAILABLE = 'سرویس موقتاً در دسترس نیست',
INTERNAL_SERVER_ERROR = '.خطای داخلی سرور',
// Validation errors
INVALID_SESSION_ID = 'شناسه جلسه نامعتبر است',
INVALID_USER_ID = 'شناسه کاربر نامعتبر است',
INVALID_MESSAGE_ID = 'شناسه پیام نامعتبر است',
REQUIRED_FIELD_MISSING = 'فیلد ضروری موجود نیست',
// Success messages
AUTHENTICATED = 'احراز هویت موفقیت‌آمیز',
SESSION_CREATED = 'جلسه چت با موفقیت ایجاد شد',
CHAT_JOINED = 'با موفقیت به چت متصل شدید',
MESSAGE_SENT = 'پیام ارسال شد',
CONNECTION_ESTABLISHED = 'اتصال برقرار شد',
}
export const enum SmsMessage {
SEND_SMS_ERROR = 'خطا در ارسال پیامک',
}
export const enum RestMessage {
NOT_FOUND = 'کسب و کار یافت نشد',
BUSINESS_ID_REQUIRED = 'شناسه کسب و کار مورد نیاز است',
SLUG_REQUIRED = 'اسلاگ مورد نیاز است',
SLUG_MUST_BE_STRING = 'اسلاگ باید یک رشته باشد',
DOMAIN_INVALID = 'دامنه وارد شده معتبر نیست',
DOMAIN_UPDATED = 'دامنه با موفقیت به‌روزرسانی شد',
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
DOMAIN_VERIFICATION_METHOD_INVALID = 'روش تایید دامنه نامعتبر است',
DOMAIN_ALREADY_VERIFIED = 'این دامنه قبلا تایید شده است',
DOMAIN_REQUIRED = 'دامنه مورد نیاز است',
DOMAIN_MUST_BE_STRING = 'دامنه باید یک رشته باشد',
STAFF_NOT_FOUND = 'کاربر ادمین یافت نشد',
ZARINPAL_MERCHANT_ID_REQUIRED = 'شناسه مرچنت زرین پال مورد نیاز است',
ZARINPAL_MERCHANT_ID_STRING = 'شناسه مرچنت زرین پال باید یک رشته باشد',
LOGO_URL_REQUIRED = 'آدرس تصویر لوگو مورد نیاز است',
LOGO_URL_STRING = 'آدرس تصویر لوگو باید یک رشته باشد',
LOGO_URL_INVALID = 'آدرس تصویر لوگو معتبر نیست',
BUSINESS_SETTINGS_UPDATED = 'تنظیمات کسب و کار با موفقیت به روز رسانی شد',
NAME_REQUIRED = 'نام کسب و کار مورد نیاز است',
NAME_STRING = 'نام کسب و کار باید یک رشته باشد',
DOMAIN_CONNECTED = 'دامنه با موفقیت متصل شد',
DOMAIN_NOT_CONNECTED = 'دامنه به سرور ما متصل نشد',
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
DOMAIN_VERIFICATION_TOKEN_REQUIRED = 'توکن تایید دامنه مورد نیاز است',
INSUFFICIENT_QUOTA = 'حجم شما برای ساخت ایمیل کافی نمی باشد، لطفا حجم را افزایش دهید',
QUOTA_DOWNGRADE_NOT_ALLOWED = 'کاهش حجم ایمیل مجاز نیست، فقط می‌توانید حجم را افزایش دهید',
QUOTA_AMOUNT_REQUIRED = 'مقدار حجم مورد نیاز است',
QUOTA_AMOUNT_MUST_BE_NUMBER = 'مقدار حجم باید یک عدد باشد',
QUOTA_AMOUNT_MUST_BE_POSITIVE = 'مقدار حجم باید مثبت باشد',
CALLBACK_URL_MUST_BE_STRING = 'آدرس بازگشت باید یک رشته باشد',
CALLBACK_URL_INVALID = 'آدرس بازگشت معتبر نیست',
PURCHASE_DESCRIPTION_MUST_BE_STRING = 'توضیحات خرید باید یک رشته باشد',
QUOTA_PURCHASE_SUCCESS = 'درخواست خرید حجم با موفقیت ثبت شد',
QUOTA_PURCHASE_FAILED = 'خرید حجم با خطا مواجه شد',
PAYMENT_GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
QUOTA_UPDATED = 'حجم ایمیل با موفقیت به روز شد',
USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = 'کاربر دسترسی به این سرویس را ندارد',
}
export const enum DomainMessage {
// Domain Ownership & Verification
NOT_BELONG_TO_BUSINESS = 'دامنه به این کسب و کار تعلق ندارد',
NOT_VERIFIED = 'دامنه باید تایید شده باشد',
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
DOMAIN_ALREADY_EXISTS = 'دامنه [name] قبلا ثبت شده است',
DOMAIN_DELETED_SUCCESSFULLY = 'دامنه با موفقیت حذف شد',
VERIFICATION_NOT_FOUND = 'تایید دامنه یافت نشد',
NO_DOMAIN_FOUND = 'دامنه ای یافت نشد',
DOMAIN_CHECKED = 'دامنه با موفقیت بررسی شد',
// Domain Creation & Setup
DOMAIN_CREATED_WITH_RECORDS = 'دامنه [name] با موفقیت ایجاد شد و تمام رکوردهای مورد نیاز تولید گردید',
ALL_REQUIRED_RECORDS_GENERATED = 'تمام رکوردهای مورد نیاز (MX, SPF, DMARC, DKIM) به طور خودکار برای شما تولید شده‌اند',
ESTIMATED_PROPAGATION_TIME = 'تغییرات DNS معمولاً ۱۵ دقیقه تا ۴۸ ساعت در سراسر جهان منتشر می‌شود',
// DNS Records Instructions
ADD_MX_RECORD = 'یک رکورد MX با نام [name] که به [value] با اولویت [priority] اشاره کند اضافه کنید',
ADD_TXT_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید',
ADD_DKIM_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (کلید عمومی DKIM)',
ADD_DMARC_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (سیاست DMARC)',
ADD_CNAME_RECORD = 'یک رکورد CNAME با نام [name] که به [value] اشاره کند اضافه کنید',
ADD_GENERIC_RECORD = 'یک رکورد [type] با نام [name] و مقدار [value] اضافه کنید',
// Domain Setup Steps
STEP_LOGIN_DNS_PANEL = 'وارد پنل مدیریت DNS ثبت کننده دامنه خود شوید',
STEP_ADD_ALL_RECORDS = 'تمام رکوردهای DNS مورد نیاز لیست شده در بالا را به زون DNS دامنه خود اضافه کنید',
STEP_INCLUDE_ALL_TYPES = 'مطمئن شوید که رکوردهای MX، SPF، DMARC و DKIM را برای عملکرد کامل ایمیل شامل کرده‌اید',
STEP_WAIT_PROPAGATION = 'منتظر انتشار DNS بمانید (معمولاً ۱۵ دقیقه تا ۴۸ ساعت)',
STEP_USE_CHECK_ENDPOINT = "از نقطه پایانی 'بررسی DNS' برای تایید تنظیم صحیح رکوردهای خود استفاده کنید",
STEP_DOMAIN_READY = 'پس از تایید، دامنه شما برای سرویس‌های ایمیل آماده خواهد بود!',
// Status Messages
RECORDS_PENDING_VERIFICATION = '[count] رکورد DNS هنوز در انتظار تایید هستند',
ALL_REQUIRED_RECORDS_MUST_BE_CONFIGURED = 'تمام [count] رکورد مورد نیاز باید برای کار درست ایمیل تنظیم شوند',
DOMAIN_FULLY_VERIFIED = 'دامنه شما به طور کامل تایید شده و برای ایمیل آماده است!',
EMAIL_ACCOUNTS_READY = 'اکنون می‌توانید حساب‌های ایمیل ایجاد کرده و شروع به ارسال/دریافت ایمیل کنید',
// Verification Status
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
DOMAIN_VERIFICATION_PENDING = 'تایید دامنه در انتظار است',
// DNS Record Status
DNS_RECORD_VERIFIED = 'رکورد DNS تایید شد',
DNS_RECORD_FAILED = 'رکورد DNS تایید نشد',
DNS_RECORD_NOT_FOUND_OR_MISMATCH = 'رکورد DNS یافت نشد یا مقدار مطابقت ندارد',
// DKIM Messages
DKIM_ENABLED_AUTOMATICALLY = 'DKIM به طور خودکار برای دامنه [name] فعال شد',
DKIM_FAILED_TO_CREATE = 'ایجاد DKIM برای دامنه [name] ناموفق بود',
DKIM_ENABLED_SUCCESSFULLY = 'DKIM برای دامنه [name] با موفقیت فعال شد',
// Error Messages
FAILED_TO_VERIFY_DOMAIN = 'تایید دامنه [name] ناموفق بود',
FAILED_TO_SETUP_MAIL_SERVER = 'راه‌اندازی دامنه [name] در سرور ایمیل ناموفق بود',
DOMAIN_CREATION_FAILED = 'ایجاد دامنه ناموفق بود',
DNS_VERIFICATION_ATTEMPTS_EXCEEDED = 'تعداد تلاش‌های تایید DNS از حد مجاز بیشتر شد',
// Service Messages
DOMAIN_UPDATED_SUCCESSFULLY = 'دامنه بروزرسانی شد',
VERIFICATION_TOKEN_PREFIX = 'توکن تایید: [token]',
RECORD_NEEDS_FIXING = 'رکورد [type] برای [name] نیاز به اصلاح دارد: [error]',
RECORD_PENDING_VERIFICATION_DETAIL = 'رکورد [type] برای [name] هنوز در انتظار تایید است. لطفا مطمئن شوید که انتشار DNS کامل شده است.',
RECORD_DESCRIPTION_EMAIL_FUNCTIONALITY = 'رکورد [type] برای عملکرد ایمیل',
BUSINESS_ALREADY_HAS_DOMAIN = 'این کسب و کار قبلا دامنه ای دارد',
CAN_NOT_USE_THIS_DOMAIN = 'این دامنه نمیتواند ثبت شود',
}
export const enum MailServerMessage {
FAILED_TO_CREATE_ACCOUNT = 'خطا در ایجاد ایمیل',
FAILED_TO_CREATE_DKIM_KEY = 'خطا در ایجاد کلید DKIM',
}
export const enum DnsRecordMessage {
DNS_RECORD_NOT_FOUND = 'رکورد DNS یافت نشد',
DNS_RECORD_DELETED = 'رکورد DNS با موفقیت حذف شد',
}
export const enum RoleMessage {
NOT_FOUND = 'نقش یافت نشد',
ROLE_HAS_ADMINS = 'نقش دارای ادمین است و قابل حذف نیست',
}
export const enum SignatureMessage {
NAME_NOT_EMPTY = 'نام امضاء نمیتواند خالی باشد',
NAME_MAX_LENGTH = 'نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
NAME_STRING = 'نام امضاء باید یک رشته باشد',
TEXT_CONTENT_NOT_EMPTY = 'محتوای متنی امضاء نمیتواند خالی باشد',
TEXT_CONTENT_STRING = 'محتوای متنی امضاء باید یک رشته باشد',
TEXT_CONTENT_MAX_LENGTH = 'محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
IS_ACTIVE_NOT_EMPTY = 'وضعیت فعال بودن نمیتواند خالی باشد',
IS_ACTIVE_BOOLEAN = 'وضعیت فعال بودن باید بولین باشد',
IS_DEFAULT_NOT_EMPTY = 'وضعیت پیش فرض نمیتواند خالی باشد',
IS_DEFAULT_BOOLEAN = 'وضعیت پیش فرض باید بولین باشد',
CREATED = 'امضاء با موفقیت ایجاد شد',
NOT_FOUND = 'امضاء یافت نشد',
DELETED = 'امضاء با موفقیت حذف شد',
TOGGLE_ACTIVE = 'وضعیت امضاء با موفقیت تغییر کرد',
}
export const enum TemplateMessage {
TEMPLATE_NOT_FOUND = 'قالب یافت نشد',
TEMPLATE_CREATED_SUCCESSFULLY = 'قالب با موفقیت ایجاد شد',
TEMPLATE_UPDATED_SUCCESSFULLY = 'قالب با موفقیت به روزرسانی شد',
TEMPLATE_DELETED_SUCCESSFULLY = 'قالب با موفقیت حذف شد',
TEMPLATE_RETRIEVED_SUCCESSFULLY = 'قالب با موفقیت دریافت شد',
TEMPLATES_LISTED_SUCCESSFULLY = 'لیست قالب ها با موفقیت دریافت شد',
TEMPLATE_NAME_REQUIRED = 'نام قالب الزامی است',
TEMPLATE_NAME_STRING = 'نام قالب باید یک رشته باشد',
TEMPLATE_NAME_MAX_LENGTH = 'نام قالب نمیتواند بیشتر از ۲۵۵ کاراکتر باشد',
TEMPLATE_STRUCTURE_REQUIRED = 'ساختار قالب الزامی است',
TEMPLATE_STRUCTURE_VALID = 'ساختار قالب باید معتبر باشد',
TEMPLATE_RAW_HTML_STRING = 'HTML خام باید یک رشته باشد',
TEMPLATE_BUSINESS_ID_REQUIRED = 'شناسه کسب و کار الزامی است',
TEMPLATE_BUSINESS_ID_UUID = 'شناسه کسب و کار باید یک UUID معتبر باشد',
TEMPLATE_ID_REQUIRED = 'شناسه قالب الزامی است',
TEMPLATE_ID_UUID = 'شناسه قالب باید یک UUID معتبر باشد',
TEMPLATE_ACCESS_DENIED = 'شما دسترسی به این قالب را ندارید',
TEMPLATE_BUSINESS_NOT_FOUND = 'کسب و کار مالک قالب یافت نشد',
SEARCH_STRING = 'نام برای جستجو باید یک رشته باشد',
TEMPLATE_SELECTED_SUCCESSFULLY = 'وضعیت قالب با موفقیت تغییر کرد',
TEMPLATE_THUMBNAIL_URL_VALID = 'آدرس تصویر قالب باید یک آدرس معتبر باشد',
TEMPLATE_RAW_HTML_REQUIRED = 'HTML خام قالب الزامی است',
}
export const enum NotificationMessage {
TITLE_IS_REQUIRED = 'عنوان اعلان الزامی است',
TITLE_STRING = 'عنوان اعلان باید یک رشته باشد',
MESSAGE_IS_REQUIRED = 'متن اعلان الزامی است',
MESSAGE_STRING = 'متن اعلان باید یک رشته باشد',
USERID_IS_REQUIRED = 'شناسه کاربر الزامی است',
USERID_IS_STRING = 'شناسه کاربر باید یک رشته باشد',
USERID_IS_UUID = 'شناسه کاربر باید یک UUID معتبر باشد',
TYPE_IS_REQUIRED = 'نوع اعلان الزامی است',
TYPE_ENUM = 'نوع اعلان باید یک enum معتبر باشد',
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
LOGIN_MESSAGE = 'ورود با موفقیت انجام شد در تاریخ [loginDate]',
LOGIN = 'ورود',
NEW_EMAIL = 'ایمیل جدید',
NEW_EMAIL_MESSAGE = 'یک ایمیل جدید با موضوع [subject] دریافت شد',
CHANGE_PASSWORD_MESSAGE = 'رمز عبور شما با موفقیت تغییر کرد',
CHANGE_PASSWORD = 'تغییر رمز عبور',
NOT_FOUND = 'اعلان یافت نشد',
READ_SUCCESS = 'اعلان با موفقیت خوانده شد',
ORDER_CREATED = 'سفارش جدید ایجاد شد',
ORDER_STATUS_CHANGED = 'وضعیت سفارش تغییر کرد',
PAYMENT_SUCCESS = 'پرداخت با موفقیت انجام شد',
PAGER_CREATED = 'پیج جدید ایجاد شد',
}
export const enum SettingMessageEnum {
SMS_MUST_BE_BOOLEAN = 'تنظیمات ارسال پیامک باید یک boolean باشد',
EMAIL_MUST_BE_BOOLEAN = 'تنظیمات ارسال ایمیل باید یک boolean باشد',
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
}
export const enum AiMessage {
NO_RESPONSE_FROM_AI_MODEL = 'خطا در دریافت پاسخ از مدل AI',
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = 'ایمیل یافت نشد یا دسترسی آن را ندارید',
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = 'محتوای ایمیل خالی است یا قابل استخراج نیست',
DESCRIPTION_IS_REQUIRED = 'شرح و هدف قالب الزامی است',
DESCRIPTION_MUST_BE_A_STRING = 'شرح قالب باید یک رشته باشد',
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = 'شرح قالب باید حداقل ۱۰ کاراکتر باشد',
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = 'شرح قالب نمی‌تواند بیشتر از ۵۰۰ کاراکتر باشد',
THEME_MUST_BE_A_VALID_VALUE = 'تم قالب باید یکی از مقادیر مجاز باشد',
STYLE_MUST_BE_A_VALID_VALUE = 'سبک قالب باید یکی از مقادیر مجاز باشد',
PURPOSE_MUST_BE_A_VALID_VALUE = 'هدف قالب باید یکی از مقادیر مجاز باشد',
TEMPLATE_NAME_MUST_BE_A_STRING = 'نام قالب باید یک رشته باشد',
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = 'نام قالب نمی‌تواند بیشتر از ۱۰۰ کاراکتر باشد',
PREFERRED_COLORS_MUST_BE_A_STRING = 'رنگ‌های ترجیحی باید یک رشته باشد',
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = 'رنگ‌های ترجیحی نمی‌تواند بیشتر از ۲۰۰ کاراکتر باشد',
PREFERRED_COLORS_IS_REQUIRED = 'رنگ‌های ترجیحی الزامی است',
}
export const enum TwoFactorMessage {
TWO_FACTOR_SETUP_INITIATED = 'راه‌اندازی احراز هویت دو مرحله‌ای با موفقیت آغاز شد',
TWO_FACTOR_ENABLED_SUCCESSFULLY = 'احراز هویت دو مرحله‌ای با موفقیت فعال شد',
TWO_FACTOR_DISABLED_SUCCESSFULLY = 'احراز هویت دو مرحله‌ای با موفقیت غیرفعال شد',
TWO_FACTOR_SETUP_FAILED = 'راه‌اندازی احراز هویت دو مرحله‌ای ناموفق بود',
TWO_FACTOR_ENABLE_FAILED = 'فعال‌سازی احراز هویت دو مرحله‌ای ناموفق بود',
TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY = 'کد احراز هویت دو مرحله‌ای با موفقیت تأیید شد',
TWO_FACTOR_TOKEN_VERIFIED_FAILED = 'کد احراز هویت دو مرحله‌ای نامعتبر می‌باشد',
TWO_FACTOR_NOT_ENABLED = 'احراز هویت دو مرحله‌ای فعال نیست',
TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY = 'کدهای پشتیبان با موفقیت تولید شدند',
TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY = 'کد پشتیبان با موفقیت تأیید شد',
TWO_FACTOR_BACKUP_CODE_VERIFIED_FAILED = 'کد پشتیبان نامعتبر است',
TWO_FACTOR_TOKEN_REQUIRED = 'کد احراز هویت دو مرحله‌ای الزامی است',
TWO_FACTOR_TOKEN_INVALID = 'کد احراز هویت دو مرحله‌ای نامعتبر است',
}
export const enum CouponMessage {
NOT_FOUND = 'کوپن یافت نشد',
CODE_ALREADY_EXISTS = 'کد کوپن قبلاً ثبت شده است',
END_DATE_MUST_BE_AFTER_START_DATE = 'تاریخ پایان باید بعد از تاریخ شروع باشد',
PERCENTAGE_MUST_BE_BETWEEN_0_AND_100 = 'درصد تخفیف باید بین ۰ تا ۱۰۰ باشد',
COUPON_INACTIVE = 'کوپن غیرفعال است',
COUPON_NOT_STARTED = 'کوپن هنوز شروع نشده است',
COUPON_EXPIRED = 'کوپن منقضی شده است',
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز است',
COUPON_CREATED = 'کوپن با موفقیت ایجاد شد',
COUPON_UPDATED = 'کوپن با موفقیت به‌روزرسانی شد',
COUPON_DELETED = 'کوپن با موفقیت حذف شد',
COUPON_VALID = 'کوپن معتبر است',
COUPON_INVALID = 'کوپن نامعتبر است',
COUPON_RESTRICTED_TO_USER = 'کوپن فقط برای کاربر مشخصی معتبر است',
}
export const enum ReviewMessage {
NOT_FOUND = 'نظر یافت نشد',
ALREADY_COMMENTED = 'شما قبلاً برای این غذا نظر داده‌اید',
CAN_ONLY_UPDATE_OWN = 'شما فقط می‌توانید نظرات خود را ویرایش کنید',
CAN_ONLY_DELETE_OWN = 'شما فقط می‌توانید نظرات خود را حذف کنید',
RATING_REQUIRED = 'امتیاز الزامی است',
RATING_INVALID = 'امتیاز باید بین ۱ تا ۵ باشد',
COMMENT_CREATED = 'نظر با موفقیت ثبت شد',
COMMENT_UPDATED = 'نظر با موفقیت به‌روزرسانی شد',
COMMENT_DELETED = 'نظر با موفقیت حذف شد',
}
export const enum IconMessage {
NOT_FOUND = 'آیکون یافت نشد',
NOT_CREATED = 'ایجاد آیکون با خطا مواجه شد',
NOT_UPDATED = 'به‌روزرسانی آیکون با خطا مواجه شد',
NOT_DELETED = 'حذف آیکون با خطا مواجه شد',
}
export const enum GroupMessage {
NOT_FOUND = 'گروه آیکون یافت نشد',
NOT_CREATED = 'ایجاد گروه آیکون با خطا مواجه شد',
NOT_UPDATED = 'به‌روزرسانی گروه آیکون با خطا مواجه شد',
NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد',
}
export const enum OrderMessage {
NOT_FOUND = 'سفارش یافت نشد',
PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است',
INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است',
CART_EMPTY = 'سبد خرید خالی است. لطفا قبل از ایجاد سفارش، آیتم‌هایی به سبد اضافه کنید',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
PAYMENT_METHOD_REQUIRED = 'روش پرداخت الزامی است. لطفا قبل از ایجاد سفارش، روش پرداخت را تنظیم کنید',
USER_NOT_FOUND = 'کاربر یافت نشد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
DELIVERY_NOT_FOUND = 'روش ارسال یافت نشد',
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز برای این روش ارسال است',
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
ADDRESS_REQUIRED = 'آدرس الزامی است. لطفا قبل از ایجاد سفارش، آدرس تحویل را تنظیم کنید',
CAR_ADDRESS_REQUIRED = 'آدرس خودرو الزامی است. لطفا قبل از ایجاد سفارش، آدرس خودرو را تنظیم کنید',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
}
export const enum CartMessage {
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
CAR_ADDRESS_REQUIRED = 'آدرس خودرو برای تحویل به خودرو الزامی است',
ADDRESS_REQUIRED = 'آدرس برای تحویل پیک الزامی است',
NOT_FOUND = 'سبد خرید یافت نشد',
COUPON_NOT_FOUND = 'کوپن یافت نشد',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
FOOD_NO_INVENTORY = 'غذا موجودی ندارد',
INSUFFICIENT_STOCK = 'موجودی کافی نیست',
USER_NOT_FOUND = 'کاربر یافت نشد',
ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
ADDRESS_NOT_BELONGS_TO_USER = 'آدرس به کاربر فعلی تعلق ندارد',
ADDRESS_NO_COORDINATES = 'آدرس مختصات جغرافیایی ندارد',
ADDRESS_OUTSIDE_SERVICE_AREA = 'آدرس خارج از محدوده سرویس رستوران است',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
DELIVERY_METHOD_NOT_ENABLED = 'روش ارسال برای این رستوران فعال نیست',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
WALLET_INSUFFICIENT = 'موجودی کیف پول کافی نیست',
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT = 'روش ارسال برای این رستوران یافت نشد',
COUPON_CANNOT_BE_APPLIED = 'این کوپن قابل اعمال بر روی آیتم‌های سبد خرید شما نیست. لطفا آیتم‌هایی از دسته‌بندی‌ها یا غذاهای مشخص شده اضافه کنید',
FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER = 'تمام غذاها باید قابلیت تحویل پیک داشته باشند. غذاهای زیر این قابلیت را ندارند: [foodTitles]',
FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'غذا [foodTitle] فقط در ساعات وعده‌های غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعده‌های غذایی است',
FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'غذا [foodTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این غذا سازگار نیست',
FOOD_ONLY_AVAILABLE_ON_DAYS = 'غذا [foodTitle] فقط در روزهای [allowedDays] در دسترس است. امروز [currentDay] است که این غذا در دسترس نیست',
}
export const enum PaymentMessage {
UNSUPPORTED_PAYMENT_METHOD = 'روش پرداخت پشتیبانی نمی‌شود',
ORDER_NOT_FOUND = 'سفارش یافت نشد',
AMOUNT_MUST_BE_GREATER_THAN_ZERO = 'مبلغ باید بیشتر از صفر باشد',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
GATEWAY_REQUIRED = 'درگاه پرداخت برای پرداخت آنلاین الزامی است',
MERCHANT_ID_REQUIRED = 'شناسه مرچنت برای پرداخت آنلاین الزامی است',
RESTAURANT_DOMAIN_REQUIRED = 'دامنه رستوران برای پرداخت آنلاین الزامی است',
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
INSUFFICIENT_WALLET_BALANCE = 'موجودی کیف پول کافی نیست',
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرین‌پال',
ZARINPAL_PAYMENT_REQUEST_ERROR = 'خطا در درخواست پرداخت زرین‌پال',
ZARINPAL_VERIFY_ERROR = 'خطا در تایید پرداخت زرین‌پال',
GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
}
export const enum DeliveryMessage {
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
}
export const enum InventoryMessage {
AVAILABLE_STOCK_EXCEEDS_TOTAL = 'موجودی موجود نمی‌تواند از موجودی کل بیشتر باشد',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
FOODS_NOT_FOUND = 'غذاها یافت نشدند',
}
export const enum PagerMessage {
CREATED_SUCCESS = 'درخواست پیج با موفقیت ایجاد شد',
}
export const enum UserSuccessMessage {
ADDRESS_DELETED_SUCCESS = 'آدرس با موفقیت حذف شد',
SCORE_CONVERTED_SUCCESS = 'امتیاز با موفقیت به کیف پول تبدیل شد',
}
export const enum ContactMessage {
NOT_FOUND = 'تماس یافت نشد',
CREATED = 'تماس با موفقیت ثبت شد',
UPDATED = 'تماس با موفقیت به‌روزرسانی شد',
DELETED = 'تماس با موفقیت حذف شد',
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Permission names enum
* All permission names used in the system should be defined here
*/
export enum Permission {
MANAGE_PAGER = 'manage_pager',
// Food Management
MANAGE_FOODS = 'manage_foods',
MANAGE_CATEGORIES = 'manage_categories',
// Order Management
MANAGE_ORDERS = 'manage_orders',
// User & Admin Management
MANAGE_ADMINS = 'manage_admins',
MANAGE_USERS = 'manage_users',
// Reports & Finances
VIEW_REPORTS = 'view_reports',
UPDATE_RESTAURANT = 'update_restaurant',
// Role Management
MANAGE_ROLES = 'manage_roles',
MANAGE_REVIEWS = 'manage_reviews',
MANAGE_COUPONS = 'manage_coupons',
MANAGE_PAYMENTS = 'manage_payments',
MANAGE_DELIVERY = 'manage_delivery',
MANAGE_SETTINGS = 'manage_settings',
MANAGE_SCHEDULES = 'manage_schedules',
MANAGE_REPORTS = 'manage_reports',
MANAGE_CONTACTS = 'manage_contacts',
NEW_ORDER_NOTIFICATION='new_order_notification',
PAGER_NOTIFICATION='pager_notification'
}
/**
* Permission titles mapping (Farsi)
* Maps permission names to their Farsi titles
*/
export const PermissionTitles: Record<Permission, string> = {
[Permission.MANAGE_PAGER]: 'مدیریت پیجر',
[Permission.MANAGE_FOODS]: 'مدیریت غذاها',
[Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها',
[Permission.MANAGE_ORDERS]: 'مدیریت سفارشات',
[Permission.MANAGE_ADMINS]: 'مدیریت مدیران',
[Permission.MANAGE_USERS]: 'مدیریت کاربران',
[Permission.VIEW_REPORTS]: 'مشاهده گزارشات',
[Permission.UPDATE_RESTAURANT]: 'ویرایش رستوران',
[Permission.MANAGE_ROLES]: 'مدیریت نقش‌ها',
[Permission.MANAGE_REVIEWS]: 'مدیریت نظرات',
[Permission.MANAGE_COUPONS]: 'مدیریت کوپن‌ها',
[Permission.MANAGE_PAYMENTS]: 'مدیریت پرداخت‌ها',
[Permission.MANAGE_DELIVERY]: 'مدیریت تحویل',
[Permission.MANAGE_SETTINGS]: 'مدیریت تنظیمات',
[Permission.MANAGE_SCHEDULES]: 'مدیریت برنامه‌ها',
[Permission.MANAGE_REPORTS]: 'مدیریت گزارشات',
[Permission.MANAGE_CONTACTS]: 'مدیریت تماس‌های کاربران',
[Permission.NEW_ORDER_NOTIFICATION]: "نوتیف سفارش جدید",
[Permission.PAGER_NOTIFICATION]: "نوتیف پیجر جدید"
};
@@ -0,0 +1,9 @@
export interface PaginatedResult<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
@@ -0,0 +1,9 @@
import type { ValueProvider } from '@nestjs/common';
import { Logger } from '@nestjs/common';
import { MIKRO_ORM_QUERY_LOGGER } from '../constants';
export const mikroOrmQueryLoggerProvider: ValueProvider = {
provide: MIKRO_ORM_QUERY_LOGGER, //
useValue: new Logger('mikro-orm'),
};
+27
View File
@@ -0,0 +1,27 @@
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
import type { CacheModuleAsyncOptions } from '@nestjs/cache-manager';
import { ConfigService } from '@nestjs/config';
import { CacheableMemory } from 'cacheable';
export function cacheConfig(): CacheModuleAsyncOptions {
return {
isGlobal: true,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const redisUri = configService.get('REDIS_URI');
const namespace = configService.get('CACHE_NAMESPACE', 'app-cache');
const ttl = +configService.get<number>('CACHE_TTL', 3600); //1 hour
return {
stores: [
new Keyv({
store: new CacheableMemory({ ttl: ttl * 1000, lruSize: 5000 }),
namespace: namespace
}),
new KeyvRedis(redisUri, { namespace: namespace }),
],
};
},
};
}
+16
View File
@@ -0,0 +1,16 @@
import type { HttpModuleAsyncOptions } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
export function httpConfig(): HttpModuleAsyncOptions {
return {
inject: [ConfigService],
global: true,
useFactory: (configService: ConfigService) => {
return {
timeout: configService.getOrThrow<number>('AXIOS_TIMEOUT'),
headers: { 'Content-Type': 'application/json' },
global: true,
};
},
};
}
+101
View File
@@ -0,0 +1,101 @@
import { defineConfig, PostgreSqlDriver } from '@mikro-orm/postgresql';
import * as dotenv from 'dotenv';
// 1. Load environment variables from your .env file
dotenv.config();
// 2. Read variables directly from process.env
const DB_PASS = process.env.DB_PASS;
const DB_USER = process.env.DB_USER;
const DB_HOST = process.env.DB_HOST;
const DB_PORT = process.env.DB_PORT ? +process.env.DB_PORT : 5432;
const DB_NAME = process.env.DB_NAME;
const isProduction = process.env.NODE_ENV === 'production';
// 3. Add checks to ensure variables are loaded
if (!DB_PASS || !DB_USER || !DB_HOST || !DB_PORT || !DB_NAME) {
throw new Error('One or more database environment variables are not set.');
}
const encodedPassword = encodeURIComponent(DB_PASS);
// 4. Export the result of defineConfig as the default
export default defineConfig({
entities: ['dist/**/*.entity.js'],
entitiesTs: ['src/**/*.entity.ts'],
driver: PostgreSqlDriver,
dbName: DB_NAME,
debug: false,
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
ensureDatabase: isProduction
? false
: {
forceCheck: true,
create: true,
schema: 'update',
},
forceUtcTimezone: true,
pool: {
min: isProduction ? 5 : 2,
max: isProduction ? 20 : 10,
idleTimeoutMillis: 30000,
acquireTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createTimeoutMillis: 15000,
destroyTimeoutMillis: 5000,
},
// 5. Use console for CLI logging, not the injected Nest logger
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
logger: isProduction ? console.log.bind(console) : console.debug.bind(console),
schemaGenerator: {
disableForeignKeys: false,
},
migrations: {
path: './database/migrations',
pathTs: './database/migrations',
tableName: 'migrations',
transactional: true,
allOrNothing: true,
dropTables: false,
safe: true,
emit: 'ts',
},
connect: true,
allowGlobalContext: true,
seeder: {
path: './dist/seeders',
pathTs: './src/seeders',
defaultSeeder: 'DatabaseSeeder',
},
driverOptions: {
connection: {
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
statement_timeout: 60000,
query_timeout: 60000,
idle_in_transaction_session_timeout: 60000,
connectionTimeoutMillis: 10000,
application_name: DB_NAME,
},
connectionRetries: 5,
connectionRetryDelay: 3000,
validateConnection: (connection: any) => {
try {
return !!(connection && !connection.closed);
} catch (e: unknown) {
// Use console.error directly
console.error(`Connection validation failed: ${(e as Error).message}`);
return false;
}
},
poolErrorHandler: (err: Error) => {
// Use console.error directly
console.error(`PostgreSQL pool error: ${err.message}`);
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
console.warn('Connection reset detected, will attempt to reconnect');
return true;
}
return false;
},
},
});
+109
View File
@@ -0,0 +1,109 @@
import type { MikroOrmModuleAsyncOptions } from '@mikro-orm/nestjs';
import { ConfigService } from '@nestjs/config';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
// import { defineConfig } from '@mikro-orm/postgresql';
import type { Logger } from '@nestjs/common';
import { MIKRO_ORM_QUERY_LOGGER } from '../common/constants';
import { mikroOrmQueryLoggerProvider } from '../common/providers/mikro-orm-logger.provider';
const dataBaseConfig: MikroOrmModuleAsyncOptions = {
// entities: ['dist/**/*.entity.js'],
// entitiesTs: ['src/**/*.entity.ts'],
// dbName: 'your_db_name',
// type: 'postgresql',
// user: 'your_user',
// password: 'your_password',
// host: 'localhost',
// port: 5432,
// debug: true,
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
providers: [mikroOrmQueryLoggerProvider],
useFactory: (configService: ConfigService, logger: Logger) => {
const DB_PASS = configService.getOrThrow<string>('DB_PASS');
const DB_USER = configService.getOrThrow<string>('DB_USER');
const DB_HOST = configService.getOrThrow<string>('DB_HOST');
const DB_PORT = configService.getOrThrow<number>('DB_PORT');
const DB_NAME = configService.getOrThrow<string>('DB_NAME');
const encodedPassword = encodeURIComponent(DB_PASS);
const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production';
return {
// entities: ['dist/**/*.entity.js'],
entitiesTs: ['src/**/*.entity.ts'],
driver: PostgreSqlDriver,
autoLoadEntities: true,
dbName: DB_NAME,
debug: false,
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
ensureDatabase: isProduction
? false
: {
forceCheck: true,
create: true,
schema: 'update',
},
forceUtcTimezone: true,
pool: {
min: isProduction ? 5 : 2,
max: isProduction ? 20 : 10,
idleTimeoutMillis: 30000,
acquireTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createTimeoutMillis: 15000,
destroyTimeoutMillis: 5000,
},
logger: isProduction ? message => logger.log(message) : message => logger.debug(message),
schemaGenerator: {
// createForeignKey: !isProduction,
disableForeignKeys: false,
// createIndex: true,
},
migrations: {
path: './database/migrations',
pathTs: './database/migrations',
tableName: 'migrations',
transactional: true,
allOrNothing: true,
dropTables: false,
safe: true,
emit: 'ts',
},
connect: true,
allowGlobalContext: true,
driverOptions: {
connection: {
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
statement_timeout: 60000,
query_timeout: 60000,
idle_in_transaction_session_timeout: 60000,
connectionTimeoutMillis: 10000,
application_name: configService.getOrThrow<string>('DB_NAME'),
},
connectionRetries: 5,
connectionRetryDelay: 3000,
validateConnection: (connection: any) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return !!(connection && !connection.closed);
} catch (e: unknown) {
logger.error(`Connection validation failed: ${(e as Error).message}`);
return false;
}
},
poolErrorHandler: (err: Error) => {
logger.error(`PostgreSQL pool error: ${err.message}`);
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
logger.warn('Connection reset detected, will attempt to reconnect');
return true;
}
return false;
},
},
};
},
};
export default dataBaseConfig;
+27
View File
@@ -0,0 +1,27 @@
import type { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export function getSwaggerConfig(app: INestApplication) {
const config = new DocumentBuilder()
.setTitle('D-menu API')
.setDescription('API documentation for D-menu backend')
.setVersion('1.0')
.addBearerAuth() // optional: for JWT endpoints
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
filter: true,
showExtensions: true,
docExpansion: 'none',
// HIDE MODELS / SCHEMAS
defaultModelsExpandDepth: -1,
defaultModelExpandDepth: -1,
},
});
}
+48
View File
@@ -0,0 +1,48 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { FastifyReply } from 'fastify';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// Handle HTTP/REST context
try {
const ctx = host.switchToHttp();
const reply = ctx.getResponse<FastifyReply>();
// Safety check: ensure reply has the status method
if (!reply || typeof reply.status !== 'function') {
return;
}
// const request = ctx.getRequest<FastifyRequest>();
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse = exception.getResponse();
const message = this.extractMessage(exceptionResponse);
reply.status(status).send({
statusCode: status,
success: false,
error: {
message,
// timestamp: new Date().toISOString(),
// path: request.url,
},
});
} catch {
// If we can't handle it, let it propagate
return;
}
}
private extractMessage(response: string | Record<string, any>): string | string[] {
if (typeof response === 'string') {
return [response];
}
if (typeof response === 'object' && 'message' in response) {
return Array.isArray(response.message) ? response.message : [response.message];
}
return 'something wrong';
}
}
+61
View File
@@ -0,0 +1,61 @@
// import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
// import { FastifyRequest } from 'fastify';
// import { Observable, map } from 'rxjs';
// import { IPageFormat } from '../../../../danak_dsc_api/src/common/interfaces/IPagination';
// @Injectable()
// export class PaginationInterceptor implements NestInterceptor {
// private readonly logger = new Logger(PaginationInterceptor.name);
// intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
// const request = context.switchToHttp().getRequest<FastifyRequest>();
// const query = request.query as { page: string; limit: string };
// const page = parseInt(query.page, 10) || 1;
// const limit = parseInt(query.limit, 10) || 10;
// const className = context.getClass().name;
// const handlerName = context.getHandler().name;
// return next.handle().pipe(
// map(data => {
// if (data && (data.paginate || data.count)) {
// const { count, paginate, ...response } = data;
// this.logger.log(`paginate response from ${className}.${handlerName}`);
// const pager = this.formatPage(page, limit, count, request);
// return {
// pager,
// ...response,
// };
// }
// return data;
// }),
// );
// }
// private formatPage(page: number, limit: number, totalItems: number, request: FastifyRequest): IPageFormat {
// const totalPages = Math.ceil(totalItems / limit);
// const prevPage = page === 1 ? false : page - 1;
// const nextPage = page >= totalPages ? false : page + 1;
// const formatLink = (pageNum: number | boolean): string | boolean => {
// if (!pageNum) return false;
// const protocol = request.protocol;
// const hostname = request.hostname;
// const originalUrl = request.url;
// return `${protocol}://${hostname}${originalUrl.split('?')[0]}?page=${pageNum}&limit=${limit}`;
// };
// return {
// page,
// limit,
// totalItems,
// totalPages,
// prevPage: formatLink(prevPage),
// nextPage: formatLink(nextPage),
// };
// }
// }
+68
View File
@@ -0,0 +1,68 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { FastifyReply } from 'fastify';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
constructor() { }
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
// For REST endpoints, wrap the response
try {
const ctx = context.switchToHttp().getResponse<FastifyReply>();
const statusCode = ctx.statusCode;
// If statusCode is undefined, skip wrapping
if (statusCode === undefined) {
return next.handle();
}
return next.handle().pipe(
map(data => {
// If data is already wrapped or doesn't need wrapping, return as-is
if (data && typeof data === 'object' && 'data' in data && 'success' in data) {
console.log('data', data)
return {
data: data,
...('nextCursor' in (data as any) && (data as any).nextCursor != null
? { nextCursor: (data as any).nextCursor }
: {}),
}
}
// Check if this is a paginated result (has both data and meta properties)
if (data && typeof data === 'object' && 'data' in data && 'meta' in data) {
return {
statusCode,
success: true,
data: data.data,
meta: data.meta,
};
}
if (data && 'data' in data) {
return {
statusCode,
success: true,
data: data.data,
...('nextCursor' in data
? { nextCursor: (data as any).nextCursor }
: {}),
};
}
return {
statusCode,
success: true,
data,
};
}),
);
} catch {
// If we can't get HTTP context, return data as-is
return next.handle();
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class HTTPLogger implements NestMiddleware {
private readonly logger = new Logger(HTTPLogger.name);
// use(req: any, res: any, next: (error?: Error | any) => void) {}
use(req: FastifyRequest, rep: FastifyReply['raw'], next: () => void) {
const { ip, method, originalUrl } = req;
const userAgent = req.headers['user-agent'] || '';
const startAt = process.hrtime();
rep.on('finish', () => {
const { statusCode } = rep;
const contentLength = rep.getHeader('content-length') || 0;
const dif = process.hrtime(startAt);
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
this.logger.log(
`${method} - ${originalUrl} - ${statusCode} - ${contentLength} - ${userAgent} - ${ip} - ${responseTime.toFixed(2)}ms`,
);
});
next();
}
}
+61
View File
@@ -0,0 +1,61 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { Logger, ValidationPipe } from '@nestjs/common';
import { getSwaggerConfig } from './config/swagger.config';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
// 👈 Import the Fastify application type
import { type NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
import { HttpExceptionFilter } from './core/exceptions/http.exceptions';
import { ResponseInterceptor } from './core/interceptors/response.interceptor';
// import { PaginationInterceptor } from './core/interceptors/pagination.interceptor';
async function bootstrap() {
const logger = new Logger('APP');
// 1. Create the application using the Fastify adapter
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(), // Instantiate the FastifyAdapter
// Optional: Pass the custom logger (like your 'APP' logger) to NestFactory
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] },
);
await app.register(fastifyCookie);
await app.register(multipart);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(
new ResponseInterceptor(),
// , new PaginationInterceptor()
);
app.useGlobalFilters(new HttpExceptionFilter());
const configService = app.get<ConfigService>(ConfigService);
// Ensure the port is handled correctly, use 4000 as fallback if needed
const APP_PORT = configService.getOrThrow<number>('APP_PORT') ?? 4000;
// 2. Swagger Integration Note
// Since you are using platform-fastify, you must ensure your getSwaggerConfig
// function is configured to use the Fastify platform option if necessary.
getSwaggerConfig(app);
app.enableCors({
origin: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
credentials: true,
optionsSuccessStatus: 204,
});
// 3. Listen on port using Fastify's listen method
// '0.0.0.0' is recommended for Docker/containerized environments
await app.listen(APP_PORT, '0.0.0.0', () => {
logger.log(`Application is running on: http://localhost:${APP_PORT}`);
logger.log(`Swagger documentation: http://localhost:${APP_PORT}/docs`);
});
}
bootstrap();
+27
View File
@@ -0,0 +1,27 @@
import { Module, forwardRef } from '@nestjs/common';
import { AdminService } from './providers/admin.service';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Admin } from './entities/admin.entity';
import { JwtModule } from '@nestjs/jwt';
import { AdminRepository } from './repositories/admin.repository';
import { Role } from '../roles/entities/role.entity';
import { Permission } from '../roles/entities/permission.entity';
import { RolePermission } from '../roles/entities/rolePermission.entity';
import { AdminController } from './controllers/admin.controller';
import { UtilsModule } from '../utils/utils.module';
import { RestaurantsModule } from '../restaurants/restaurants.module';
import { AdminRole } from './entities/adminRole.entity';
import { AdminRoleRepository } from './repositories/admin-role.repository';
@Module({
providers: [AdminService, AdminRepository, AdminRoleRepository],
controllers: [AdminController],
imports: [
MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission, AdminRole]),
JwtModule,
UtilsModule,
forwardRef(() => RestaurantsModule),
],
exports: [AdminService, AdminRepository],
})
export class AdminModule { }
@@ -0,0 +1,104 @@
import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards, Patch, Param, Delete } from '@nestjs/common';
import { AdminService } from '../providers/admin.service';
import { CreateAdminDto } from '../dto/create-admin.dto';
import { UpdateAdminDto } from '../dto/update-admin.dto';
import { ApiTags, ApiOperation, ApiBody, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
import { RestId } from 'src/common/decorators';
import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { Admin } from '../entities/admin.entity';
import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
@ApiBearerAuth()
@ApiTags('admin')
@Controller()
export class AdminController {
constructor(private readonly adminService: AdminService) { }
@Get('admin/admins')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
getAll(@RestId() restId: string) {
return this.adminService.findAllByRestaurantId(restId);
}
@Get('admin/admins/profile')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
getMe(@AdminId() adminId: string, @RestId() restId: string) {
return this.adminService.finAdminById(adminId, restId);
}
@Post('admin/admins')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new admin' })
@ApiBody({ type: CreateAdminDto })
async create(@Body() dto: CreateAdminDto, @RestId() restId: string) {
const admin = await this.adminService.createAdminForMyRestaurant(restId, dto);
return admin;
}
@Patch('admin/admins/:adminId')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
@ApiOperation({ summary: 'Update an admin' })
@ApiParam({ name: 'adminId', description: 'Admin ID' })
update(@Param('adminId') adminId: string, @Body() dto: UpdateAdminDto,
@RestId() restId: string) {
return this.adminService.update(adminId, restId, dto);
}
@Get('admin/admins/:adminId')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
getById(@Param('adminId') adminId: string, @RestId() restId: string) {
return this.adminService.finAdminById(adminId, restId);
}
@Delete('admin/admins/:adminId')
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ADMINS)
@ApiOperation({ summary: 'Delete an admin by ID' })
@ApiParam({ name: 'id', description: 'Admin ID' })
deleteById(@Param('adminId') adminId: string, @RestId() restId: string) {
return this.adminService.remove(adminId, restId);
}
/** Super Admin Endpoints */
@UseGuards(SuperAdminAuthGuard)
@Get('super-admin/restaurants/:restaurantId/admins')
@ApiOperation({ summary: 'Get admins for a specific restaurant (Super Admin only)' })
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
getAdminForRestaurant(@Param('restaurantId') restaurantId: string) {
return this.adminService.getAdminsForRestaurantBySuperAdmin(restaurantId);
}
@UseGuards(SuperAdminAuthGuard)
@Post('super-admin/restaurants/:restaurantId/admins')
@ApiOperation({ summary: 'Create admin for a specific restaurant (Super Admin only)' })
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
@ApiBody({ type: CreateMyRestaurantAdminDto })
createAdminForRestaurant(
@Param('restaurantId') restaurantId: string,
@Body() dto: CreateMyRestaurantAdminDto,
): Promise<Admin> {
return this.adminService.createAdminForRestaurantBySuperAdmin(restaurantId, dto);
}
@UseGuards(SuperAdminAuthGuard)
@Delete('super-admin/restaurants/:restaurantId/admins/:adminId')
@ApiOperation({ summary: 'Revoke admin role from a restaurant (Super Admin only)' })
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
@ApiParam({ name: 'adminId', description: 'Admin ID' })
revokeAdminFromRestaurant(
@Param('restaurantId') restaurantId: string,
@Param('adminId') adminId: string,
) {
return this.adminService.remove(adminId, restaurantId);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateAdminDto {
@ApiProperty({ description: 'Mobile phone number' })
@IsNotEmpty()
@IsString()
phone!: string;
@ApiProperty({ description: 'First name', required: false })
@IsOptional()
@IsString()
firstName?: string;
@ApiProperty({ description: 'Last name', required: false })
@IsOptional()
@IsString()
lastName?: string;
@ApiProperty({ description: 'Role id for the admin' })
@IsNotEmpty()
@IsString()
roleId!: string;
}
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateMyRestaurantAdminDto {
@ApiProperty({ description: 'Mobile phone number' })
@IsNotEmpty()
@IsString()
phone!: string;
@ApiProperty({ description: 'First name', required: false })
@IsOptional()
@IsString()
firstName?: string;
@ApiProperty({ description: 'Last name', required: false })
@IsOptional()
@IsString()
lastName?: string;
@ApiProperty({ description: 'Role id for the admin' })
@IsNotEmpty()
@IsString()
roleId!: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateAdminDto } from './create-admin.dto';
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}
@@ -0,0 +1,31 @@
import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { AdminRole } from './adminRole.entity';
import { normalizePhone } from '../../utils/phone.util';
@Entity({ tableName: 'admins' })
export class Admin extends BaseEntity {
@Property({ nullable: true })
firstName?: string;
@Property({ nullable: true })
lastName?: string;
private _phone!: string;
@Property({ unique: true })
get phone(): string {
return this._phone;
}
set phone(value: string) {
this._phone = normalizePhone(value);
}
@OneToMany(() => AdminRole, adminRole => adminRole.admin, {
cascade: [Cascade.ALL],
orphanRemoval: true,
})
roles = new Collection<AdminRole>(this);
}
@@ -0,0 +1,21 @@
import { Entity, Index, ManyToOne, Unique } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Role } from '../../roles/entities/role.entity';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { Admin } from './admin.entity';
@Entity({ tableName: 'admin_roles' })
@Unique({ properties: ['admin', 'restaurant'] })
@Index({ properties: ['admin', 'restaurant'] })
@Index({ properties: ['admin'] })
@Index({ properties: ['restaurant'] })
export class AdminRole extends BaseEntity {
@ManyToOne(() => Admin)
admin!: Admin;
@ManyToOne(() => Role)
role!: Role;
@ManyToOne(() => Restaurant, { nullable: true })
restaurant?: Restaurant;
}
@@ -0,0 +1,203 @@
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { Admin } from '../entities/admin.entity';
import { Role } from '../../roles/entities/role.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { wrap } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/postgresql';
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
import { UpdateAdminDto } from '../dto/update-admin.dto';
import { AdminRole } from '../entities/adminRole.entity';
import { normalizePhone } from '../../utils/phone.util';
import { AdminRepository } from '../repositories/admin.repository';
import { AdminRoleRepository } from '../repositories/admin-role.repository';
@Injectable()
export class AdminService {
constructor(
private readonly adminRepository: AdminRepository,
private readonly adminRoleRepository: AdminRoleRepository,
private readonly em: EntityManager,
) { }
async findByPhone(phone: string) {
const normalizedPhone = normalizePhone(phone);
return this.adminRepository.findOne({ phone: normalizedPhone });
}
async finAdminById(
adminId: string,
restId: string,
) {
const admin = await this.adminRepository.findOne(
{ id: adminId, roles: { restaurant: { id: restId } } },
{ populate: ['roles'], exclude: ['roles'] },
);
if (!admin) {
throw new NotFoundException('Admin not found');
}
const adminPlain = wrap(admin).toObject();
const adminRole = await this.adminRoleRepository.findOne({ admin: admin, restaurant: { id: restId } });
return { ...adminPlain, role: adminRole?.role };
}
async findAllByRestaurantId(restId: string) {
return this.adminRepository.find({ roles: { restaurant: { id: restId } } }, { populate: ['roles', 'roles.role'] });
}
async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) {
const { phone, firstName, lastName, roleId } = dto;
const normalizedPhone = normalizePhone(phone);
let admin: Admin | null = null;
admin = await this.adminRepository.findOne({
phone: normalizedPhone,
});
if (!admin) {
admin = this.adminRepository.create({
phone: normalizedPhone,
firstName,
lastName,
});
await this.em.persistAndFlush(admin);
}
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
if (!role) throw new NotFoundException('Role not found');
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) throw new NotFoundException('Restaurant not found');
let adminRole = await this.adminRoleRepository.findOne({
admin: admin,
restaurant: restaurant,
});
if (!adminRole) {
adminRole = this.adminRoleRepository.create({
admin: admin,
role,
restaurant,
});
} else {
adminRole.role = role;
}
await this.em.persistAndFlush(adminRole);
return admin;
}
async createAdminForRestaurantBySuperAdmin(restId: string, dto: CreateMyRestaurantAdminDto) {
const { phone, firstName, lastName, roleId } = dto;
const normalizedPhone = normalizePhone(phone);
let admin: Admin | null = null;
admin = await this.adminRepository.findOne({
phone: normalizedPhone,
});
if (!admin) {
admin = this.adminRepository.create({
phone: normalizedPhone,
firstName,
lastName,
});
await this.em.persistAndFlush(admin);
}
const role = await this.em.findOne(Role, { id: roleId });
if (!role) throw new NotFoundException('Role* not found' + roleId);
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) throw new NotFoundException('Restaurant not found');
let adminRole = await this.adminRoleRepository.findOne({
admin: admin,
restaurant: restaurant,
});
if (!adminRole) {
adminRole = this.adminRoleRepository.create({
admin: admin,
role,
restaurant,
});
} else {
adminRole.role = role;
}
await this.em.persistAndFlush(adminRole);
return admin;
}
async getAdminsForRestaurantBySuperAdmin(restId: string) {
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) throw new NotFoundException('Restaurant not found');
return this.adminRepository.find({ roles: { restaurant: restaurant } }, { populate: ['roles', 'roles.role'] });
}
async update(adminId: string, restId: string, dto: UpdateAdminDto) {
const admin = await this.adminRepository.findOne(
{ id: adminId, roles: { restaurant: { id: restId } } },
{ populate: ['roles', 'roles.role', 'roles.restaurant'] },
);
if (!admin) {
throw new NotFoundException('Admin not found');
}
const { roleId, ...rest } = dto;
// Update scalar fields (firstName, lastName, phone)
if (rest.firstName !== undefined) {
admin.firstName = rest.firstName;
}
if (rest.lastName !== undefined) {
admin.lastName = rest.lastName;
}
if (rest.phone !== undefined && rest.phone !== admin.phone) {
const normalizedPhone = normalizePhone(rest.phone);
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
if (exists) {
throw new ConflictException('This Phone Number is already taken');
}
admin.phone = normalizedPhone;
}
// Update role if roleId is provided
if (roleId) {
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
if (!role) {
throw new NotFoundException('Role not found');
}
// Find existing AdminRole for this admin and restaurant
const existingAdminRole = await this.em.findOne(AdminRole, {
admin: { id: adminId },
restaurant: { id: restId },
});
if (existingAdminRole) {
// Update existing role
existingAdminRole.role = role;
} else {
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) throw new NotFoundException('Restaurant not found');
// Create new AdminRole
const newAdminRole = this.em.create(AdminRole, {
admin,
role,
restaurant,
});
admin.roles.add(newAdminRole);
}
}
await this.em.persistAndFlush(admin);
return admin;
}
async remove(adminId: string, restId: string): Promise<void> {
const adminRole = await this.adminRoleRepository.findOne({ admin: { id: adminId }, restaurant: { id: restId } });
if (!adminRole) {
throw new NotFoundException('Admin role not found');
}
return this.em.removeAndFlush(adminRole);
}
}
@@ -0,0 +1,13 @@
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { AdminRole } from '../entities/adminRole.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AdminRoleRepository extends EntityRepository<AdminRole> {
constructor(
readonly em: EntityManager,
) {
super(em, AdminRole);
}
}
@@ -0,0 +1,58 @@
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { Admin } from '../entities/admin.entity';
import { AdminRole } from '../entities/adminRole.entity';
import { RestRepository } from '../../restaurants/repositories/rest.repository';
import { Injectable } from '@nestjs/common';
import { normalizePhone } from '../../utils/phone.util';
@Injectable()
export class AdminRepository extends EntityRepository<Admin> {
constructor(
readonly em: EntityManager,
private readonly restRepository: RestRepository,
) {
super(em, Admin);
}
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
console.log('phone', phone);
const normalizedPhone = normalizePhone(phone);
// First, find the restaurant by slug using the same repository as auth service
const restaurant = await this.restRepository.findOne({ slug });
if (!restaurant) {
return null;
}
// Find AdminRole that matches the admin phone and restaurant
const adminRole = await this.em.findOne(
AdminRole,
{
admin: { phone: normalizedPhone },
restaurant: { id: restaurant.id },
},
{ populate: ['admin', 'admin.roles', 'role', 'role.permissions', 'restaurant'] },
);
if (!adminRole || !adminRole.admin) {
return null;
}
// Ensure all roles are populated
await adminRole.admin.roles.loadItems();
for (const role of adminRole.admin.roles.getItems()) {
if (role.role && role.role.permissions && !role.role.permissions.isInitialized()) {
await role.role.permissions.loadItems();
}
}
return adminRole.admin;
}
async findAdminsWithPermission(restaurantId: string, permission: string): Promise<Admin[]> {
const admins = await this.em.find(
Admin,
{ roles: { restaurant: { id: restaurantId }, role: { permissions: { name: permission } } } },
{ populate: ['roles', 'roles.role', 'roles.role.permissions'] },
);
return admins;
}
}
@@ -0,0 +1,22 @@
import type { Admin } from '../entities/admin.entity';
export interface AdminDetailResponse {
id: string;
firstName?: string;
lastName?: string;
phone: string;
role: {
id: string;
name: string;
};
permissions: string[];
restaurant?: {
id: string;
name: string;
slug: string;
};
}
// Extract permissions - ensure collection is loaded if needed
+45
View File
@@ -0,0 +1,45 @@
import { Module, forwardRef } from '@nestjs/common';
import { AuthService } from './services/auth.service';
import { AuthController } from './controllers/auth.controller';
import { UtilsModule } from '../utils/utils.module';
import { UserModule } from '../users/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AdminModule } from '../admin/admin.module';
import { TokensService } from './services/tokens.service';
import { RestaurantsModule } from '../restaurants/restaurants.module';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { User } from '../users/entities/user.entity';
import { AdminAuthGuard } from './guards/adminAuth.guard';
import { RefreshToken } from './entities/refresh-token.entity';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [
UtilsModule,
forwardRef(() => UserModule),
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => {
const expiresIn = configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
return {
global: true,
secret: configService.getOrThrow<string>('JWT_SECRET'),
signOptions: {
// Use string format with time unit for explicit expiration (value should be in seconds)
expiresIn: `${expiresIn}s`,
},
};
},
inject: [ConfigService],
}),
forwardRef(() => AdminModule),
forwardRef(() => RestaurantsModule),
MikroOrmModule.forFeature([User, RefreshToken]),
forwardRef(() => NotificationsModule),
],
controllers: [AuthController],
providers: [AuthService, TokensService, AdminAuthGuard],
exports: [AdminAuthGuard],
})
export class AuthModule {}
@@ -0,0 +1,71 @@
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
import { RequestOtpDto } from '../dto/request-otp.dto';
import { DirectLoginDto } from '../dto/direct-login.dto';
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
import { VerifyOtpDto } from '../dto/verify-otp.dto';
import { Throttle } from '@nestjs/throttler';
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard';
@ApiTags('auth')
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) { }
@Throttle({ default: { limit: 3, ttl: 180_000 } })
@Post('public/auth/otp/request')
@ApiOperation({ summary: 'Request OTP for login or signup' })
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
otpRequest(@Body() dto: RequestOtpDto) {
return this.authService.requestOtp(dto, false);
}
@Post('public/auth/otp/verify')
@ApiOperation({ summary: 'Verify OTP code' })
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
otpVerify(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
}
@RefreshTokenRateLimit()
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
@Post('public/auth/refresh')
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}
@Throttle({ default: { limit: 3, ttl: 180_000 } })
@Post('admin/auth/otp/request')
@ApiOperation({ summary: 'Request OTP for login or signup' })
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
otpRequestAdmin(@Body() dto: RequestOtpDto) {
return this.authService.requestOtp(dto, true);
}
@Post('admin/auth/otp/verify')
@ApiOperation({ summary: 'Verify OTP code' })
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp);
}
@RefreshTokenRateLimit()
@ApiOperation({ summary: 'refresh the admin access token / refresh token' })
@Post('admin/auth/refresh')
refreshTokenAdmin(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}
//super admin routes
@UseGuards(SuperAdminAuthGuard)
@Post('super-admin/auth/direct-login')
@ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
@ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
directloginForDsc(@Body() dto: DirectLoginDto) {
return this.authService.loginAdminForDsc(dto.phone, dto.slug);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class DirectLoginDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
@IsMobilePhone('fa-IR')
phone: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
slug: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class RefreshTokenDto {
@ApiProperty({ description: 'Refresh token' })
@IsString({ message: 'Refresh token must be a string' })
@IsNotEmpty({ message: 'Refresh token is required' })
refreshToken: string;
}
+15
View File
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RequestOtpDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
@IsMobilePhone('fa-IR')
phone: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
slug: string;
}
+21
View File
@@ -0,0 +1,21 @@
import { IsNotEmpty, IsString, Length, IsMobilePhone } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class VerifyOtpDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
@IsMobilePhone('fa-IR')
phone: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '', description: 'Otp' })
@Length(5)
otp: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
slug: string;
}
@@ -0,0 +1,29 @@
import { Entity, Enum, Index, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
export enum RefreshTokenType {
USER = 'user',
ADMIN = 'admin',
}
@Entity({ tableName: 'refreshtokens' })
@Index({ properties: ['ownerId', 'restId', 'type'] })
@Index({ properties: ['hashedToken'] })
@Index({ properties: ['expiresAt'] })
export class RefreshToken extends BaseEntity {
@Property({ type: 'varchar', length: 255 })
hashedToken!: string;
@Property()
ownerId!: string;
@Property()
restId!: string;
@Enum(() => RefreshTokenType)
type!: RefreshTokenType;
@Property({ type: 'timestamptz' })
expiresAt!: Date;
}
+120
View File
@@ -0,0 +1,120 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
Inject,
Logger,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { IAdminTokenPayload } from '../interfaces/IToken-payload';
import { PERMISSIONS_KEY } from '../../../common/decorators/permissions.decorator';
import { PermissionsService } from 'src/modules/roles/providers/permissions.service';
export interface AdminAuthRequest extends Request {
adminId: string;
restId: string;
}
@Injectable()
export class AdminAuthGuard implements CanActivate {
private readonly logger = new Logger(AdminAuthGuard.name);
constructor(
@Inject(JwtService)
private readonly jwtService: JwtService,
@Inject(ConfigService)
private readonly configService: ConfigService,
private readonly permissionsService: PermissionsService,
private readonly reflector: Reflector,
) { }
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<AdminAuthRequest>();
const token = this.extractTokenFromHeader(request);
if (!token) {
this.logger.warn('No token provided', {
hasAuthHeader: !!request.headers.authorization,
authHeader: request.headers.authorization ? 'present' : 'missing',
headers: Object.keys(request.headers),
});
throw new UnauthorizedException('No token provided');
}
try {
// Get the JWT secret from config
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
// Verify token with the secret
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
secret,
});
if (!payload.adminId || !payload.restId) {
this.logger.error('Invalid token payload structure', payload);
throw new UnauthorizedException('Invalid token payload');
}
request['adminId'] = payload.adminId;
request['restId'] = payload.restId;
// check if the user has the required permissions
const requiredPermissions =
this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]) ?? [];
if (!requiredPermissions || requiredPermissions.length === 0) {
return true;
}
const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.restId);
if (!adminPermission || !Array.isArray(adminPermission)) {
this.logger.error('No permissions found', { adminId: payload.adminId, restId: payload.restId });
throw new ForbiddenException('No permissions found');
}
const hasPermission = requiredPermissions.every(p => adminPermission.includes(p));
if (!hasPermission) {
this.logger.warn('Insufficient permissions', {
adminId: payload.adminId,
restId: payload.restId,
required: requiredPermissions,
has: adminPermission,
});
throw new ForbiddenException('You are not authorized to access this resource');
}
return true;
} catch (err) {
if (err instanceof ForbiddenException || err instanceof UnauthorizedException) {
throw err;
}
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
const errorStack = err instanceof Error ? err.stack : undefined;
this.logger.error('Token verification error in AdminAuthGuard', {
error: errorMessage,
stack: errorStack,
});
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const authHeader = request.headers.authorization || request.headers['authorization'];
if (!authHeader) {
return undefined;
}
const [type, token] = authHeader.split(' ');
if (type?.toLowerCase() !== 'bearer' || !token) {
return undefined;
}
return token;
}
}
+72
View File
@@ -0,0 +1,72 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ITokenPayload } from '../interfaces/IToken-payload';
export interface UserAuthRequest extends Request {
userId: string;
restId: string;
}
@Injectable()
export class AuthGuard implements CanActivate {
private readonly logger = new Logger(AuthGuard.name);
constructor(
@Inject(JwtService)
private readonly jwtService: JwtService,
@Inject(ConfigService)
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<UserAuthRequest>();
try {
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
const token = this.extractTokenFromHeader(request);
const slug = this.extractSlugFromHeader(request);
if (!slug) {
throw new UnauthorizedException('Slug is required');
}
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
secret,
});
if (!payload.userId || !payload.restId) {
this.logger.error('Invalid token payload structure', payload);
throw new UnauthorizedException('Invalid token payload');
}
if (payload.slug !== slug) {
throw new UnauthorizedException('Invalid slug');
}
request['userId'] = payload.userId;
request['restId'] = payload.restId;
} catch {
throw new UnauthorizedException();
}
return true;
} catch (err) {
if (err instanceof UnauthorizedException) {
throw err;
}
this.logger.error('error in AuthGuard', err);
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
private extractSlugFromHeader(request: Request): string | undefined {
// Fastify normalizes headers to lowercase, so check both cases
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
return slug;
}
}
@@ -0,0 +1,94 @@
import { CanActivate, ExecutionContext, Injectable, Inject, Logger } from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ITokenPayload } from '../interfaces/IToken-payload';
export interface UserOptionalAuthRequest extends Request {
userId?: string;
restId?: string;
slug?: string;
}
@Injectable()
export class OptionalAuthGuard implements CanActivate {
private readonly logger = new Logger(OptionalAuthGuard.name);
constructor(
@Inject(JwtService)
private readonly jwtService: JwtService,
@Inject(ConfigService)
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<UserOptionalAuthRequest>();
const token = this.extractTokenFromHeader(request);
const slug = this.extractSlugFromHeader(request);
// If no token is provided, allow the request without authentication
if (!slug) {
console.log('No slug provided');
this.logger.warn('No slug provided');
return false;
}
if (!token) {
request['slug'] = slug;
return true;
}
// Try to verify the token if it exists
try {
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
secret,
});
// Validate payload structure
if (!payload.userId || !payload.restId) {
this.logger.warn('Invalid token payload structure', payload);
// Allow request but don't set user info
request['slug'] = slug;
return true;
}
// Validate slug if provided
if (slug && payload.slug !== slug) {
this.logger.warn('Token slug does not match header slug', {
tokenSlug: payload.slug,
headerSlug: slug,
});
// Allow request but don't set user info
request['slug'] = slug;
return true;
}
// Token is valid, set user info
request['userId'] = payload.userId;
request['restId'] = payload.restId;
request['slug'] = slug;
return true;
} catch (err) {
// Token is invalid or expired, but allow the request anyway
this.logger.debug('Token verification failed in OptionalAuthGuard', {
error: err instanceof Error ? err.message : 'Unknown error',
});
// Set restId from slug
request['slug'] = slug;
return true;
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
private extractSlugFromHeader(request: Request): string | undefined {
// Fastify normalizes headers to lowercase, so check both cases
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
return slug;
}
}
@@ -0,0 +1,66 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class SuperAdminAuthGuard implements CanActivate {
private readonly logger = new Logger(SuperAdminAuthGuard.name);
constructor(
@Inject(ConfigService)
private readonly configService: ConfigService,
) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
try {
const credentials = this.extractBasicAuthCredentials(request);
if (!credentials) {
throw new UnauthorizedException('Basic authentication required');
}
const { username, password } = credentials;
const expectedUsername = this.configService.get<string>('SUPER_ADMIN_USERNAME') ?? 'danak@dsc.com';
const expectedPassword = this.configService.get<string>('SUPER_ADMIN_PASSWORD') ?? 'DsCdAnAk?@ABC';
if (username !== expectedUsername || password !== expectedPassword) {
this.logger.warn(`Invalid super admin credentials attempt for username: ${username}`);
throw new UnauthorizedException('Invalid credentials');
}
return true;
} catch (err) {
if (err instanceof UnauthorizedException) {
throw err;
}
this.logger.error('error in SuperAdminAuthGuard', err);
throw new UnauthorizedException('Authentication failed');
}
}
private extractBasicAuthCredentials(request: Request): { username: string; password: string } | null {
const authHeader = request.headers.authorization;
if (!authHeader) {
return null;
}
const [type, encoded] = authHeader.split(' ');
if (type?.toLowerCase() !== 'basic' || !encoded) {
return null;
}
try {
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
const [username, password] = decoded.split(':');
if (!username || !password) {
return null;
}
return { username, password };
} catch (error) {
this.logger.error('Failed to decode Basic Auth credentials', error);
return null;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
export interface ITokenPayload {
userId: string;
restId: string;
slug: string;
}
export interface IAdminTokenPayload {
adminId: string;
restId: string;
}
+144
View File
@@ -0,0 +1,144 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { RequestOtpDto } from '../dto/request-otp.dto';
import { CacheService } from '../../utils/cache.service';
import { SmsService } from '../../notifications/services/sms.service';
import { UserService } from '../../users/providers/user.service';
import { randomInt } from 'crypto';
import { TokensService } from './tokens.service';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
import { UserLoginTransformer } from '../transformers/user-login.transformer';
import { ConfigService } from '@nestjs/config';
import { normalizePhone } from '../../utils/phone.util';
@Injectable()
export class AuthService {
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
private OTP_EXPIRATION_TIME: number;
constructor(
private readonly cacheService: CacheService,
private readonly smsService: SmsService,
private readonly userService: UserService,
private readonly tokensService: TokensService,
private readonly restRepository: RestRepository,
private readonly adminRepository: AdminRepository,
private readonly configService: ConfigService,
) {
this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240;
}
private userOtpKey(restaurantSlug: string, phone: string) {
return `otp:${restaurantSlug}:${phone}`;
}
private adminOtpKey(restaurantSlug: string, phone: string) {
return `otp-admin:${restaurantSlug}:${phone}`;
}
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
const { phone, slug } = dto;
const normalizedPhone = normalizePhone(phone);
if (isAdmin) {
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
if (!admin) {
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
}
}
const code = this.generateOtpCode();
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
await this.smsService.sendotp(normalizedPhone, code);
return { code: null };
}
async verifyOtp(phone: string, slug: string, code: string) {
const normalizedPhone = normalizePhone(phone);
const key = this.userOtpKey(slug, normalizedPhone);
const cachedCode = await this.cacheService.get(key);
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
await this.cacheService.del(key);
const user = await this.userService.findOrCreateByPhone(normalizedPhone);
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug);
const userResponse = UserLoginTransformer.transform(user, rest);
return { tokens, user: userResponse };
}
async verifyOtpAdmin(phone: string, slug: string, code: string) {
const normalizedPhone = normalizePhone(phone);
const key = this.adminOtpKey(slug, normalizedPhone);
const cachedCode = await this.cacheService.get(key);
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
await this.cacheService.del(key);
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
if (!admin) {
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
}
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
return { tokens, admin: adminResponse };
}
/**
*
to use for login directly from DSC
*/
async loginAdminForDsc(phone: string, slug: string) {
const normalizedPhone = normalizePhone(phone);
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
if (!admin) {
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
}
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
return { tokens, admin: adminResponse };
}
private generateOtpCode(): string {
const code = randomInt(10000, 100000);
return code.toString();
}
refreshToken(oldRefreshToken: string) {
return this.tokensService.refreshToken(oldRefreshToken);
}
}
+145
View File
@@ -0,0 +1,145 @@
import { createHash, randomBytes } from 'node:crypto';
import { EntityManager } from '@mikro-orm/postgresql';
import { LockMode } from '@mikro-orm/core';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import dayjs from 'dayjs';
import { AuthMessage } from '../../../common/enums/message.enum';
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
@Injectable()
export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
) {}
async generateAccessAndRefreshToken(
ownerId: string,
restaurantId: string,
isAdmin: boolean,
slug: string,
em?: EntityManager,
) {
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
? { adminId: ownerId, restId: restaurantId }
: { userId: ownerId, restId: restaurantId, slug };
const accessToken = await this.generateAccessToken(payload, accessExpire);
const refreshToken = this.generateRefreshToken();
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER;
// Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
};
}
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expiresIn: number) {
// Ensure expiresIn is passed as a string with time unit for reliability
// JWT library accepts: number (seconds) or string with unit (e.g., "3600s", "1h")
// Using string format is more explicit and prevents unit confusion
return this.jwtService.signAsync(payload, { expiresIn: `${expiresIn}s` });
}
async storeRefreshToken(
ownerId: string,
restId: string,
refreshToken: string,
type: RefreshTokenType,
em?: EntityManager,
) {
const entityManager = em || this.em;
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
const hashedToken = this.hashToken(refreshToken);
const token = entityManager.create(RefreshToken, {
hashedToken,
ownerId,
restId,
type,
expiresAt,
});
if (em) {
// Within transaction, just persist (flush will be called by transaction)
entityManager.persist(token);
} else {
// Outside transaction, persist and flush immediately
await entityManager.persistAndFlush(token);
}
}
async refreshToken(oldRefreshToken: string) {
const hashedToken = this.hashToken(oldRefreshToken);
// Use transaction to ensure atomicity and prevent race conditions
return this.em.transactional(async em => {
// Lock the token row to prevent concurrent refresh attempts
// Using pessimistic write lock (FOR UPDATE) to prevent race conditions
const token = await em.findOne(RefreshToken, { hashedToken }, { lockMode: LockMode.PESSIMISTIC_WRITE });
if (!token) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
// Check expiration
if (dayjs(token.expiresAt).isBefore(dayjs())) {
em.remove(token);
await em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Store token data before removal
const ownerId = token.ownerId;
const restId = token.restId;
const isAdmin = token.type === RefreshTokenType.ADMIN;
// Verify restaurant still exists
const restaurant = await em.findOne(Restaurant, { id: restId });
if (!restaurant) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
try {
// Generate new tokens first (before removing old token)
// Pass the transaction EntityManager to ensure operations are within the transaction
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
// Remove old token only after new token is created
em.remove(token);
// Persist changes atomically
await em.flush();
return tokens;
} catch (error) {
this.logger.error('Failed to generate new tokens after refresh', error);
// Transaction will rollback automatically, preserving the old token
throw new UnauthorizedException('Failed to refresh token');
}
});
}
private generateRefreshToken() {
return randomBytes(32).toString('hex');
}
private hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
}
@@ -0,0 +1,93 @@
import type { Admin } from '../../admin/entities/admin.entity';
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
export interface AdminLoginResponse {
id: string;
firstName?: string;
lastName?: string;
phone: string;
role: string;
permissions: string[];
restaurant?: {
id: string;
name: string;
slug: string;
};
}
export class AdminLoginTransformer {
static async transform(admin: Admin, restaurant?: Restaurant): Promise<AdminLoginResponse> {
// Find the AdminRole that matches the restaurant (or get the first one)
let adminRole = admin.roles.getItems().find(r => (restaurant ? r.restaurant?.id === restaurant.id : true));
// If no match found, get the first one
if (!adminRole && admin.roles.getItems().length > 0) {
adminRole = admin.roles.getItems()[0];
}
if (!adminRole) {
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: '',
permissions: [],
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
};
}
// Get the role from AdminRole
const role = adminRole.role;
if (!role) {
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: '',
permissions: [],
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
};
}
// Extract permissions - ensure collection is loaded if needed
let permissions: string[] = [];
if (role.permissions) {
if (role.permissions.isInitialized()) {
permissions = role.permissions.getItems().map(p => p.name);
} else {
await role.permissions.loadItems();
permissions = role.permissions.getItems().map(p => p.name);
}
}
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: role.name,
permissions,
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
};
}
}
@@ -0,0 +1,33 @@
import type { User } from '../../users/entities/user.entity';
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
export interface UserLoginResponse {
id: string;
firstName: string;
lastName?: string;
phone: string;
isActive?: boolean;
restaurant: {
id: string;
name: string;
slug: string;
};
}
export class UserLoginTransformer {
static transform(user: User, restaurant: Restaurant): UserLoginResponse {
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
isActive: user.isActive,
restaurant: {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
},
};
}
}
+53
View File
@@ -0,0 +1,53 @@
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { CartService } from './providers/cart.service';
import { CartController } from './controllers/cart.controller';
import { Food } from '../foods/entities/food.entity';
import { Restaurant } from '../restaurants/entities/restaurant.entity';
import { PaymentMethod } from '../payments/entities/payment-method.entity';
import { UserAddress } from '../users/entities/user-address.entity';
import { User } from '../users/entities/user.entity';
import { Delivery } from '../delivery/entities/delivery.entity';
import { Order } from '../orders/entities/order.entity';
import { AuthModule } from '../auth/auth.module';
import { UserModule } from '../users/user.module';
import { JwtModule } from '@nestjs/jwt';
import { UtilsModule } from '../utils/utils.module';
import { CouponModule } from '../coupons/coupon.module';
import { CartRepository } from './repositories/cart.repository';
import { CartValidationService } from './providers/cart-validation.service';
import { CartCalculationService } from './providers/cart-calculation.service';
import { CartItemService } from './providers/cart-item.service';
import { PointTransaction } from '../users/entities/point-transaction.entity';
import { WalletTransaction } from '../users/entities/wallet-transaction.entity';
@Module({
imports: [
MikroOrmModule.forFeature([
Food,
Restaurant,
PaymentMethod,
UserAddress,
User,
PointTransaction,
WalletTransaction,
Delivery,
Order,
]),
AuthModule,
UserModule,
JwtModule,
UtilsModule,
CouponModule,
],
controllers: [CartController],
providers: [
CartService,
CartRepository,
CartValidationService,
CartCalculationService,
CartItemService,
],
exports: [CartService],
})
export class CartModule { }
@@ -0,0 +1,92 @@
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
import { CartService } from '../providers/cart.service';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
import { AuthGuard } from '../../auth/guards/auth.guard';
import { UserId } from 'src/common/decorators/user-id.decorator';
import { RestId } from 'src/common/decorators/rest-id.decorator';
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
import { API_HEADER_SLUG } from 'src/common/constants/index';
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiTags('cart')
@Controller('public/cart')
export class CartController {
constructor(private readonly cartService: CartService) {}
@Get()
@ApiOperation({ summary: 'Get cart for current user and restaurant' })
@ApiHeader(API_HEADER_SLUG)
async findOne(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.getOrCreateCart(userId, restaurantId);
}
@Post('items/:foodId/increment')
@ApiOperation({ summary: 'Increment item quantity in cart by 1' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.incrementItem(userId, restaurantId, foodId, 1);
}
@Post('items/:foodId/decrement')
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' })
async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.decrementItem(userId, restaurantId, foodId);
}
@Post('items/bulk')
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: BulkAddItemsToCartDto })
bulkAddItems(
@UserId() userId: string,
@RestId() restaurantId: string,
@Body() bulkAddItemsDto: BulkAddItemsToCartDto,
) {
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
}
@Delete('items/:foodId')
@ApiOperation({ summary: 'Remove item from cart' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.removeItem(userId, restaurantId, foodId);
}
@Delete()
@ApiOperation({ summary: 'Clear entire cart' })
@ApiHeader(API_HEADER_SLUG)
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.clearCart(userId, restaurantId);
}
@Post('apply-coupon')
@ApiOperation({ summary: 'Apply coupon to cart' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: ApplyCouponDto })
async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
}
@Delete('coupon')
@ApiOperation({ summary: 'Remove coupon from cart' })
@ApiHeader(API_HEADER_SLUG)
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.removeCoupon(userId, restaurantId);
}
@Patch('all')
@ApiOperation({ summary: 'Set all cart params' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetAllCartParmsDto })
setAllCartParams(@UserId() userId: string, @RestId() restaurantId: string, @Body() dto: SetAllCartParmsDto) {
return this.cartService.setAllCartParams(userId, restaurantId, dto);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class ApplyCouponDto {
@ApiProperty({ description: 'Coupon code', example: 'DISCOUNT10' })
@IsNotEmpty()
@IsString()
code!: string;
}
@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, Min, IsString } from 'class-validator';
import { Type } from 'class-transformer';
class AddItemToCartDto {
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
@IsNotEmpty()
@Type(() => Number)
@IsInt()
@Min(1)
quantity!: number;
@ApiProperty({ description: 'Food ID' })
@IsNotEmpty()
@IsString()
foodId!: string;
}
export class BulkAddItemsToCartDto {
@ApiProperty({
description: 'Array of items to add to cart',
type: [AddItemToCartDto],
example: [
{ foodId: 'food-123', quantity: 2 },
{ foodId: 'food-456', quantity: 1 },
],
})
@IsNotEmpty()
@IsArray()
@ArrayMinSize(1, { message: 'At least one item is required' })
@ValidateNested({ each: true })
@Type(() => AddItemToCartDto)
items!: AddItemToCartDto[];
}
+29
View File
@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsArray, ValidateNested, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';
export class CartItemDto {
@ApiProperty({ description: 'Food ID' })
@IsNotEmpty()
@IsString()
foodId!: string;
@ApiProperty({ description: 'Quantity of the food item', example: 2, minimum: 1 })
@IsNotEmpty()
@Type(() => Number)
@IsInt()
@Min(1)
quantity!: number;
}
export class CreateCartDto {
@ApiProperty({
description: 'List of cart items',
type: [CartItemDto],
})
@IsNotEmpty()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CartItemDto)
items!: CartItemDto[];
}
@@ -0,0 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
import { SetCarDeliveryDto } from './set-car-delivery.dto';
export class SetAllCartParmsDto {
@ApiProperty({
description: 'Cart description or notes',
required: false,
example: 'Please deliver to the back door',
})
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ description: 'Delivery method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsNotEmpty()
@IsString()
deliveryMethodId!: string;
@ApiProperty({ description: 'User address ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsOptional()
@IsString()
addressId?: string;
@ApiProperty({ description: 'Payment method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsNotEmpty()
@IsString()
paymentMethodId!: string;
@ApiProperty({ description: 'Table number for dine-in orders', example: '5', required: false })
@IsOptional()
@IsString()
tableNumber?: string;
@ApiProperty()
@IsOptional()
@IsObject()
carAddress?: SetCarDeliveryDto;
}
@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class SetCarDeliveryDto {
@ApiProperty({ description: 'Car model', example: 'Toyota Camry' })
@IsNotEmpty()
@IsString()
carModel!: string;
@ApiProperty({ description: 'Car color', example: 'White' })
@IsNotEmpty()
@IsString()
carColor!: string;
@ApiProperty({ description: 'License plate number', example: '12ABC345' })
@IsNotEmpty()
@IsString()
plateNumber!: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { PartialType, ApiProperty } from '@nestjs/swagger';
import { CreateCartDto, CartItemDto } from './create-cart.dto';
export class UpdateCartItemDto extends PartialType(CartItemDto) {}
export class UpdateCartDto {
@ApiProperty({ description: 'List of cart items to update', type: [UpdateCartItemDto], required: false })
items?: UpdateCartItemDto[];
}
@@ -0,0 +1,38 @@
import type { OrderCouponDetail, OrderUserAddress, OrderCarAddress } from 'src/modules/orders/interface/order.interface';
export interface CartItem {
foodId: string;
foodTitle?: string;
quantity: number;
price: number;
discount: number;
totalPrice: number;
}
export interface Cart {
userId: string;
restaurantId: string;
restaurantName?: string;
items: CartItem[];
coupon?: OrderCouponDetail;
paymentMethodId?: string;
deliveryMethodId?: string;
description?: string;
tableNumber?: string;
deliveryFee: number;
subTotal: number;
tax: number;
couponDiscount: number;
itemsDiscount: number;
totalDiscount: number;
total: number;
carAddress?: OrderCarAddress | null;
userAddress?: OrderUserAddress | null;
totalItems: number;
createdAt: string;
updatedAt: string;
}
@@ -0,0 +1,244 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { DeliveryFeeTypeEnum } from '../../delivery/interface/delivery';
import { CouponType } from '../../coupons/interface/coupon';
import { Food } from '../../foods/entities/food.entity';
import { Cart } from '../interfaces/cart.interface';
import { CouponService } from '../../coupons/providers/coupon.service';
import { GeographicUtils } from '../utils/geographic.utils';
@Injectable()
export class CartCalculationService {
constructor(
private readonly em: EntityManager,
private readonly couponService: CouponService,
) {}
/**
* Calculate total price for a cart item
*/
calculateItemTotalPrice(unitPrice: number, unitDiscount: number, quantity: number): number {
const safeUnitPrice = Number(unitPrice) || 0;
const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice);
const itemTotalPrice = safeUnitPrice * quantity;
const itemTotalDiscount = safeUnitDiscount * quantity;
return itemTotalPrice - itemTotalDiscount;
}
/**
* Calculate items totals (subtotal, items discount, total items)
*/
calculateItemsTotals(cart: Cart): { subTotal: number; itemsDiscount: number; totalItems: number } {
let subTotal = 0;
let itemsDiscount = 0;
let totalItems = 0;
for (const item of cart.items) {
const unitPrice = Number(item.price) || 0;
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
const itemPrice = unitPrice * item.quantity;
const itemDiscount = unitDiscount * item.quantity;
subTotal += itemPrice;
itemsDiscount += itemDiscount;
totalItems += item.quantity;
item.totalPrice = itemPrice - itemDiscount;
}
return { subTotal, itemsDiscount, totalItems };
}
/**
* Calculate coupon discount
*/
async calculateCouponDiscount(cart: Cart, subTotal: number, itemsDiscount: number): Promise<number> {
if (!cart.coupon) return 0;
const coupon = await this.getCouponRestrictionsOrClear(cart);
if (!cart.coupon || !coupon) return 0;
const hasCategoryRestriction = (coupon.foodCategories?.length ?? 0) > 0;
const hasFoodRestriction = (coupon.foods?.length ?? 0) > 0;
let eligibleItemsTotal = subTotal;
let eligibleItemsDiscount = itemsDiscount;
if (hasCategoryRestriction || hasFoodRestriction) {
eligibleItemsTotal = 0;
eligibleItemsDiscount = 0;
const foodMap = await this.getFoodsInCartWithCategories(cart);
for (const item of cart.items) {
const food = foodMap.get(item.foodId);
if (!food) continue;
const matchesCategory =
hasCategoryRestriction && food.category && (coupon.foodCategories ?? []).includes(food.category.id);
const matchesFood = hasFoodRestriction && (coupon.foods ?? []).includes(food.id);
if (matchesCategory || matchesFood) {
const unitPrice = Number(item.price) || 0;
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
eligibleItemsTotal += unitPrice * item.quantity;
eligibleItemsDiscount += unitDiscount * item.quantity;
}
}
}
const priceAfterItemDiscount = Math.max(0, eligibleItemsTotal - eligibleItemsDiscount);
if (cart.coupon.type === CouponType.PERCENTAGE) {
let discount = (priceAfterItemDiscount * cart.coupon.value) / 100;
if (cart.coupon.maxDiscount && discount > cart.coupon.maxDiscount) {
discount = cart.coupon.maxDiscount;
}
return discount;
}
return Math.min(cart.coupon.value, priceAfterItemDiscount);
}
/**
* Calculate tax
*/
async calculateTax(restaurantId: string, amountAfterDiscounts: number): Promise<number> {
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
const vat = restaurant?.vat ? Number(restaurant.vat) : 0;
if (!vat || vat <= 0) return 0;
return (Math.max(0, amountAfterDiscounts) * vat) / 100;
}
/**
* Calculate delivery fee
*/
async calculateDeliveryFee(cart: Cart): Promise<number> {
const deliveryMethodId = cart.deliveryMethodId;
if (!deliveryMethodId) return 0;
const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId }, { populate: ['restaurant'] });
if (!deliveryMethod || !deliveryMethod.enabled) return 0;
// If not distance based, return fixed delivery fee
if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) {
return Number(deliveryMethod.deliveryFee) || 0;
}
// For distance based calculation we need restaurant and user coordinates
const restaurant = (deliveryMethod as any).restaurant as Restaurant | undefined;
const userAddr = cart.userAddress;
if (!restaurant || restaurant.latitude == null || restaurant.longitude == null) {
// fallback to configured fixed fee when coordinates are missing
return Number(deliveryMethod.deliveryFee) || 0;
}
if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) {
return Number(deliveryMethod.deliveryFee) || 0;
}
const restLat = Number(restaurant.latitude);
const restLng = Number(restaurant.longitude);
const userLat = Number(userAddr.latitude);
const userLng = Number(userAddr.longitude);
const distanceKm = GeographicUtils.getDistanceKmRounded(restLat, restLng, userLat, userLng);
// Try to read either possible property names (legacy vs new)
const perKm = Number(deliveryMethod.perKilometerFee);
const minFee = Number(deliveryMethod.distanceBasedMinCost);
let fee = 0;
if (perKm <= 0) {
fee = Number(deliveryMethod.deliveryFee) || 0;
} else {
fee = distanceKm * perKm;
}
if (minFee > 0 && fee < minFee) fee = minFee;
return Math.max(0, Number(fee));
}
/**
* Recalculate cart totals (including coupon discount and tax)
*/
async recalculateCartTotals(cart: Cart): Promise<void> {
const { subTotal, itemsDiscount, totalItems } = this.calculateItemsTotals(cart);
cart.subTotal = subTotal;
cart.itemsDiscount = itemsDiscount;
const couponDiscount = await this.calculateCouponDiscount(cart, subTotal, itemsDiscount);
cart.couponDiscount = couponDiscount;
cart.totalDiscount = couponDiscount + itemsDiscount;
cart.tax = await this.calculateTax(cart.restaurantId, Math.max(0, subTotal - cart.totalDiscount));
cart.deliveryFee = await this.calculateDeliveryFee(cart);
// total = subtotal totalDiscount + tax + deliveryFee
cart.total = Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee;
cart.totalItems = totalItems;
cart.updatedAt = this.nowIso();
}
/**
* Get coupon restrictions or clear if invalid
*/
private async getCouponRestrictionsOrClear(cart: Cart): Promise<{
isActive?: boolean;
startDate?: Date;
endDate?: Date;
foodCategories?: string[];
foods?: string[];
} | null> {
if (!cart.coupon) return null;
try {
const c = await this.couponService.findById(cart.coupon.couponId);
const now = new Date();
if (c.isActive === false) {
cart.coupon = undefined;
return null;
}
if (c.startDate && now < c.startDate) {
cart.coupon = undefined;
return null;
}
if (c.endDate && now > c.endDate) {
cart.coupon = undefined;
return null;
}
return {
isActive: c.isActive,
startDate: c.startDate,
endDate: c.endDate,
foodCategories: c.foodCategories,
foods: c.foods,
};
} catch {
cart.coupon = undefined;
return null;
}
}
/**
* Get foods in cart with categories
*/
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Food>> {
const foodIds = cart.items.map(item => item.foodId);
if (foodIds.length === 0) return new Map();
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
return new Map(foods.map(f => [f.id, f]));
}
/**
* Get current ISO timestamp
*/
private nowIso(): string {
return new Date().toISOString();
}
}
@@ -0,0 +1,113 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Food } from '../../foods/entities/food.entity';
import { Cart, CartItem } from '../interfaces/cart.interface';
import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service';
import { CartMessage } from 'src/common/enums/message.enum';
@Injectable()
export class CartItemService {
constructor(
private readonly validationService: CartValidationService,
private readonly calculationService: CartCalculationService,
) {}
/**
* Create a new cart item from food
*/
createCartItem(food: Food, quantity: number): CartItem {
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
return {
foodId: food.id,
foodTitle: food.title,
quantity,
price: itemPrice,
discount: itemDiscount,
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
};
}
/**
* Build cart item from food (updating existing item if provided)
*/
buildCartItemFromFood(food: Food, quantity: number, previous?: CartItem): CartItem {
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
return {
...(previous ?? ({} as CartItem)),
foodId: food.id,
foodTitle: food.title,
quantity,
price: itemPrice,
discount: itemDiscount,
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
};
}
/**
* Get item index in cart
*/
getItemIndex(cart: Cart, foodId: string): number {
return cart.items.findIndex(item => item.foodId === foodId);
}
/**
* Add or increment item in cart
*/
async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
const food = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd);
// Validate meal type compatibility
this.validationService.assertMealTypeCompatibility(food);
// Validate weekday compatibility
this.validationService.assertWeekdayCompatibility(food);
const index = this.getItemIndex(cart, food.id);
if (index < 0) {
cart.items.push(this.createCartItem(food, quantityToAdd));
return;
}
const existingItem = cart.items[index];
const newQuantity = existingItem.quantity + quantityToAdd;
this.validationService.validateStock(food, newQuantity);
cart.items[index] = this.buildCartItemFromFood(food, newQuantity, existingItem);
}
/**
* Remove item from cart
*/
removeItemOrFail(cart: Cart, foodId: string): void {
const itemIndex = this.getItemIndex(cart, foodId);
if (itemIndex < 0) {
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
}
cart.items.splice(itemIndex, 1);
}
/**
* Decrement item quantity or remove if quantity reaches 0
*/
async decrementOrRemoveItem(cart: Cart, foodId: string): Promise<void> {
const itemIndex = this.getItemIndex(cart, foodId);
if (itemIndex < 0) {
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
}
const existingItem = cart.items[itemIndex];
const newQuantity = existingItem.quantity - 1;
if (newQuantity <= 0) {
cart.items.splice(itemIndex, 1);
return;
}
const food = await this.validationService.getFoodOrFail(foodId);
cart.items[itemIndex] = this.buildCartItemFromFood(food, newQuantity, existingItem);
}
}
@@ -0,0 +1,422 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { Food } from '../../foods/entities/food.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { UserAddress } from '../../users/entities/user-address.entity';
import { User } from '../../users/entities/user.entity';
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
import { PointTransaction } from '../../users/entities/point-transaction.entity';
import { Order } from '../../orders/entities/order.entity';
import { OrderStatus } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { GeographicUtils } from '../utils/geographic.utils';
import { MealType } from '../../foods/interface/food.interface';
import { CartMessage } from 'src/common/enums/message.enum';
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';
@Injectable()
export class CartValidationService {
constructor(
private readonly em: EntityManager,
private readonly walletTransactionRepository: WalletTransactionRepository,
) { }
/**
* Validate food exists, belongs to restaurant, has inventory, and check stock
*/
async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) {
throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
}
if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
}
if (!food.inventory) {
throw new BadRequestException(CartMessage.FOOD_NO_INVENTORY);
}
this.validateStock(food, quantity);
return food;
}
/**
* Validate stock availability for food
*/
validateStock(food: Food, quantity: number): void {
const availableStock = food.inventory!.availableStock;
if (availableStock < quantity) {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title);
}
}
/**
* Get user or throw if not found
*/
async getUserOrFail(userId: string): Promise<User> {
const user = await this.em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException(CartMessage.USER_NOT_FOUND);
}
return user;
}
/**
* Get user address or throw if not found
*/
async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
if (!address) {
throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND);
}
return address;
}
/**
* Get food or throw if not found
*/
async getFoodOrFail(foodId: string): Promise<Food> {
const food = await this.em.findOne(Food, { id: foodId });
if (!food) {
throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
}
return food;
}
/**
* Get restaurant or throw if not found
*/
async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
if (!restaurant) {
throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND);
}
return restaurant;
}
/**
* Validate address belongs to user
*/
validateAddressOwnership(address: UserAddress, userId: string): void {
if (address.user.id !== userId) {
throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER);
}
}
/**
* Assert address is inside restaurant service area
*/
async assertAddressInsideServiceArea(
restaurantId: string,
latitude?: number | null,
longitude?: number | null,
): Promise<void> {
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES);
}
const restaurant = await this.getRestaurantOrFail(restaurantId);
const serviceArea = restaurant.serviceArea;
// If no service area is defined, assume service is available everywhere
if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return;
// GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring
const rings = serviceArea.coordinates;
if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return;
const ring = rings[0];
const point: [number, number] = [Number(longitude), Number(latitude)];
// Ensure polygon has the correct tuple typing ([lng, lat] pairs)
const polygon: [number, number][] = ring.map(
coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number],
);
if (!GeographicUtils.isPointInPolygon(point, polygon)) {
throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA);
}
}
/**
* Get delivery method for restaurant or throw if not found
*/
async getDeliveryMethodForRestaurantOrFail(
restaurantId: string,
deliveryMethodId: string,
): Promise<Delivery> {
const deliveryMethod = await this.em.findOne(Delivery, {
id: deliveryMethodId,
restaurant: { id: restaurantId },
});
if (!deliveryMethod) {
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT);
}
return deliveryMethod;
}
/**
* Get enabled delivery method or throw if not found or disabled
*/
async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise<Delivery> {
const deliveryMethod = await this.em.findOne(
Delivery,
{ id: deliveryMethodId, restaurant: { id: restaurantId } },
{ populate: ['restaurant'] },
);
if (!deliveryMethod) {
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND);
}
if (!deliveryMethod.enabled) {
throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
}
return deliveryMethod;
}
/**
* Assert delivery method matches expected type
*/
assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void {
if (actual !== expected) {
throw new BadRequestException(message);
}
}
/**
* Require delivery method ID or throw
*/
requireDeliveryMethodId(cart: Cart, message: string): string {
if (!cart.deliveryMethodId) {
throw new BadRequestException(message);
}
return cart.deliveryMethodId;
}
/**
* Get enabled payment method or throw if not found or disabled
*/
async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise<PaymentMethod> {
const paymentMethod = await this.em.findOne(
PaymentMethod,
{ id: paymentMethodId, restaurant: { id: restaurantId } },
{ populate: ['restaurant'] },
);
if (!paymentMethod) {
throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND);
}
if (!paymentMethod.enabled) {
throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
}
return paymentMethod;
}
/**
* Assert wallet has enough balance
*/
async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId);
if (balance < amount) {
throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
}
}
/**
* Assert coupon usage limit
*/
async assertCouponUsageLimit(userId: string, couponId: string, maxUsesPerUser?: number): Promise<void> {
if (!maxUsesPerUser) return;
const ordersThatUsedTheCouponCount = await this.em.count(Order, {
user: { id: userId },
couponDetail: { couponId: couponId },
status: { $ne: OrderStatus.CANCELED },
deletedAt: null,
});
if (ordersThatUsedTheCouponCount >= maxUsesPerUser) {
throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED);
}
}
/**
* Assert coupon matches cart if restricted
*/
async assertCouponMatchesCartIfRestricted(
cart: Cart,
foodCategories?: string[] | null,
foods?: string[] | null,
): Promise<void> {
const categoryRestriction = foodCategories?.filter(Boolean) ?? [];
const foodRestriction = foods?.filter(Boolean) ?? [];
const hasCategoryRestriction = categoryRestriction.length > 0;
const hasFoodRestriction = foodRestriction.length > 0;
if (!hasCategoryRestriction && !hasFoodRestriction) return;
const foodMap = await this.getFoodsInCartWithCategories(cart);
for (const item of cart.items) {
const food = foodMap.get(item.foodId);
if (!food) continue;
const matchesCategory = hasCategoryRestriction && food.category && categoryRestriction.includes(food.category.id);
const matchesFood = hasFoodRestriction && foodRestriction.includes(food.id);
if (matchesCategory || matchesFood) return;
}
throw new BadRequestException(CartMessage.COUPON_CANNOT_BE_APPLIED);
}
/**
* Get foods in cart with categories
*/
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Food>> {
const foodIds = cart.items.map(item => item.foodId);
if (foodIds.length === 0) return new Map();
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
return new Map(foods.map(f => [f.id, f]));
}
/**
* Assert all foods in cart have pickupServe true when delivery method is courier
*/
async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise<void> {
if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) {
return;
}
if (cart.items.length === 0) {
return;
}
const foodIds = cart.items.map(item => item.foodId);
const foods = await this.em.find(Food, { id: { $in: foodIds } });
const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe);
if (foodsWithoutPickupServe.length > 0) {
const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', ');
throw new BadRequestException(
CartMessage.FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER.replace('[foodTitles]', foodTitles),
);
}
}
/**
* Get current Iran time context (weekday and meal type)
*/
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
const currentHour = nowInIran.getHours();
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
const iranWeekDay = (weekDay + 1) % 7;
const mealType = this.getMealTypeByHour(currentHour);
return { iranWeekDay, mealType };
}
/**
* Determine meal type based on current hour in Iran timezone.
* @param hour - Current hour (0-23)
* @returns Meal type or null if outside meal hours
*/
private getMealTypeByHour(hour: number): MealType | null {
const MEAL_TIME_RANGES = {
BREAKFAST: { start: 6, end: 11 },
LUNCH: { start: 11, end: 15 },
SNACK: { start: 15, end: 19 },
DINNER: { start: 19, end: 24 },
} as const;
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
return MealType.BREAKFAST;
}
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
return MealType.LUNCH;
}
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
return MealType.SNACK;
}
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
return MealType.DINNER;
}
return null;
}
/**
* Assert meal type compatibility - if food has mealTypes, current meal time must be in that array
*/
assertMealTypeCompatibility(food: Food): void {
// If food has no meal type restrictions, allow it
if (!food.mealTypes || food.mealTypes.length === 0) {
return;
}
const { mealType } = this.getCurrentIranTimeContext();
// If current time is outside meal hours, throw error
if (mealType === null) {
const foodTitle = food.title || food.id;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES.replace('[foodTitle]', foodTitle),
);
}
// Check if current meal type is in food's allowed meal types
if (!food.mealTypes.includes(mealType)) {
const foodTitle = food.title || food.id;
const mealTypeMap: Record<MealType, string> = {
[MealType.BREAKFAST]: 'صبحانه',
[MealType.LUNCH]: 'ناهار',
[MealType.SNACK]: 'عصرانه',
[MealType.DINNER]: 'شام',
};
const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', ');
const currentMealType = mealType ? mealTypeMap[mealType] : mealType;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES.replace('[foodTitle]', foodTitle)
.replace('[allowedMealTypes]', allowedMealTypes)
.replace('[mealType]', currentMealType),
);
}
}
/**
* Assert weekday compatibility - current weekday must be in food's weekDays array
*/
assertWeekdayCompatibility(food: Food): void {
// If food has no weekday restrictions (empty array or all days), allow it
if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) {
return;
}
const { iranWeekDay } = this.getCurrentIranTimeContext();
// Check if current weekday is in food's allowed weekdays
if (!food.weekDays.includes(iranWeekDay)) {
const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'];
const currentDayName = dayNames[iranWeekDay];
const allowedDays = food.weekDays.map(day => dayNames[day]).join(', ');
const foodTitle = food.title || food.id;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_ON_DAYS.replace('[foodTitle]', foodTitle)
.replace('[allowedDays]', allowedDays)
.replace('[currentDay]', currentDayName),
);
}
}
}
+487
View File
@@ -0,0 +1,487 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
import { PaymentMethodEnum } from '../../payments/interface/payment';
import { OrderCouponDetail } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
import { CouponService } from '../../coupons/providers/coupon.service';
import { CartRepository } from '../repositories/cart.repository';
import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service';
import { CartItemService } from './cart-item.service';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
import { CartMessage } from 'src/common/enums/message.enum';
@Injectable()
export class CartService {
constructor(
private readonly cartRepository: CartRepository,
private readonly validationService: CartValidationService,
private readonly calculationService: CartCalculationService,
private readonly itemService: CartItemService,
private readonly couponService: CouponService,
) { }
/**
* Set all cart parameters at once
*/
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;
// get existing cart (or throw)
const cart = await this.findOneOrFail(userId, restaurantId);
// Validate and get delivery method
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId,
deliveryMethodId,
);
cart.deliveryMethodId = deliveryMethodId;
// Handle each delivery method with its specific requirements
switch (deliveryMethod.method) {
case DeliveryMethodEnum.DineIn:
// DineIn requires table number and clears incompatible fields
if (!tableNumber) {
throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED);
}
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
cart.tableNumber = tableNumber;
break;
case DeliveryMethodEnum.CustomerPickup:
// CustomerPickup just clears incompatible fields
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup);
break;
case DeliveryMethodEnum.DeliveryCar:
// DeliveryCar requires carAddress and clears incompatible fields
if (!carAddress) {
throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED);
}
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
const user = await this.validationService.getUserOrFail(userId);
cart.carAddress = {
carModel: carAddress.carModel,
carColor: carAddress.carColor,
plateNumber: carAddress.plateNumber,
phone: user.phone,
};
break;
case DeliveryMethodEnum.DeliveryCourier:
// DeliveryCourier requires addressId and clears incompatible fields
if (!addressId) {
throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
}
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
const address = await this.validationService.getUserAddressOrFail(addressId);
this.validationService.validateAddressOwnership(address, userId);
// ensure address is within restaurant service area
await this.validationService.assertAddressInsideServiceArea(
restaurantId,
address.latitude,
address.longitude,
);
cart.userAddress = {
address: address.address,
latitude: address.latitude,
longitude: address.longitude,
city: address.city,
province: address.province || '',
postalCode: address.postalCode || undefined,
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
phone: address.user.phone,
};
break;
}
// Validate and set payment method
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId,
paymentMethodId,
);
// Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above).
await this.calculationService.recalculateCartTotals(cart);
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
}
cart.paymentMethodId = paymentMethodId;
// Set description (if provided)
if (description !== undefined) {
cart.description = description;
}
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
// Final recalculation + save and return cart
return this.recalculateAndSaveCart(cart);
}
/**
* Get or create cart for user and restaurant
*/
async getOrCreateCart(userId: string, restaurantId: string): Promise<Cart> {
const existingCart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
if (existingCart) {
return existingCart;
}
// Find restaurant
const restaurant = await this.validationService.getRestaurantOrFail(restaurantId);
// Create new cart
const now = this.nowIso();
const cart: Cart = {
userId,
restaurantId,
restaurantName: restaurant.name,
items: [],
deliveryFee: 0,
subTotal: 0,
itemsDiscount: 0,
couponDiscount: 0,
totalDiscount: 0,
tax: 0,
total: 0,
totalItems: 0,
createdAt: now,
updatedAt: now,
};
await this.cartRepository.save(cart);
return cart;
}
/**
* Find cart by user and restaurant
*/
async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
if (!cart) {
throw new NotFoundException(CartMessage.NOT_FOUND);
}
return cart;
}
/**
* Increment item quantity in cart
*/
async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
await this.itemService.addOrIncrementItem(cart, foodId, restaurantId, quantity);
return this.recalculateAndSaveCart(cart);
}
/**
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
*/
async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
await this.itemService.decrementOrRemoveItem(cart, foodId);
return this.recalculateAndSaveCart(cart);
}
/**
* Bulk add items to cart (increments quantity if items exist)
*/
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
for (const addItemDto of bulkAddItemsDto.items) {
await this.itemService.addOrIncrementItem(cart, addItemDto.foodId, restaurantId, addItemDto.quantity);
}
return this.recalculateAndSaveCart(cart);
}
/**
* Remove item from cart
*/
async removeItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
this.itemService.removeItemOrFail(cart, foodId);
return this.recalculateAndSaveCart(cart);
}
/**
* Apply coupon to cart
*/
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const orderAmount = cart.subTotal - cart.itemsDiscount;
const user = await this.validationService.getUserOrFail(userId);
// check if coupon is valid and belong to the restaurant
const { valid, coupon, message } = await this.couponService.validateCoupon(
applyCouponDto.code,
restaurantId,
Number(orderAmount),
user.phone,
);
if (!valid) {
throw new BadRequestException(message || CartMessage.COUPON_NOT_FOUND);
}
if (!coupon) {
throw new BadRequestException(CartMessage.COUPON_NOT_FOUND);
}
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
await this.validationService.assertCouponUsageLimit(userId, coupon.id, coupon.maxUsesPerUser);
await this.validationService.assertCouponMatchesCartIfRestricted(
cart,
coupon.foodCategories,
coupon.foods,
);
const couponDetail: OrderCouponDetail = {
couponId: coupon.id,
couponName: coupon.name,
couponCode: coupon.code,
type: coupon.type,
value: coupon.value,
maxDiscount: coupon.maxDiscount,
};
cart.coupon = couponDetail;
return this.recalculateAndSaveCart(cart);
}
/**
* Remove coupon from cart
*/
async removeCoupon(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
cart.coupon = undefined;
return this.recalculateAndSaveCart(cart);
}
/**
* Set address for cart
*/
async setAddress(userId: string, restaurantId: string, addressId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
cart,
'Delivery method must be set before setting address',
);
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
restaurantId,
deliveryMethodId,
);
this.validationService.assertDeliveryMethod(
deliveryMethod.method,
DeliveryMethodEnum.DeliveryCourier,
'Delivery method must be DeliveryCourier',
);
// Find and validate address belongs to user
const address = await this.validationService.getUserAddressOrFail(addressId);
// Verify address belongs to the user
this.validationService.validateAddressOwnership(address, userId);
// ensure address is within restaurant service area
await this.validationService.assertAddressInsideServiceArea(
restaurantId,
address.latitude,
address.longitude,
);
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
cart.userAddress = {
address: address.address,
latitude: address.latitude,
longitude: address.longitude,
city: address.city,
province: address.province || '',
postalCode: address.postalCode || undefined,
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
phone: address.user.phone,
};
return this.saveTouchedCart(cart);
}
/**
* Set payment method for cart
*/
async setPaymentMethod(
userId: string,
restaurantId: string,
paymentMethodId: string,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId,
paymentMethodId,
);
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
}
cart.paymentMethodId = paymentMethodId;
return this.recalculateAndSaveCart(cart);
}
/**
* Set delivery method for cart
*/
async setDeliveryMethod(
userId: string,
restaurantId: string,
deliveryMethodId: string,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId,
deliveryMethodId,
);
cart.deliveryMethodId = deliveryMethodId;
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
return this.recalculateAndSaveCart(cart);
}
/**
* Set description for cart
*/
async setDescription(userId: string, restaurantId: string, description: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
cart.description = description;
return this.saveTouchedCart(cart);
}
/**
* Set table number for cart
*/
async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
cart,
'Delivery method must be set before setting table number',
);
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
restaurantId,
deliveryMethodId,
);
this.validationService.assertDeliveryMethod(
deliveryMethod.method,
DeliveryMethodEnum.DineIn,
'Delivery method must be DineIn',
);
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
cart.tableNumber = tableNumber;
return this.saveTouchedCart(cart);
}
/**
* Set car delivery for cart
*/
async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
cart,
'Delivery method must be set before setting car delivery',
);
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
restaurantId,
deliveryMethodId,
);
this.validationService.assertDeliveryMethod(
deliveryMethod.method,
DeliveryMethodEnum.DeliveryCar,
'Delivery method must be DeliveryCar',
);
const user = await this.validationService.getUserOrFail(userId);
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
cart.carAddress = {
carModel: setCarDeliveryDto.carModel,
carColor: setCarDeliveryDto.carColor,
plateNumber: setCarDeliveryDto.plateNumber,
phone: user.phone,
};
return this.saveTouchedCart(cart);
}
/**
* Clear cart from cache
*/
async clearCart(userId: string, restaurantId: string): Promise<void> {
return this.cartRepository.delete(userId, restaurantId);
}
/**
* Clears cart fields that are not applicable for a given delivery method.
*/
private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void {
if (method === DeliveryMethodEnum.DineIn) {
cart.userAddress = null;
cart.carAddress = null;
return;
}
if (method === DeliveryMethodEnum.CustomerPickup) {
cart.userAddress = null;
cart.carAddress = null;
cart.tableNumber = undefined;
return;
}
if (method === DeliveryMethodEnum.DeliveryCourier) {
cart.carAddress = null;
cart.tableNumber = undefined;
return;
}
if (method === DeliveryMethodEnum.DeliveryCar) {
cart.userAddress = null;
cart.tableNumber = undefined;
return;
}
}
/**
* Save cart with updated timestamp
*/
private async saveTouchedCart(cart: Cart): Promise<Cart> {
cart.updatedAt = this.nowIso();
await this.cartRepository.save(cart);
return cart;
}
/**
* Recalculate cart totals and save
*/
private async recalculateAndSaveCart(cart: Cart): Promise<Cart> {
await this.calculationService.recalculateCartTotals(cart);
await this.cartRepository.save(cart);
return cart;
}
/**
* Get current ISO timestamp
*/
private nowIso(): string {
return new Date().toISOString();
}
}
@@ -0,0 +1,81 @@
import { Injectable } from '@nestjs/common';
import { CacheService } from '../../utils/cache.service';
import { Cart } from '../interfaces/cart.interface';
@Injectable()
export class CartRepository {
private readonly CART_TTL = 3600; // 1 hour in seconds
private readonly CART_KEY_PREFIX = 'cart';
constructor(private readonly cacheService: CacheService) {}
/**
* Get cart by user and restaurant
*/
async findByUserAndRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
const cacheKey = this.getCacheKey(userId, restaurantId);
const cachedCart = await this.cacheService.get<string>(cacheKey);
if (cachedCart) {
try {
const parsed: unknown = JSON.parse(cachedCart);
if (this.isCart(parsed) && parsed.userId === userId && parsed.restaurantId === restaurantId) {
return parsed;
}
} catch {
// If parsing fails, return null
}
}
return null;
}
/**
* Save cart to cache
*/
async save(cart: Cart): Promise<void> {
const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId);
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
}
/**
* Delete cart from cache
*/
async delete(userId: string, restaurantId: string): Promise<void> {
const cacheKey = this.getCacheKey(userId, restaurantId);
await this.cacheService.del(cacheKey);
}
/**
* Generate cache key for cart
*/
private getCacheKey(userId: string, restaurantId: string): string {
return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
}
/**
* Type guard to check if an object is a Cart
*/
private isCart(obj: unknown): obj is Cart {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const cart = obj as Record<string, unknown>;
return (
typeof cart.userId === 'string' &&
typeof cart.restaurantId === 'string' &&
Array.isArray(cart.items) &&
typeof cart.subTotal === 'number' &&
typeof cart.itemsDiscount === 'number' &&
typeof cart.couponDiscount === 'number' &&
typeof cart.totalDiscount === 'number' &&
typeof cart.tax === 'number' &&
typeof cart.deliveryFee === 'number' &&
typeof cart.total === 'number' &&
typeof cart.totalItems === 'number' &&
typeof cart.createdAt === 'string' &&
typeof cart.updatedAt === 'string'
);
}
}
@@ -0,0 +1,68 @@
/**
* Utility class for geographic calculations
*/
export class GeographicUtils {
/**
* Check if a point is inside a polygon using ray-casting algorithm
* @param point - [longitude, latitude]
* @param polygon - Array of [longitude, latitude] pairs
*/
static isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean {
const x = point[0];
const y = point[1];
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect =
yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi;
if (intersect) inside = !inside;
}
return inside;
}
/**
* Convert degrees to radians
*/
private static toRad(value: number): number {
return (value * Math.PI) / 180;
}
/**
* Calculate distance between two points in kilometers using Haversine formula
*/
static getDistanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371; // Earth radius in KM
const dLat = this.toRad(lat2 - lat1);
const dLng = this.toRad(lng2 - lng1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Calculate distance between two points in kilometers (rounded)
*/
static getDistanceKmRounded(
lat1: number,
lng1: number,
lat2: number,
lng2: number,
precision = 2,
): number {
const distance = this.getDistanceKm(lat1, lng1, lat2, lng2);
return Number(distance.toFixed(precision));
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { ContactService } from './providers/contact.service';
import { ContactController } from './controllers/contact.controller';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Contact } from './entities/contact.entity';
import { ContactRepository } from './repositories/contact.repository';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [MikroOrmModule.forFeature([Contact]), JwtModule],
controllers: [ContactController],
providers: [ContactService, ContactRepository],
exports: [ContactRepository],
})
export class ContactModule {}
@@ -0,0 +1,108 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Query,
UseGuards,
Delete,
} from '@nestjs/common';
import { ContactService } from '../providers/contact.service';
import { CreateContactDto } from '../dto/create-contact.dto';
import { FindContactsDto } from '../dto/find-contacts.dto';
import { UpdateContactStatusDto } from '../dto/update-contact-status.dto';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
import { API_HEADER_SLUG } from 'src/common/constants';
import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { RestId } from 'src/common/decorators/rest-id.decorator';
import { RestSlug } from 'src/common/decorators/rest-slug.decorator';
@ApiTags('contact')
@Controller()
export class ContactController {
constructor(private readonly contactService: ContactService) { }
@UseGuards(OptionalAuthGuard)
@Post('public/contact')
@ApiOperation({ summary: 'Create a new contact (public endpoint)' })
@ApiHeader(API_HEADER_SLUG)
async create(@Body() createContactDto: CreateContactDto, @RestSlug() slug: string, @RestId() restId?: string) {
return this.contactService.create(createContactDto, restId, slug);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_CONTACTS)
@Get('admin/contact')
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
async findAllPaginated(@RestId() restId: string, @Query() dto: FindContactsDto) {
return this.contactService.findAllPaginatedAdmin(dto, restId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_CONTACTS)
@Get('admin/contact/:id')
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' })
async findOne(@RestId() restId: string, @Param('id') id: string) {
return this.contactService.findOne(id, restId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_CONTACTS)
@Patch('admin/contact/:id/status')
@ApiOperation({ summary: 'Update contact status (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' })
async updateStatus(@RestId() restId: string, @Param('id') id: string, @Body() dto: UpdateContactStatusDto) {
return this.contactService.updateStatus(id, dto, restId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_CONTACTS)
@Delete('admin/contact/:id')
@ApiOperation({ summary: 'Hard delete a contact (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' })
async remove(@RestId() restId: string, @Param('id') id: string) {
await this.contactService.remove(id, restId);
}
/*** Super Admin ***/
@UseGuards(SuperAdminAuthGuard)
@ApiBearerAuth()
@Get('super-admin/contact')
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
async findAllPaginatedForSuper(@Query() dto: FindContactsDto) {
return this.contactService.findAllPaginatedSuper(dto);
}
@UseGuards(SuperAdminAuthGuard)
@ApiBearerAuth()
@Get('super-admin/contact/:id')
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' })
async findOneForSuper(@Param('id') id: string) {
return this.contactService.findOne(id);
}
}
@@ -0,0 +1,26 @@
import { IsNotEmpty, IsString, IsOptional, IsMobilePhone, IsEnum } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ContactScope } from '../interface/interface';
export class CreateContactDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'Question about menu', description: 'Contact subject' })
subject?: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ example: 'I have a question about your menu items', description: 'Contact content/message' })
content!: string;
@IsNotEmpty()
@IsEnum(['application', 'restaurant'])
@ApiProperty({ description: 'Contact Scope' })
scope!: ContactScope;
@IsOptional()
@IsString()
@IsMobilePhone('fa-IR')
@ApiPropertyOptional({ example: '09362532122', description: 'Phone number' })
phone?: string;
}
@@ -0,0 +1,42 @@
import { IsOptional, IsNumber, IsString, IsIn, IsEnum } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ContactStatusEnum } from '../interface/interface';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindContactsDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 1, description: 'Page number', minimum: 1, default: 1 })
page?: number = 1;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10, description: 'Items per page', minimum: 1, default: 10 })
limit?: number = 10;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'menu', description: 'Search in subject, content, or phone' })
search?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'createdAt', description: 'Field to sort by', default: 'createdAt' })
orderBy?: string = 'createdAt';
@IsOptional()
@IsIn(sortOrderOptions)
@ApiPropertyOptional({ example: 'desc', enum: sortOrderOptions, default: 'desc' })
order?: SortOrder = 'desc';
@IsOptional()
@IsEnum(ContactStatusEnum)
@ApiPropertyOptional({ enum: ContactStatusEnum, description: 'Filter by status' })
status?: ContactStatusEnum;
}
@@ -0,0 +1,10 @@
import { IsEnum, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ContactStatusEnum } from '../interface/interface';
export class UpdateContactStatusDto {
@IsNotEmpty()
@IsEnum(ContactStatusEnum)
@ApiProperty({ enum: ContactStatusEnum, description: 'New status for the contact' })
status!: ContactStatusEnum;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContactDto } from './create-contact.dto';
export class UpdateContactDto extends PartialType(CreateContactDto) {}

Some files were not shown because too many files have changed in this diff Show More