From bae7ae89ed7d77eba2448b36d482587335e47cf6 Mon Sep 17 00:00:00 2001 From: Arth <65160206@go.buu.ac.th> Date: Thu, 27 Mar 2025 22:50:14 +0700 Subject: [PATCH] deploy to dekdee --- .dockerignore | 3 + .gitignore | 24 +- docker-compose.yml | 49 ++ dockerfile | 20 + node_modules/.package-lock.json | 37 +- node_modules/dotenv/CHANGELOG.md | 17 +- node_modules/dotenv/README.md | 151 ++---- node_modules/dotenv/package.json | 14 +- package-lock.json | 38 +- package.json | 1 + src/app.module.ts | 17 +- src/main.ts | 9 +- src/queues/AutoQueue/Autoqueue.py | 506 ------------------ src/queues/AutoQueue/Autoqueuenew.py | 446 --------------- src/queues/AutoQueue/Log copy.py | 438 --------------- src/queues/AutoQueue/Log.py | 45 -- .../__pycache__/Autoqueue.cpython-310.pyc | Bin 10637 -> 0 bytes .../__pycache__/Autoqueue.cpython-311.pyc | Bin 18940 -> 0 bytes .../__pycache__/Autoqueue.cpython-313.pyc | Bin 16952 -> 0 bytes src/queues/AutoQueue/cleandata.py | 228 -------- src/queues/AutoQueue/requirements.txt | 6 - src/queues/AutoQueue/testAllapi.py | 132 ----- 22 files changed, 242 insertions(+), 1939 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.yml create mode 100644 dockerfile delete mode 100644 src/queues/AutoQueue/Autoqueue.py delete mode 100644 src/queues/AutoQueue/Autoqueuenew.py delete mode 100644 src/queues/AutoQueue/Log copy.py delete mode 100644 src/queues/AutoQueue/Log.py delete mode 100644 src/queues/AutoQueue/__pycache__/Autoqueue.cpython-310.pyc delete mode 100644 src/queues/AutoQueue/__pycache__/Autoqueue.cpython-311.pyc delete mode 100644 src/queues/AutoQueue/__pycache__/Autoqueue.cpython-313.pyc delete mode 100644 src/queues/AutoQueue/cleandata.py delete mode 100644 src/queues/AutoQueue/requirements.txt delete mode 100644 src/queues/AutoQueue/testAllapi.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4c245931 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.env.local \ No newline at end of file diff --git a/.gitignore b/.gitignore index f94630c5..be863f26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,25 @@ +# Node.js node_modules/ dist/ -.env \ No newline at end of file +*.log +*.tsbuildinfo + +# Docker +.docker/ +docker-compose.override.yml +docker-compose.prod.yml + +# .env files (สำคัญที่สุด!) +.env +.env.local +.env.docker +.env.production + +# IDE & OS files +.vscode/ +.idea/ +*.swp +.DS_Store + +# NPM package-lock.json files (จะ add หรือ commit ขึ้นอยู่กับสถานการณ์) +package-lock.json diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b2e0b901 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +services: + nestjs: + build: + context: . # ✅ ชี้ไปยังโฟลเดอร์ที่มี package.json และ Dockerfile + dockerfile: Dockerfile + container_name: LOOKOAD_NestJS + ports: + - "8012:4000" + environment: + NODE_ENV: docker + DB_HOST: mysql + DB_PORT: 3306 + DB_USERNAME: root + DB_PASSWORD: Lookoad2024! + DB_NAME: water + depends_on: + - mysql + restart: always + + mysql: + image: mysql:8.0 + container_name: lookoad-mysql + environment: + MYSQL_ROOT_PASSWORD: Lookoad2024! + MYSQL_DATABASE: water + volumes: + - mysql_data:/var/lib/mysql + restart: always + healthcheck: + test: ["CMD", "mysqladmin", "ping", "--silent"] + interval: 10s + retries: 5 + start_period: 30s + timeout: 20s + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: LOOKOAD_Phpmyadmin + ports: + - "8014:80" + environment: + PMA_HOST: mysql + MYSQL_ROOT_PASSWORD: Lookoad2024! + depends_on: + - mysql + restart: always + +volumes: + mysql_data: diff --git a/dockerfile b/dockerfile new file mode 100644 index 00000000..23cef4c6 --- /dev/null +++ b/dockerfile @@ -0,0 +1,20 @@ +FROM node:18 AS builder +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +FROM node:18 +WORKDIR /app + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package*.json ./ + +EXPOSE 4000 + +# ✅ รันตรง ๆ ด้วย Node ไม่ผ่าน npm +CMD ["node", "dist/main.js"] diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 41594d94..768931bc 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1719,6 +1719,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", @@ -4321,9 +4336,25 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, "engines": { "node": ">=12" }, diff --git a/node_modules/dotenv/CHANGELOG.md b/node_modules/dotenv/CHANGELOG.md index e35152ae..e3e40d6e 100644 --- a/node_modules/dotenv/CHANGELOG.md +++ b/node_modules/dotenv/CHANGELOG.md @@ -2,13 +2,26 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/motdotla/dotenv/compare/v16.4.5...master) +## [Unreleased](https://github.com/motdotla/dotenv/compare/v16.4.7...master) + +## [16.4.7](https://github.com/motdotla/dotenv/compare/v16.4.6...v16.4.7 (2024-12-03) + +### Changed + +- Ignore `.tap` folder when publishing. (oops, sorry about that everyone. - @motdotla) [#848](https://github.com/motdotla/dotenv/pull/848) + +## [16.4.6](https://github.com/motdotla/dotenv/compare/v16.4.5...v16.4.6) (2024-12-02) + +### Changed + +- Clean up stale dev dependencies [#847](https://github.com/motdotla/dotenv/pull/847) +- Various README updates clarifying usage and alternative solutions using [dotenvx](https://github.com/dotenvx/dotenvx) ## [16.4.5](https://github.com/motdotla/dotenv/compare/v16.4.4...v16.4.5) (2024-02-19) ### Changed -- 🐞 fix recent regression when using `path` option. return to historical behavior: do not attempt to auto find `.env` if `path` set. (regression was introduced in `16.4.3`) [#814](https://github.com/motdotla/dotenv/pull/814) +- 🐞 Fix recent regression when using `path` option. return to historical behavior: do not attempt to auto find `.env` if `path` set. (regression was introduced in `16.4.3`) [#814](https://github.com/motdotla/dotenv/pull/814) ## [16.4.4](https://github.com/motdotla/dotenv/compare/v16.4.3...v16.4.4) (2024-02-13) diff --git a/node_modules/dotenv/README.md b/node_modules/dotenv/README.md index e54075ee..3bd511c4 100644 --- a/node_modules/dotenv/README.md +++ b/node_modules/dotenv/README.md @@ -33,16 +33,6 @@ <sup>Add Single Sign-On, Multi-Factor Auth, and more, in minutes instead of months.</sup> </div> </a> -<br/> -<a href="https://runalloy.com/?utm_source=github&utm_medium=referral&utm_campaign=1224_dotenv"> - <div> - <img src="https://res.cloudinary.com/dotenv-org/image/upload/c_crop,g_center,h_65,w_290,x_0,y_0/v1704258787/AlloyAutomation-logo_dqin8c.svg" width="370" alt="Alloy Automation"> - </div> - <b>Launch user-facing integrations faster</b> - <div> - <sup>Easily spin up hundreds of integrations. Sign up free or read our docs first</sup> - </div> -</a> <hr> </div> @@ -59,7 +49,7 @@ Dotenv is a zero-dependency module that loads environment variables from a `.env * [🌱 Install](#-install) * [🏗️ Usage (.env)](#%EF%B8%8F-usage) * [🌴 Multiple Environments 🆕](#-manage-multiple-environments) -* [🚀 Deploying (.env.vault) 🆕](#-deploying) +* [🚀 Deploying (encryption) 🆕](#-deploying) * [📚 Examples](#-examples) * [📖 Docs](#-documentation) * [❓ FAQ](#-faq) @@ -83,7 +73,7 @@ Or installing with yarn? `yarn add dotenv` </div> </a> -Create a `.env` file in the root of your project: +Create a `.env` file in the root of your project (if using a monorepo structure like `apps/backend/app.js`, put it in the root of the folder where your `app.js` process runs): ```dosini S3_BUCKET="YOURS3BUCKET" @@ -107,6 +97,7 @@ That's it. `process.env` now has the keys and values you defined in your `.env` ```javascript require('dotenv').config() +// or import 'dotenv/config' if you're using ES6 ... @@ -186,23 +177,41 @@ $ DOTENV_CONFIG_ENCODING=latin1 DOTENV_CONFIG_DEBUG=true node -r dotenv/config y You need to add the value of another variable in one of your variables? Use [dotenv-expand](https://github.com/motdotla/dotenv-expand). +### Command Substitution + +Use [dotenvx](https://github.com/dotenvx/dotenvx) to use command substitution. + +Add the output of a command to one of your variables in your .env file. + +```ini +# .env +DATABASE_URL="postgres://$(whoami)@localhost/my_database" +``` +```js +// index.js +console.log('DATABASE_URL', process.env.DATABASE_URL) +``` +```sh +$ dotenvx run --debug -- node index.js +[dotenvx@0.14.1] injecting env (1) from .env +DATABASE_URL postgres://yourusername@localhost/my_database +``` + ### Syncing -You need to keep `.env` files in sync between machines, environments, or team members? Use [dotenv-vault](https://github.com/dotenv-org/dotenv-vault). +You need to keep `.env` files in sync between machines, environments, or team members? Use [dotenvx](https://github.com/dotenvx/dotenvx) to encrypt your `.env` files and safely include them in source control. This still subscribes to the twelve-factor app rules by generating a decryption key separate from code. ### Multiple Environments -You need to manage your secrets across different environments and apply them as needed? Use a `.env.vault` file with a `DOTENV_KEY`. +Use [dotenvx](https://github.com/dotenvx/dotenvx) to generate `.env.ci`, `.env.production` files, and more. ### Deploying -You need to deploy your secrets in a cloud-agnostic manner? Use a `.env.vault` file. See [deploying `.env.vault` files](https://github.com/motdotla/dotenv/tree/master#-deploying). +You need to deploy your secrets in a cloud-agnostic manner? Use [dotenvx](https://github.com/dotenvx/dotenvx) to generate a private decryption key that is set on your production server. ## 🌴 Manage Multiple Environments -Use [dotenvx](https://github.com/dotenvx/dotenvx) or [dotenv-vault](https://github.com/dotenv-org/dotenv-vault). - -### dotenvx +Use [dotenvx](https://github.com/dotenvx/dotenvx) Run any environment locally. Create a `.env.ENVIRONMENT` file and use `--env-file` to load it. It's straightforward, yet flexible. @@ -228,78 +237,23 @@ Hello local [more environment examples](https://dotenvx.com/docs/quickstart/environments) -### dotenv-vault - -Edit your production environment variables. - -```bash -$ npx dotenv-vault open production -``` - -Regenerate your `.env.vault` file. - -```bash -$ npx dotenv-vault build -``` - -*ℹ️ 🔐 Vault Managed vs 💻 Locally Managed: The above example, for brevity's sake, used the 🔐 Vault Managed solution to manage your `.env.vault` file. You can instead use the 💻 Locally Managed solution. [Read more here](https://github.com/dotenv-org/dotenv-vault#how-do-i-use--locally-managed-dotenv-vault). Our vision is that other platforms and orchestration tools adopt the `.env.vault` standard as they did the `.env` standard. We don't expect to be the only ones providing tooling to manage and generate `.env.vault` files.* - -<a href="https://github.com/dotenv-org/dotenv-vault#-manage-multiple-environments">Learn more at dotenv-vault: Manage Multiple Environments</a> - ## 🚀 Deploying -Use [dotenvx](https://github.com/dotenvx/dotenvx) or [dotenv-vault](https://github.com/dotenv-org/dotenv-vault). +Use [dotenvx](https://github.com/dotenvx/dotenvx). -### dotenvx +Add encryption to your `.env` files with a single command. Pass the `--encrypt` flag. -Encrypt your secrets to a `.env.vault` file and load from it (recommended for production and ci). - -```bash -$ echo "HELLO=World" > .env -$ echo "HELLO=production" > .env.production +``` +$ dotenvx set HELLO Production --encrypt -f .env.production $ echo "console.log('Hello ' + process.env.HELLO)" > index.js -$ dotenvx encrypt -[dotenvx][info] encrypted to .env.vault (.env,.env.production) -[dotenvx][info] keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION) - -$ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js -[dotenvx][info] loading env (1) from encrypted .env.vault -Hello production -^ :-] +$ DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js +[dotenvx] injecting env (2) from .env.production +Hello Production ``` [learn more](https://github.com/dotenvx/dotenvx?tab=readme-ov-file#encryption) -### dotenv-vault - -*Note: Requires dotenv >= 16.1.0* - -Encrypt your `.env.vault` file. - -```bash -$ npx dotenv-vault build -``` - -Fetch your production `DOTENV_KEY`. - -```bash -$ npx dotenv-vault keys production -``` - -Set `DOTENV_KEY` on your server. - -```bash -# heroku example -heroku config:set DOTENV_KEY=dotenv://:key_1234…@dotenvx.com/vault/.env.vault?environment=production -``` - -That's it! On deploy, your `.env.vault` file will be decrypted and its secrets injected as environment variables – just in time. - -*ℹ️ A note from [Mot](https://github.com/motdotla): Until recently, we did not have an opinion on how and where to store your secrets in production. We now strongly recommend generating a `.env.vault` file. It's the best way to prevent your secrets from being scattered across multiple servers and cloud providers – protecting you from breaches like the [CircleCI breach](https://techcrunch.com/2023/01/05/circleci-breach/). Also it unlocks interoperability WITHOUT native third-party integrations. Third-party integrations are [increasingly risky](https://coderpad.io/blog/development/heroku-github-breach/) to our industry. They may be the 'du jour' of today, but we imagine a better future.* - -<a href="https://github.com/dotenv-org/dotenv-vault#-deploying">Learn more at dotenv-vault: Deploying</a> - ## 📚 Examples See [examples](https://github.com/dotenv-org/examples) of using dotenv with various frameworks, languages, and configurations. @@ -308,7 +262,6 @@ See [examples](https://github.com/dotenv-org/examples) of using dotenv with vari * [nodejs (debug on)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nodejs-debug) * [nodejs (override on)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-nodejs-override) * [nodejs (processEnv override)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-custom-target) -* [nodejs (DOTENV_KEY override)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-vault-custom-target) * [esm](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-esm) * [esm (preload)](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-esm-preload) * [typescript](https://github.com/dotenv-org/examples/tree/master/usage/dotenv-typescript) @@ -409,20 +362,10 @@ Specify an object to write your secrets to. Defaults to `process.env` environmen const myObject = {} require('dotenv').config({ processEnv: myObject }) -console.log(myObject) // values from .env or .env.vault live here now. +console.log(myObject) // values from .env console.log(process.env) // this was not changed or written to ``` -##### DOTENV_KEY - -Default: `process.env.DOTENV_KEY` - -Pass the `DOTENV_KEY` directly to config options. Defaults to looking for `process.env.DOTENV_KEY` environment variable. Note this only applies to decrypting `.env.vault` files. If passed as null or undefined, or not passed at all, dotenv falls back to its traditional job of parsing a `.env` file. - -```js -require('dotenv').config({ DOTENV_KEY: 'dotenv://:key_1234…@dotenvx.com/vault/.env.vault?environment=production' }) -``` - ### Parse The engine which parses the contents of your file containing environment @@ -493,22 +436,6 @@ Default: `false` Override any environment variables that have already been set. -### Decrypt - -The engine which decrypts the ciphertext contents of your .env.vault file is available for use. It accepts a ciphertext and a decryption key. It uses AES-256-GCM encryption. - -For example, decrypting a simple ciphertext: - -```js -const dotenv = require('dotenv') -const ciphertext = 's7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R' -const decryptionKey = 'ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00' - -const decrypted = dotenv.decrypt(ciphertext, decryptionKey) - -console.log(decrypted) // # development@v6\nALPHA="zeta" -``` - ## ❓ FAQ ### Why is the `.env` file not loading my environment variables successfully? @@ -532,7 +459,7 @@ password than your development database. ### Should I have multiple `.env` files? -We recommend creating on `.env` file per environment. Use `.env` for local/development, `.env.production` for production and so on. This still follows the twelve factor principles as each is attributed individually to its own environment. Avoid custom set ups that work in inheritance somehow (`.env.production` inherits values form `.env` for example). It is better to duplicate values if necessary across each `.env.environment` file. +We recommend creating one `.env` file per environment. Use `.env` for local/development, `.env.production` for production and so on. This still follows the twelve factor principles as each is attributed individually to its own environment. Avoid custom set ups that work in inheritance somehow (`.env.production` inherits values form `.env` for example). It is better to duplicate values if necessary across each `.env.environment` file. > In a twelve-factor app, env vars are granular controls, each fully orthogonal to other env vars. They are never grouped together as “environments”, but instead are independently managed for each deploy. This is a model that scales up smoothly as the app naturally expands into more deploys over its lifetime. > @@ -685,11 +612,7 @@ Try [dotenv-expand](https://github.com/motdotla/dotenv-expand) ### What about syncing and securing .env files? -Use [dotenv-vault](https://github.com/dotenv-org/dotenv-vault) - -### What is a `.env.vault` file? - -A `.env.vault` file is an encrypted version of your development (and ci, staging, production, etc) environment variables. It is paired with a `DOTENV_KEY` to deploy your secrets more securely than scattering them across multiple platforms and tools. Use [dotenv-vault](https://github.com/dotenv-org/dotenv-vault) to manage and generate them. +Use [dotenvx](https://github.com/dotenvx/dotenvx) ### What if I accidentally commit my `.env` file to code? diff --git a/node_modules/dotenv/package.json b/node_modules/dotenv/package.json index bb06025b..528426b4 100644 --- a/node_modules/dotenv/package.json +++ b/node_modules/dotenv/package.json @@ -1,6 +1,6 @@ { "name": "dotenv", - "version": "16.4.5", + "version": "16.4.7", "description": "Loads environment variables from .env file", "main": "lib/main.js", "types": "lib/main.d.ts", @@ -21,10 +21,9 @@ "scripts": { "dts-check": "tsc --project tests/types/tsconfig.json", "lint": "standard", - "lint-readme": "standard-markdown", "pretest": "npm run lint && npm run dts-check", - "test": "tap tests/*.js --100 -Rspec", - "test:coverage": "tap --coverage-report=lcov", + "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000", + "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov", "prerelease": "npm test", "release": "standard-version" }, @@ -45,15 +44,12 @@ "readmeFilename": "README.md", "license": "BSD-2-Clause", "devDependencies": { - "@definitelytyped/dtslint": "^0.0.133", "@types/node": "^18.11.3", - "decache": "^4.6.1", + "decache": "^4.6.2", "sinon": "^14.0.1", "standard": "^17.0.0", - "standard-markdown": "^7.1.0", "standard-version": "^9.5.0", - "tap": "^16.3.0", - "tar": "^6.1.11", + "tap": "^19.2.0", "typescript": "^4.8.4" }, "engines": { diff --git a/package-lock.json b/package-lock.json index 582dfb79..26a4d461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.3.3", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", @@ -1770,6 +1771,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", @@ -4372,9 +4388,25 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 48563146..fdc6c1ac 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@nestjs/common": "^10.3.3", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index eeb0491d..5be3db30 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { join } from 'path'; import { DataSource } from 'typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { ConfigModule } from '@nestjs/config'; // เพิ่ม ConfigModule // Import Modules import { UsersModule } from './users/users.module'; @@ -55,13 +56,18 @@ import { ProductionTarget } from './production_targets/entities/production_targe import { QueueType } from './queue-types/entities/queue-type.entity'; @Module({ imports: [ + ConfigModule.forRoot({ + isGlobal: true, // ทำให้ใช้ได้ทั่วทั้งแอป + envFilePath: + process.env.NODE_ENV === 'docker' ? '.env.docker' : '.env.local', // หรือ '.env.local' / '.env.docker' ตามที่ต้องการ + }), TypeOrmModule.forRoot({ type: 'mysql', - host: 'localhost', - port: 3306, - username: 'root', - password: '', - database: 'water', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '3306'), + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME, entities: [ User, Role, @@ -86,6 +92,7 @@ import { QueueType } from './queue-types/entities/queue-type.entity'; QueueType, ], synchronize: true, + autoLoadEntities: true, }), ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', 'public'), diff --git a/src/main.ts b/src/main.ts index 5d8d8da9..f28f67cf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,13 @@ import { ValidationPipe } from '@nestjs/common'; import { exec } from 'child_process'; async function bootstrap() { + console.log('✅ NestJS is running on http://localhost:4000'); + console.log('🌱 ENV Loaded:'); + console.log('DB_HOST:', process.env.DB_HOST); + console.log('DB_PORT:', process.env.DB_PORT); + console.log('DB_USERNAME:', process.env.DB_USERNAME); + console.log('DB_PASSWORD:', process.env.DB_PASSWORD); + console.log('DB_NAME:', process.env.DB_NAME); const app = await NestFactory.create(AppModule); // 🔹 เพิ่ม Swagger API Documentation @@ -45,7 +52,7 @@ async function bootstrap() { // ); // 🔹 Start NestJS await app.listen(4000); - console.log('✅ NestJS is running on http://localhost:4000'); + // console.log('✅ FastAPI started successfully: http://localhost:9000'); } diff --git a/src/queues/AutoQueue/Autoqueue.py b/src/queues/AutoQueue/Autoqueue.py deleted file mode 100644 index cbc993e2..00000000 --- a/src/queues/AutoQueue/Autoqueue.py +++ /dev/null @@ -1,506 +0,0 @@ -import simpy -from datetime import datetime, timedelta -import math -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -# ============================================== -# 1) ข้อมูลเครื่องจักร (machines_info) -# ============================================== -machines_info = [ - { - "machine_id": "MC001", - "machine_name": "เครื่องเป่าขวด", - "function": "เป่า", - "materials": ["Preform"], # ใช้วัตถุดิบ Preform - "bottle_size": ["600 ml", "1500 ml"], - "speed": [2200, 1100], # 600 ml => 2200 ขวด/ชม, 1500 ml => 1100 ขวด/ชม - "changeover_time": [3, 3], - "status": "พร้อมใช้งาน" - }, - { - "machine_id": "MC002", - "machine_name": "เครื่องเป่าขวด 350ml", - "function": "เป่า", - "materials": ["Preform"], - "bottle_size": ["350 ml"], - "speed": [2000], - "changeover_time": [0], - "status": "พร้อมใช้งาน" - }, - { - "machine_id": "MC003", - "machine_name": "เครื่องสวมฉลาก", - "function": "สวมฉลาก", - "materials": ["Label", "EmptyBottle"], - "bottle_size": ["350 ml", "600 ml", "1500 ml"], - "speed": [4000, 4000, 2000], - "changeover_time": [0, 0, 0], - "status": "พร้อมใช้งาน" - }, - { - "machine_id": "MC004", - "machine_name": "เครื่องบรรจุ + แพ็ค", - "function": "บรรจุ, แพ็ค", - "materials": ["EmptyBottleLabeled", "Cap", "PE_film"], - "bottle_size": ["350 ml", "600 ml", "1500 ml"], - "speed": [2400, 2400, 2400], - "changeover_time": [0.5, 0.5, 0.5], - "status": "พร้อมใช้งาน" - } -] - -# ============================================== -# 2) สต็อกวัตถุดิบ -# ============================================== -raw_material_stock = { - "Preform": 20000, - "Label_A": 20000, - "Label_B": 20000, - "Cap_A": 20000, - "Cap_B": 20000, - # 1 roll => 9600 ขวด - "PE_film_roll": 20 -} - -# ============================================== -# 3) Orders ลูกค้า (ตัวอย่าง) -# ============================================== -customer_orders = [ - { - "customer": "Wittaya", - "type": "water", - "size": "600 ml", - "brand": "A", - "quantity_bottles": 8400, - "due_date": "2025-01-08", - "priority": 2 - }, - { - "customer": "Aof", - "type": "water", - "size": "600 ml", - "brand": "A", - "quantity_bottles": 3600, - "due_date": "2025-01-07", - "priority": 1 - } -] - -# ============================================== -# 4) ตาราง config Buffer Stock -# ============================================== -buffer_config = [ - {"type": "water", "size": "350 ml", "brand": "A", "want_to_store": 0, "current_store": 0}, - {"type": "water", "size": "600 ml", "brand": "A", "want_to_store": 8000, "current_store": 0}, - {"type": "water", "size": "1500 ml", "brand": "A", "want_to_store": 6000, "current_store": 0}, - {"type": "water", "size": "350 ml", "brand": "B", "want_to_store": 0, "current_store": 0}, - {"type": "water", "size": "600 ml", "brand": "B", "want_to_store": 6000, "current_store": 0}, - {"type": "water", "size": "1500 ml", "brand": "B", "want_to_store": 6000, "current_store": 0}, - {"type": "bottle", "size": "350 ml", "brand": None,"want_to_store": 0, "current_store": 0}, - {"type": "bottle", "size": "600 ml", "brand": None,"want_to_store": 6000, "current_store": 0}, - {"type": "bottle", "size": "1500 ml", "brand": None,"want_to_store": 6000, "current_store": 0} -] - -# ============================================== -# 5) ตาราง priority ของการผลิตเผื่อ -# ============================================== -buffer_priority = [ - {"priority": 1, "type": "water", "size": "600 ml", "brand": "A"}, - {"priority": 2, "type": "water", "size": "1500 ml", "brand": "A"}, - {"priority": 3, "type": "water", "size": "350 ml", "brand": "A"}, - {"priority": 4, "type": "water", "size": "600 ml", "brand": "B"}, - {"priority": 5, "type": "water", "size": "1500 ml", "brand": "B"}, - {"priority": 6, "type": "water", "size": "350 ml", "brand": "B"}, - {"priority": 7, "type": "bottle", "size": "600 ml", "brand": None}, - {"priority": 8, "type": "bottle", "size": "1500 ml", "brand": None}, - {"priority": 9, "type": "bottle", "size": "350 ml", "brand": None} -] - -# ============================================== -# 6) เวลา และ Block การทำงาน -# ============================================== -WORK_START_1 = 8 -WORK_END_1 = 12 -WORK_START_2 = 13 -WORK_END_2 = 17 -MIN_BLOCK_LEFT = 0.5 # ถ้าบล็อกเหลือ < 0.5 ชม => ข้าม - -def get_current_datetime(env): - start_sim = datetime(2025, 1, 1, 8, 0, 0) - return start_sim + timedelta(hours=env.now) - -def skip_to_next_work_time(env): - while True: - now_dt = get_current_datetime(env) - wd = now_dt.weekday() - hr = now_dt.hour - if wd >= 5: # เสาร์/อาทิตย์ - days_to_monday = 7 - wd - next_mon_8 = datetime(now_dt.year, now_dt.month, now_dt.day, 8) + timedelta(days=days_to_monday) - yield env.timeout((next_mon_8 - now_dt).total_seconds()/3600) - continue - - if hr < WORK_START_1: - next_8 = datetime(now_dt.year, now_dt.month, now_dt.day, WORK_START_1) - yield env.timeout((next_8 - now_dt).total_seconds()/3600) - continue - - if WORK_END_1 <= hr < WORK_START_2: - next_13 = datetime(now_dt.year, now_dt.month, now_dt.day, WORK_START_2) - yield env.timeout((next_13 - now_dt).total_seconds()/3600) - continue - - if hr >= WORK_END_2: - next_day_8 = datetime(now_dt.year, now_dt.month, now_dt.day, WORK_START_1) + timedelta(days=1) - yield env.timeout((next_day_8 - now_dt).total_seconds()/3600) - continue - - # อยู่ในเวลางานแล้ว - break - -def get_time_to_block_end(env): - now_dt = get_current_datetime(env) - hr = now_dt.hour - if WORK_START_1 <= hr < WORK_END_1: - block_end = datetime(now_dt.year, now_dt.month, now_dt.day, WORK_END_1) - else: - block_end = datetime(now_dt.year, now_dt.month, now_dt.day, WORK_END_2) - return (block_end - now_dt).total_seconds()/3600 - -# ============================================== -# 7) Machine Resource + Log -# ============================================== -class MachineResource: - def __init__(self, env, machine_id): - self.env = env - self.machine_id = machine_id - self.resource = simpy.PriorityResource(env, capacity=1) - self.last_size_run = None # เก็บ track ว่าล่าสุดผลิตขนาดไหน - -production_log = [] -queue_counter = 0 - -def log_event(machine_id, order_id, page_id, - start_dt, finish_dt, - bottle_size, produced_qty, duration_hrs): - """ - เก็บ Log ในรูปแบบเบื้องต้นก่อน - """ - global queue_counter - queue_counter += 1 - production_log.append({ - "QueueID": queue_counter, - "MachineID": machine_id, - "OrderID": order_id, # -1 สำหรับงานเผื่อ, -2 สำหรับ Changeover - "PageID": page_id, - "startTime": start_dt.strftime("%Y-%m-%d %H:%M:%S"), - "finishTime": finish_dt.strftime("%Y-%m-%d %H:%M:%S"), - "status": "Completed", - "bottleSize": bottle_size, - "producedQuantity": int(produced_qty), - "durationHours": round(duration_hrs, 2) - }) - -def create_machines(env): - d = {} - for mc in machines_info: - d[mc["machine_id"]] = MachineResource(env, mc["machine_id"]) - return d - -# ============================================== -# 8) ฟังก์ชัน check/consume วัตถุดิบ -# ============================================== -def check_raw_materials(item_type, brand, size, quantity): - if item_type == "bottle": - # จะใช้ preform 1 ชิ้น/ขวด - if raw_material_stock["Preform"] < quantity: - return False - elif item_type == "water": - # สุดท้ายจะใช้ preform, label, cap, pe film - if raw_material_stock["Preform"] < quantity: - return False - label_key = f"Label_{brand}" - if label_key not in raw_material_stock or raw_material_stock[label_key] < quantity: - return False - cap_key = f"Cap_{brand}" - if cap_key not in raw_material_stock or raw_material_stock[cap_key] < quantity: - return False - rolls_needed = math.ceil(quantity / 9600) - if raw_material_stock["PE_film_roll"] < rolls_needed: - return False - return True - -def consume_raw_materials(item_type, brand, size, quantity): - if item_type == "bottle": - raw_material_stock["Preform"] -= quantity - elif item_type == "water": - raw_material_stock["Preform"] -= quantity - raw_material_stock[f"Label_{brand}"] -= quantity - raw_material_stock[f"Cap_{brand}"] -= quantity - rolls_needed = math.ceil(quantity / 9600) - raw_material_stock["PE_film_roll"] -= rolls_needed - -# ============================================== -# 9) do_split_task: ทำงานจริง แบ่งเป็นบล็อก -# ============================================== -def do_split_task(env, machine_id, order_id, page_id, - bottle_size, rate, hours_needed): - remain = hours_needed - while remain > 0: - # ข้ามเวลาจนกว่าจะเข้าสู่ช่วงเวลาทำงาน - yield from skip_to_next_work_time(env) - - block_left = get_time_to_block_end(env) - if block_left < MIN_BLOCK_LEFT: - # ถ้าช่วงเวลาทำงานเหลือน้อยกว่า 0.5 ชม ให้ข้ามไปก่อน - yield env.timeout(block_left) - continue - - work_hrs = min(remain, block_left) - start_dt = get_current_datetime(env) - yield env.timeout(work_hrs) - finish_dt = get_current_datetime(env) - - remain -= work_hrs - produced = rate * work_hrs if rate>0 else 0 - dur_hrs = (finish_dt - start_dt).total_seconds()/3600 - - # บันทึก event - log_event(machine_id, order_id, page_id, start_dt, finish_dt, - bottle_size, produced, dur_hrs) - -# ============================================== -# 10) Pipeline สำหรับ Orders ลูกค้า -# ============================================== -def customer_pipeline(env, order, machines): - order_id = order["customer"] - item_type = order["type"] - brand = order["brand"] - size = order["size"] - qty = order["quantity_bottles"] - priority = order["priority"] - - # ตรวจสอบวัตถุดิบก่อน - if not check_raw_materials(item_type, brand, size, qty): - print(f"** วัตถุดิบไม่พอสำหรับ Order {order_id} => skip **") - return - consume_raw_materials(item_type, brand, size, qty) - - # เลือก Pipeline - if item_type == "water": - if size == "350 ml": - pipeline_seq = ["MC002", "MC003", "MC004"] - else: - pipeline_seq = ["MC001", "MC003", "MC004"] - else: - if size == "350 ml": - pipeline_seq = ["MC002"] - else: - pipeline_seq = ["MC001"] - - remain_qty = qty - for mc_id in pipeline_seq: - mc_info = next(m for m in machines_info if m["machine_id"]==mc_id) - idx = mc_info["bottle_size"].index(size) - rate = mc_info["speed"][idx] - cng = mc_info["changeover_time"][idx] - - mc_res = machines[mc_id] - with mc_res.resource.request(priority=priority) as req: - yield req - # === ทำ Changeover แยกเป็น orderID = -2 === - if mc_res.last_size_run != size: - if mc_res.last_size_run is not None: - yield env.process( - do_split_task(env, mc_id, -2, "Changeover", size, 0, cng) - ) - mc_res.last_size_run = size - - hours_needed = remain_qty / rate - yield env.process( - do_split_task(env, mc_id, order_id, mc_id, size, rate, hours_needed) - ) - -# ============================================== -# 11) Pipeline สำหรับการผลิต “เผื่อ” -# ============================================== -def buffer_pipeline(env, item, machines, priority): - order_id = -1 # หมายถึงงานผลิตเพื่อ buffer - item_type = item["type"] - brand = item["brand"] - size = item["size"] - - can_store = item["want_to_store"] - item["current_store"] - if can_store <= 0: - return - - # ตรวจสอบวัตถุดิบ - if not check_raw_materials(item_type, brand, size, can_store): - return - - # ตัดสต็อก - consume_raw_materials(item_type, brand, size, can_store) - - # Pipeline - if item_type == "water": - if size == "350 ml": - pipeline_seq = ["MC002", "MC003", "MC004"] - else: - pipeline_seq = ["MC001", "MC003", "MC004"] - else: - if size == "350 ml": - pipeline_seq = ["MC002"] - else: - pipeline_seq = ["MC001"] - - remain_qty = can_store - for mc_id in pipeline_seq: - mc_info = next(m for m in machines_info if m["machine_id"]==mc_id) - idx = mc_info["bottle_size"].index(size) - rate = mc_info["speed"][idx] - cng = mc_info["changeover_time"][idx] - - mc_res = machines[mc_id] - with mc_res.resource.request(priority=priority) as req: - yield req - if mc_res.last_size_run != size: - if mc_res.last_size_run is not None: - yield env.process( - do_split_task(env, mc_id, -2, "Changeover", size, 0, cng) - ) - mc_res.last_size_run = size - - hours_needed = remain_qty / rate - yield env.process( - do_split_task(env, mc_id, order_id, f"Buffer-{mc_id}", size, rate, hours_needed) - ) - - item["current_store"] += can_store - -# ============================================== -# 12) Process หลักในการผลิตเผื่อ -# ============================================== -def buffer_production_flow(env, machines): - done = False - while not done: - done = True - for bf in sorted(buffer_priority, key=lambda x: x["priority"]): - bf_type = bf["type"] - bf_size = bf["size"] - bf_brand = bf["brand"] - item = next((it for it in buffer_config - if it["type"]==bf_type and it["size"]==bf_size and it["brand"]==bf_brand), - None) - if item is None: - continue - - need = item["want_to_store"] - item["current_store"] - if need>0: - old_val = item["current_store"] - yield env.process(buffer_pipeline(env, item, machines, priority=9999)) - if item["current_store"]>old_val: - done = False - -# ============================================== -# 13) run_simulation -# ============================================== -def run_simulation(): - env = simpy.Environment() - machines = create_machines(env) - - # สั่งผลิตตามลำดับ priority ของ order - sorted_orders = sorted(customer_orders, key=lambda x: x["priority"]) - for od in sorted_orders: - env.process(customer_pipeline(env, od, machines)) - - # สั่งผลิต buffer - env.process(buffer_production_flow(env, machines)) - - # run จนถึง 2000 ชั่วโมง (เผื่อพอสำหรับคำสั่งทั้งหมด) - env.run(until=2000) - - return production_log - -# ============================================== -# ฟังก์ชันเสริม: แปลงโครงสร้าง production_log -# ============================================== -from datetime import datetime - -def transform_production_log(prod_log): - """ - แปลงข้อมูลให้อยู่ใน structure ตามต้องการ - - orderID == -1 → "ผลิตเผื่อ" - - orderID == -2 → "เปลี่ยนขนาด" - - Mapping machineID (MC001 → MC1, etc.) - - แก้ format ของเวลาเป็น d/m/YYYY HH:MM - - ตัด space ออกจาก bottleSize เช่น "600 ml" -> "600ml" - """ - - # Mapping ค่าของ MachineID - machine_map = { - "MC001": "MC1", - "MC002": "MC2", - "MC003": "MC3", - "MC004": "MC4" - } - - new_log = [] - for row in prod_log: - # แปลง string "YYYY-mm-dd HH:MM:SS" เป็น datetime object - start_dt = datetime.strptime(row["startTime"], "%Y-%m-%d %H:%M:%S") - finish_dt = datetime.strptime(row["finishTime"], "%Y-%m-%d %H:%M:%S") - - # format วัน-เวลาให้เป็น d/m/YYYY HH:MM - start_time_str = f"{start_dt.day}/{start_dt.month}/{start_dt.year} {start_dt.hour:02d}:{start_dt.minute:02d}" - finish_time_str = f"{finish_dt.day}/{finish_dt.month}/{finish_dt.year} {finish_dt.hour:02d}:{finish_dt.minute:02d}" - - # แปลง orderID ตามเงื่อนไขที่กำหนด - if row["OrderID"] == -1: - order_id = "ผลิตเผื่อ" - elif row["OrderID"] == -2: - order_id = "เปลี่ยนขนาด" - else: - order_id = row["OrderID"] - - new_log.append({ - "queueID": row["QueueID"], - "machineID": machine_map.get(row["MachineID"], row["MachineID"]), # Map ค่า MachineID - "orderID": order_id, # ใช้ค่าเปลี่ยนชื่อ orderID - "pageNumber": 1, # Fix เป็น 1 ตามที่กำหนด - "status": row["status"], - "startTime": start_time_str, - "finishTime": finish_time_str, - "bottleSize": row["bottleSize"].replace(" ", ""), # ตัด space ใน bottleSize เช่น "600 ml" -> "600ml" - "producedQuantity": row["producedQuantity"], - }) - - return new_log - - - - -# ============================================== -# 14) FastAPI -# ============================================== -app = FastAPI() -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:5173"], # หรือใช้ ["*"] ถ้าต้องการอนุญาตทุกที่ - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -@app.post("/run-simulation/") -def run_simulation_endpoint(): - # เรียก simulation - prod_log = run_simulation() - # แปลง log ให้ได้โครงสร้างตามที่ต้องการ - transformed_log = transform_production_log(prod_log) - return { - "message": "Simulation completed", - "production_log": transformed_log, - "buffer_config": buffer_config, - "raw_material_stock": raw_material_stock - } diff --git a/src/queues/AutoQueue/Autoqueuenew.py b/src/queues/AutoQueue/Autoqueuenew.py deleted file mode 100644 index ba87ba86..00000000 --- a/src/queues/AutoQueue/Autoqueuenew.py +++ /dev/null @@ -1,446 +0,0 @@ -########################### -# app.py (FastAPI) -########################### - -import sys -import os -import requests -import json -from fastapi import FastAPI, Body -from fastapi.middleware.cors import CORSMiddleware -from typing import List, Dict, Any, Optional, Tuple -from dataclasses import dataclass, field -from datetime import datetime, timedelta - -########################### -# 0) CONFIG -########################### -if os.name == "nt": - # ให้รองรับภาษาไทย + emoji บน Windows - sys.stdout.reconfigure(encoding='utf-8') - -API_BASE = "http://localhost:4000" # TODO: ปรับตามจริง -URLS = { - "products": f"{API_BASE}/products", - "materials": f"{API_BASE}/materials", - "machines": f"{API_BASE}/machines", - "stock_configs": f"{API_BASE}/stock-config", - # ถ้าจะโพสต์กลับ NestJS ก็เพิ่ม: - # "queues_batch": f"{API_BASE}/queues/batch", - # "production_targets_many": f"{API_BASE}/production-targets/many", -} - -# เราจะใช้ fastAPI -app = FastAPI() -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # หรือกำหนด origin ตามต้องการ - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -########################### -# 1) DATACLASSES -########################### -@dataclass -class ProductData: - ProductID: int - brand: str - size: str - unit: str - status: str - lowStockLevel: int - quantityInStock: int - pricePerUnit: str - name: str = field(init=False) - - def __post_init__(self): - self.name = f"{self.brand} {self.size} ({self.unit})" - - -@dataclass -class MaterialData: - MaterialID: int - name: str - size: str - brand: str - quantityInStock: int - lowStockLevel: int - status: str - ReorderLevel: int - unit: str - pricePerUnit: str - LastUpdate: str - -@dataclass -class IngredientData: - recipeIngredientID: int - quantityNeed: str - material: Dict[str,Any] # ถ้าอยาก map เป็น MaterialData ก็ได้ - -@dataclass -class RecipeData: - recipeID: int - outputItemID: int - outputItemType: str - outputQuantity: int - ingredients: List[IngredientData] - -@dataclass -class MachineDetailData: - MachineDetailID: int - outputRate: int - changOver: float - recipes: List[RecipeData] - -@dataclass -class MachineData: - MachineID: int - name: str - type: str - lastMaintenanceDate: Optional[str] - status: str - notes: Optional[str] - machineDetails: List[MachineDetailData] - -@dataclass -class StockConfigData: - stockConfigID: int - itemID: int - itemType: str - priorityLevel: int - targetStockLevel: int - status: str - lastUpdated: str - page: Any = None - itemName: Optional[str] = None - itemDetail: Optional[Dict[str,Any]] = None - - -########################### -# 2) LOAD & CLEAN FUNCTIONS -########################### - -def load_products() -> List[ProductData]: - resp = requests.get(URLS["products"]) - data_list = resp.json() - cleaned = [] - for d in data_list: - # filter key - f = { - "ProductID": d.get("ProductID",0), - "brand": d.get("brand",""), - "size": d.get("size",""), - "unit": d.get("unit",""), - "status": d.get("status","ปกติ"), - "lowStockLevel": int(d.get("lowStockLevel",0)), - "quantityInStock": int(d.get("quantityInStock",0)), - "pricePerUnit": d.get("pricePerUnit","0.00"), - } - obj = ProductData(**f) - cleaned.append(obj) - return cleaned - -def load_materials() -> List[MaterialData]: - resp = requests.get(URLS["materials"]) - data_list = resp.json() - cleaned = [] - for d in data_list: - f = { - "MaterialID": d.get("MaterialID",0), - "name": d.get("name","Unknown"), - "size": d.get("size","any"), - "brand": d.get("brand","any"), - "quantityInStock": int(d.get("quantityInStock",0)), - "lowStockLevel": int(d.get("lowStockLevel",0)), - "status": d.get("status","ปกติ"), - "ReorderLevel": int(d.get("ReorderLevel",0)), - "unit": d.get("unit","ชิ้น"), - "pricePerUnit": d.get("pricePerUnit","0.00"), - "LastUpdate": d.get("LastUpdate","2023-01-01T00:00:00Z") - } - cleaned.append(MaterialData(**f)) - return cleaned - -def load_machines() -> List[MachineData]: - resp = requests.get(URLS["machines"]) - raw_list = resp.json() - machines = [] - for mc in raw_list: - # filter machine - m = MachineData( - MachineID=mc.get("MachineID",0), - name=mc.get("name","Unknown"), - type=mc.get("type","Unknown"), - lastMaintenanceDate=mc.get("lastMaintenanceDate"), - status=mc.get("status","ACTIVE"), - notes=mc.get("notes"), - machineDetails=[] - ) - md_list = mc.get("machineDetails",[]) - for md in md_list: - detail = MachineDetailData( - MachineDetailID=md.get("MachineDetailID",0), - outputRate=md.get("outputRate",1000), - changOver=float(md.get("changOver",0)), - recipes=[] - ) - for r in md.get("recipes",[]): - recipe = RecipeData( - recipeID=r.get("recipeID",0), - outputItemID=r.get("outputItemID",0), - outputItemType=r.get("outputItemType","MATERIAL"), - outputQuantity=r.get("outputQuantity",1), - ingredients=[] - ) - for ing in r.get("ingredients",[]): - iobj = IngredientData( - recipeIngredientID=ing.get("recipeIngredientID",0), - quantityNeed=ing.get("quantityNeed","1.0"), - material=ing.get("material",{}) - ) - recipe.ingredients.append(iobj) - detail.recipes.append(recipe) - m.machineDetails.append(detail) - machines.append(m) - return machines - -def load_stockconfigs() -> List[StockConfigData]: - resp = requests.get(URLS["stock_configs"]) - data_list = resp.json() - cleaned = [] - for sc in data_list: - # rename key - scid = sc.get("StockConfigID",0) - f = { - "stockConfigID": scid, - "itemID": sc.get("itemID",0), - "itemType": sc.get("itemType","PRODUCT"), - "priorityLevel": int(sc.get("priorityLevel",1)), - "targetStockLevel": int(sc.get("targetStockLevel",0)), - "status": sc.get("status","Active"), - "lastUpdated": sc.get("lastUpdated","2023-01-01T00:00:00Z"), - "page": sc.get("page"), - "itemName": sc.get("itemName"), - "itemDetail": sc.get("itemDetail"), - } - cleaned.append(StockConfigData(**f)) - return cleaned - -########################### -# 3) BFS + SCHEDULING -########################### -@dataclass -class PipelineStep: - machine_id: int - machine_detail_id: int - item_id: int - item_type: str - -# dict -> (rate, co) -machine_detail_map: Dict[Tuple[int,int],Tuple[int,float]] = {} - -def build_machine_detail_map(machines: List[MachineData]): - machine_detail_map.clear() - for mc in machines: - for md in mc.machineDetails: - machine_detail_map[(mc.MachineID, md.MachineDetailID)] = ( - md.outputRate, - md.changOver - ) - return machine_detail_map - -def find_recipe_for_item(machines: List[MachineData], item_id: int, item_type: str): - found = [] - for mc in machines: - for md in mc.machineDetails: - for r in md.recipes: - if r.outputItemID == item_id and r.outputItemType.upper() == item_type.upper(): - found.append((mc, md, r)) - return found - -def build_pipeline(machines: List[MachineData], item_id:int, item_type:str, visited=None)->List[PipelineStep]: - if visited is None: - visited = set() - key_ = f"{item_type}:{item_id}" - if key_ in visited: - return [] - visited.add(key_) - - matched = find_recipe_for_item(machines, item_id, item_type) - if not matched: - return [] # raw - - # pick first - mc, md, recipe = matched[0] - # BFS for ingredient - for ing in recipe.ingredients: - mat_id = ing.material.get("MaterialID",0) - # assum item_type= "MATERIAL" - build_pipeline(machines, mat_id, "MATERIAL", visited) - - # add final step - step = PipelineStep(machine_id=mc.MachineID, - machine_detail_id=md.MachineDetailID, - item_id=item_id, - item_type=item_type) - return [step] - -# scheduling -WORKBLOCKS = [(8,12),(13,17)] -MIN_BLOCK_LEFT = 0.5 - -def is_weekend(dt: datetime)->bool: - return dt.weekday() >= 5 - -def next_work_time(dt: datetime)->datetime: - while True: - if is_weekend(dt): - add_days = 7 - dt.weekday() - dt = datetime(dt.year, dt.month, dt.day, 8,0,0) + timedelta(days=add_days) - return dt - hr = dt.hour - if hr < 8: - return datetime(dt.year, dt.month, dt.day, 8,0,0) - if 8<=hr<12: - return dt - if hr<13: - return datetime(dt.year, dt.month, dt.day, 13,0,0) - if hr<17: - return dt - dt = datetime(dt.year, dt.month, dt.day, 8,0,0) + timedelta(days=1) - -def get_block_end(dt: datetime)->datetime: - hr = dt.hour - if 8<=hr<12: - return datetime(dt.year, dt.month, dt.day, 12,0,0) - else: - return datetime(dt.year, dt.month, dt.day, 17,0,0) - -def get_rate_co(machine_id:int, md_id:int)->Tuple[int,float]: - if (machine_id,md_id) in machine_detail_map: - return machine_detail_map[(machine_id,md_id)] - return (1000,0) - -def schedule_pipeline(pipeline:List[PipelineStep], qty:int, start_dt:datetime, order_id:int): - """Return (events, finishTime)""" - events = [] - current_dt = start_dt - for step in pipeline: - rate, co = get_rate_co(step.machine_id, step.machine_detail_id) - if rate<=0: - continue - - hours_needed = qty / rate - remain = hours_needed - while remain>0: - current_dt = next_work_time(current_dt) - block_end = get_block_end(current_dt) - block_left = (block_end - current_dt).total_seconds()/3600 - if block_left<MIN_BLOCK_LEFT: - current_dt = block_end - continue - used = min(remain, block_left) - st = current_dt - ft = st + timedelta(hours=used) - remain-=used - current_dt=ft - - produced = rate*used - events.append({ - "startTime": st.isoformat(), - "finishTime": ft.isoformat(), - "machineID": step.machine_id, - "itemID": step.item_id, - "itemType": step.item_type, - "producedQuantity": int(produced), - "orderID": order_id, - # "status": "Pending" # TODO: อาจใส่ - # "bottleSize": "???" # TODO: ถ้าอยาก mapping size/brand - }) - return events, current_dt - - -########################### -# 4) ตัวอย่าง Endpoint FastAPI -########################### -@app.post("/run-simulation") -def run_simulation_endpoint(payload: dict = Body(...)): - """ - ตัวอย่าง endpoint ที่ front-end จะยิงมา - เช่น JSON: { - "itemID": 12, - "itemType": "MATERIAL", - "qty": 5000, - "startDate": "2025-03-25T08:00:00", - "orderID": 1 - } - """ - - item_id = payload.get("itemID",12) - item_type = payload.get("itemType","MATERIAL") - qty = payload.get("qty",1000) - startDateStr = payload.get("startDate","2025-03-25T08:00:00") - order_id = payload.get("orderID",1) - - start_dt = datetime.fromisoformat(startDateStr) - - # 1) load data - products = load_products() - materials = load_materials() - machines = load_machines() - stockconfigs = load_stockconfigs() - - # 2) build machine_detail_map - build_machine_detail_map(machines) - - # TODO: ตัวอย่างถ้ามี Logic เช็คของใน stock ว่าพอไหม => skip ถ้าไม่พอ - # (ยกตัวอย่างเล็กๆ) - # material_2 = next((m for m in materials if m.MaterialID==2), None) - # if material_2 and material_2.quantityInStock<qty: - # return {"message":"Not enough raw material for item #2. Skip."} - - # 3) BFS pipeline - pipeline = build_pipeline(machines, item_id, item_type) - - # 4) schedule - events, finish_dt = schedule_pipeline(pipeline, qty, start_dt, order_id) - - # 5) สร้าง ProductionTarget mock-up - production_target = { - "OrderID": order_id, - "itemID": item_id, - "itemType": item_type, - "TargetProduced": qty, - "ActualProduced": 0, - "Status": "กำลังรอ", - "Date": start_dt.strftime("%Y-%m-%d"), - # "startTime": start_dt.isoformat(), - # "endTime": finish_dt.isoformat() - } - - # 6) ถ้าจะ POST ต่อไป NestJS => uncomment - # r1 = requests.post(URLS["production_targets_many"], json={"productionTargets":[production_target]}) - # r2 = requests.post(URLS["queues_batch"], json=events) - # etc... - - return { - "message": "Simulation completed", - "pipelineSteps": [f"{s.machine_id}:{s.machine_detail_id} => item {s.item_id}" for s in pipeline], - "queueEvents": events, - "productionTargetExample": production_target, - "finishedAt": finish_dt.isoformat() - } - - -########################### -# 5) ตัวอย่างวิธีรัน -########################### -# - เซฟไฟล์นี้เป็น app.py -# - รันด้วยคำสั่ง: uvicorn app:app --reload -# -# แล้วลอง POST มาที่: -# http://127.0.0.1:8000/run-simulation -# ด้วย JSON ตามตัวอย่าง - diff --git a/src/queues/AutoQueue/Log copy.py b/src/queues/AutoQueue/Log copy.py deleted file mode 100644 index dae9ed6b..00000000 --- a/src/queues/AutoQueue/Log copy.py +++ /dev/null @@ -1,438 +0,0 @@ -import requests -import json -import sys -import os -from dataclasses import dataclass, field -from typing import List, Optional, Dict, Any - -# ======================================================= -# 0) CONFIG -# ======================================================= -if os.name == "nt": - sys.stdout.reconfigure(encoding='utf-8') - -API_BASE = "http://localhost:4000" -URLS = { - "products": f"{API_BASE}/products", - "materials": f"{API_BASE}/materials", - "machines": f"{API_BASE}/machines", - "stock_configs": f"{API_BASE}/stock-config", - "orders": f"{API_BASE}/orders", - "order_prio": f"{API_BASE}/orderpiorities" -} - -# ======================================================= -# 1) DATACLASSES -# ======================================================= - -# ---------- 1.1 Product ---------- -@dataclass -class ProductData: - ProductID: int - brand: str - size: str - unit: str - status: str - lowStockLevel: int - quantityInStock: int - pricePerUnit: str # อาจเป็น string - name: str = field(init=False) - - def __post_init__(self): - # เช่น รวม brand + size + unit เป็น name - self.name = f"{self.brand} {self.size} ({self.unit})" - -# ---------- 1.2 Material ---------- -@dataclass -class MaterialData: - MaterialID: int - name: str - size: str - brand: str - quantityInStock: int - lowStockLevel: int - status: str - ReorderLevel: int - unit: str - pricePerUnit: str - LastUpdate: str - display_name: str = field(init=False) - - def __post_init__(self): - self.display_name = f"{self.name} {self.size} {self.brand} ({self.unit})" - -# ---------- 1.3 Ingredient (Recipe_Ingredient) ---------- -@dataclass -class IngredientData: - recipeIngredientID: int - quantityNeed: str # เช่น "1.000000" - material: MaterialData - -# ---------- 1.4 Recipe ---------- -@dataclass -class RecipeData: - recipeID: int - outputItemID: int - outputItemType: str # 'PRODUCT' | 'MATERIAL' - outputQuantity: int - ingredients: List[IngredientData] - -# ---------- 1.5 MachineDetail ---------- -@dataclass -class MachineDetailData: - MachineDetailID: int - outputRate: int - changOver: float - recipes: List[RecipeData] - -# ---------- 1.6 Machine ---------- -@dataclass -class MachineData: - MachineID: int - name: str - type: str - lastMaintenanceDate: Optional[str] - status: str - notes: Optional[str] - machineDetails: List[MachineDetailData] - -# ---------- 1.7 StockConfigData ---------- -@dataclass -class StockConfigData: - stockConfigID: int - itemID: int - itemType: str - priorityLevel: int - targetStockLevel: int - status: str - lastUpdated: str - # สมมุติ page เป็นอะไรก็ได้ (object/dict) หรือ int - page: Any = None - itemName: Optional[str] = None - itemDetail: Optional[Dict[str, Any]] = None - -# ---------- 1.8 Order ---------- -@dataclass -class OrderData: - OrderID: int - status: str - quantity: int - totalPriceall: int - # คุณอาจใส่ฟิลด์อื่น เช่น date/time, customerID, ฯลฯ - -# ---------- 1.9 OrderPriority ---------- -@dataclass -class OrderPriorityData: - orderPriorityID: int - Priority: int - OrderID: int - PageID: Optional[int] = None # ให้มี default = None - -# ======================================================= -# 2) HELPER LOAD & CLEAN FUNCTIONS -# ======================================================= - -def rename_keys_if_needed(d: dict, old_key: str, new_key: str): - """ถ้าใน d มี old_key ให้ย้ายไป new_key""" - if old_key in d: - d[new_key] = d[old_key] - del d[old_key] - -# ---------- load & clean Products ---------- -def load_and_clean_products() -> List[ProductData]: - resp = requests.get(URLS["products"]) - raw_list = resp.json() - - allowed_keys = { - "ProductID","brand","size","unit", - "status","lowStockLevel","quantityInStock", - "pricePerUnit" - } - cleaned_list = [] - for r in raw_list: - # filter - f = {k:r[k] for k in r if k in allowed_keys} - # fill defaults - if "status" not in f: - f["status"] = "ปกติ" - if "lowStockLevel" not in f: - f["lowStockLevel"] = 0 - if "quantityInStock" not in f: - f["quantityInStock"] = 0 - if "pricePerUnit" not in f: - f["pricePerUnit"] = "0.00" - # convert if needed - f["lowStockLevel"] = int(f["lowStockLevel"]) - f["quantityInStock"] = int(f["quantityInStock"]) - # create dataclass - obj = ProductData(**f) - cleaned_list.append(obj) - return cleaned_list - -# ---------- load & clean Materials ---------- -def load_and_clean_materials() -> List[MaterialData]: - resp = requests.get(URLS["materials"]) - raw_list = resp.json() - - allowed_keys = { - "MaterialID","name","size","brand","quantityInStock","lowStockLevel", - "status","ReorderLevel","unit","pricePerUnit","LastUpdate" - } - cleaned_list = [] - for r in raw_list: - f = {k:r[k] for k in r if k in allowed_keys} - - # fill default - if "status" not in f: - f["status"] = "ปกติ" - if "lowStockLevel" not in f: - f["lowStockLevel"] = 0 - if "quantityInStock" not in f: - f["quantityInStock"] = 0 - if "ReorderLevel" not in f: - f["ReorderLevel"] = 0 - if "pricePerUnit" not in f: - f["pricePerUnit"] = "0.00" - if "LastUpdate" not in f: - f["LastUpdate"] = "2023-01-01T00:00:00.000Z" - - # cast - f["lowStockLevel"] = int(f["lowStockLevel"]) - f["quantityInStock"] = int(f["quantityInStock"]) - f["ReorderLevel"] = int(f["ReorderLevel"]) - - obj = MaterialData(**f) - cleaned_list.append(obj) - return cleaned_list - -# ---------- load & clean Machines (มี machineDetails -> recipes -> ingredients) ---------- -def load_and_clean_machines() -> List[MachineData]: - resp = requests.get(URLS["machines"]) - raw_list = resp.json() - - # ฟิลด์ของ Machine - machine_allowed_keys = { - "MachineID","name","type","lastMaintenanceDate","status","notes","machineDetails" - } - - # ฟิลด์ของ MachineDetail - detail_allowed_keys = { - "MachineDetailID","outputRate","changOver","recipes" - } - - # ฟิลด์ของ Recipe - recipe_allowed_keys = { - "recipeID","outputItemID","outputItemType","outputQuantity","ingredients" - } - - # ฟิลด์ของ Ingredient - ingredient_allowed_keys = { - "recipeIngredientID","quantityNeed","material" - } - - # ฟิลด์ของ Material ภายใน Ingredient - ing_mat_allowed_keys = { - "MaterialID","name","size","brand","quantityInStock","lowStockLevel", - "status","ReorderLevel","unit","pricePerUnit","LastUpdate" - } - - cleaned_machines = [] - for mc in raw_list: - # filter machine level - m = {k:mc[k] for k in mc if k in machine_allowed_keys} - - # ถ้าไม่มี machineDetails ให้เป็น [] - if "machineDetails" not in m or not m["machineDetails"]: - m["machineDetails"] = [] - - # ตอนนี้ m["machineDetails"] เป็น list - new_details = [] - for md in m["machineDetails"]: - # filter detail - md_f = {k:md[k] for k in md if k in detail_allowed_keys} - - # fill default - if "changOver" not in md_f: - md_f["changOver"] = 0.0 - - # recipes - if "recipes" not in md_f or not md_f["recipes"]: - md_f["recipes"] = [] - - new_recipes = [] - for r in md_f["recipes"]: - r_f = {k:r[k] for k in r if k in recipe_allowed_keys} - # fill default - if "ingredients" not in r_f or not r_f["ingredients"]: - r_f["ingredients"] = [] - - new_ingredients = [] - for ing in r_f["ingredients"]: - ing_f = {k:ing[k] for k in ing if k in ingredient_allowed_keys} - - # material inside ingredient - if "material" in ing_f and ing_f["material"]: - mat_in_ing = ing_f["material"] - mat_filtered = {mk:mat_in_ing[mk] for mk in mat_in_ing if mk in ing_mat_allowed_keys} - # fill default if needed - if "status" not in mat_filtered: - mat_filtered["status"] = "ปกติ" - if "lowStockLevel" not in mat_filtered: - mat_filtered["lowStockLevel"] = 0 - if "quantityInStock" not in mat_filtered: - mat_filtered["quantityInStock"] = 0 - ing_f["material"] = mat_filtered - else: - ing_f["material"] = { - "MaterialID":0,"name":"Unnamed" - } - - new_ingredients.append(IngredientData(**ing_f)) - r_f["ingredients"] = new_ingredients - - new_recipes.append(RecipeData(**r_f)) - md_f["recipes"] = new_recipes - - new_details.append(MachineDetailData(**md_f)) - m["machineDetails"] = new_details - - # fill default - if "notes" not in m: - m["notes"] = None - if "lastMaintenanceDate" not in m: - m["lastMaintenanceDate"] = None - - # cast if needed - obj = MachineData(**m) - cleaned_machines.append(obj) - - return cleaned_machines - -# ---------- load & clean StockConfigs ---------- -def load_and_clean_stockconfigs() -> List[StockConfigData]: - resp = requests.get(URLS["stock_configs"]) - raw_list = resp.json() - - allowed_keys = { - "stockConfigID","itemID","itemType","priorityLevel","targetStockLevel", - "status","lastUpdated","page","itemName","itemDetail" - } - cleaned_list = [] - for sc in raw_list: - f = {k:sc[k] for k in sc if k in allowed_keys} - - # fill default - if "stockConfigID" not in f: - f["stockConfigID"] = 0 - if "status" not in f: - f["status"] = "Active" - if "priorityLevel" not in f: - f["priorityLevel"] = 1 - if "targetStockLevel" not in f: - f["targetStockLevel"] = 0 - - # cast - f["priorityLevel"] = int(f["priorityLevel"]) - f["targetStockLevel"] = int(f["targetStockLevel"]) - - obj = StockConfigData(**f) - cleaned_list.append(obj) - return cleaned_list - -# ---------- load & clean Orders ---------- -def load_and_clean_orders() -> List[OrderData]: - resp = requests.get(URLS["orders"]) - raw_list = resp.json() - - allowed_keys = { - "OrderID","status","quantity","totalPriceall" - } - cleaned_list = [] - for od in raw_list: - f = {k:od[k] for k in od if k in allowed_keys} - - # fill default - if "status" not in f: - f["status"] = "NEW" - if "quantity" not in f: - f["quantity"] = 0 - if "totalPriceall" not in f: - f["totalPriceall"] = 0 - - obj = OrderData(**f) - cleaned_list.append(obj) - return cleaned_list - -# ---------- load & clean OrderPriorities ---------- -def load_and_clean_order_prio() -> List[OrderPriorityData]: - resp = requests.get(URLS["order_prio"]) - if resp.status_code != 200: - # เผื่อกรณีไม่มี API ให้ - print("No order priorities found or 404 not found") - return [] - - raw_list = resp.json() - allowed_keys = { - "orderPriorityID","Priority","OrderID","PageID" - } - cleaned_list = [] - for op in raw_list: - f = {k:op[k] for k in op if k in allowed_keys} - # fill default - if "Priority" not in f: - f["Priority"] = 999 - if "OrderID" not in f: - f["OrderID"] = 0 - - obj = OrderPriorityData(**f) - cleaned_list.append(obj) - return cleaned_list - - -# ======================================================= -# 3) MAIN DEMO -# ======================================================= -def main(): - print("=== Loading & Cleaning Data from NestJS ===") - - products = load_and_clean_products() - print(">>> Products:") - for p in products: - print(p) - print("==========================================\n") - - materials = load_and_clean_materials() - print(">>> Materials:") - for m in materials: - print(m) - print("==========================================\n") - - machines = load_and_clean_machines() - print(">>> Machines:") - for mc in machines: - print(mc) - print("==========================================\n") - - stockconfigs = load_and_clean_stockconfigs() - print(">>> StockConfigs:") - for sc in stockconfigs: - print(sc) - print("==========================================\n") - - orders = load_and_clean_orders() - print(">>> Orders:") - for od in orders: - print(od) - print("==========================================\n") - - order_prios = load_and_clean_order_prio() - print(">>> OrderPriorities:") - for op in order_prios: - print(op) - print("==========================================\n") - - print("All data cleaned successfully! 🎉") - - -if __name__ == "__main__": - main() diff --git a/src/queues/AutoQueue/Log.py b/src/queues/AutoQueue/Log.py deleted file mode 100644 index 9557e43d..00000000 --- a/src/queues/AutoQueue/Log.py +++ /dev/null @@ -1,45 +0,0 @@ -import requests -import json -import sys -import os - -# ให้รองรับภาษาไทยและ emoji บน Windows -if os.name == "nt": - sys.stdout.reconfigure(encoding="utf-8") - -API_BASE = "http://localhost:4000" -URLS = { - "products": f"{API_BASE}/products", - "materials": f"{API_BASE}/materials", - "machines": f"{API_BASE}/machines", - "stock_configs": f"{API_BASE}/stock-config", -} - -def log_json_from_api(name: str, url: str): - print(f"\n📦 ดึงข้อมูลจาก API: {name} -> {url}") - try: - response = requests.get(url) - response.raise_for_status() - data = response.json() - - # แสดงจำนวนรายการ - print(f"✅ ได้ข้อมูลทั้งหมด: {len(data)} รายการ") - - # แสดงเฉพาะ 1-2 รายการแรก - preview_count = min(2, len(data)) - print(f"\n🔍 แสดงตัวอย่าง {preview_count} รายการแรก:") - for i in range(preview_count): - print(f"\n🔹 {name} #{i+1}") - print(json.dumps(data[i], indent=2, ensure_ascii=False)) - - except Exception as e: - print(f"❌ เกิดข้อผิดพลาดในการดึง {name}: {e}") - -def main(): - log_json_from_api("Products", URLS["products"]) - log_json_from_api("Materials", URLS["materials"]) - log_json_from_api("Machines", URLS["machines"]) - log_json_from_api("Stock Configs", URLS["stock_configs"]) - -if __name__ == "__main__": - main() diff --git a/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-310.pyc b/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-310.pyc deleted file mode 100644 index 890596d9249b96652c7a5c93ae31b40707cb30d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10637 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�C5g@<M2Kya*U>Ajg3=IPjpoux~5Oon~u>BT8Q8Xxfaz?R( z2cSJ&Kno<mizqx@qS&FyA&Lt$p6pkeo0M7v(F^tg$njuPAZBoZvp8tTxu^g%L|p`) zA_7kqfv1f?^GUbZax?QvOHzyCKt&UCdTPln_M+5+oW$f*O-|6Pd439LygP~;njGSD zLCXX3Qp-W2x0s9a%Zm`hFnr+g_jr(9@x>)YQT)&$ZkPb1T?j6*zyzqgy(Ljnl$cix zN<HyNbx%%y`Z`bn2CDIyctE4sOpI)QSlBrj1(*aF^_ca*gEKs!$vh?&CN@SP@YpvS znB)WXxW2RS6oJ}kRiYUsB?VUc`rsaJMt*ULm8qe*amX!3Elrjv-o%`o{POtxqRjNn zyy7T92sgPXH6=B#1hggvrXn}BBqKki7{<*=O-xBGDy|aHFDlK`EzZn^)(`ri@hni4 z2gabb3uv4aT%gK;>%C@%TG;3wV+v^2o~eX6g=r2;FoPzuUzJEO+$4o$*c6B+%PscY z)Z*gA^i*)QP_zt`Z1O;<gaurdgZ+sl%2c!kRQ>S712z>LhmaZ{5%A)u^#y2VupmD( zujD-_+(GpKsISVz1PlA3ogf?4z*QSKVSy)zi}FEMv8N>#mn0Ts7J+K|C~+uHFBiT- zNiR9SsJI9e7T_U)B9QBgR)YKi_RLZc3o)|^3a=vYRAw1S0+a?2gN@+121r1FniAk~ zMM&)m8iRmzUqF2maL*dthzD1zh>*I)oLEp0#g~|p5|40T6iY#VaS7OuNa2JCEDjsU zoShx0IZ^xuG~>b`z{tS}>f<x9gTjb~5i}79N`Wjtn7AP#kR}roBTNLM0$B~R2+Knz zeULsdW@7uv#1CeH(=-z!%TFdQR6ff$K1n`CRLJt3M*!p|WX$rNg&*P)7Df&x4i*ks rJ{}f!Fk6&^lS71qmqV7rgu{YEf=7gdiOm8ulLWymjBLL-IQSR=X?WMO diff --git a/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-311.pyc b/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-311.pyc deleted file mode 100644 index cfbd03aa3caea8a42257690736d8716b154f84e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18940 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 diff --git a/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-313.pyc b/src/queues/AutoQueue/__pycache__/Autoqueue.cpython-313.pyc deleted file mode 100644 index 7918080c8ceba2fa002c7a2a0f4d7019e9a94089..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16952 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 C?%*;2 diff --git a/src/queues/AutoQueue/cleandata.py b/src/queues/AutoQueue/cleandata.py deleted file mode 100644 index 34d8c578..00000000 --- a/src/queues/AutoQueue/cleandata.py +++ /dev/null @@ -1,228 +0,0 @@ -import requests -import sys -import os -from dataclasses import dataclass, field -from typing import List, Optional - -# ปรับ encoding สำหรับ Windows ให้รองรับภาษาไทยและ emoji -if os.name == "nt": - sys.stdout.reconfigure(encoding="utf-8") - -# กำหนด API endpoints -API_BASE = "http://localhost:4000" -URLS = { - "products": f"{API_BASE}/products", - "materials": f"{API_BASE}/materials", - "machines": f"{API_BASE}/machines", - "stock_configs": f"{API_BASE}/stock-config", -} - -# ======================== -# Data Model Definitions -# ======================== - -@dataclass -class Product: - ProductID: int - brand: str - size: str - unit: str - status: str - lowStockLevel: int - quantityInStock: int - pricePerUnit: str # API ส่งมาเป็น string - name: str = field(init=False) - - def __post_init__(self): - self.name = f"{self.brand} {self.size} ({self.unit})" - -@dataclass -class Material: - MaterialID: int - name: str - size: str - brand: str - quantityInStock: int - lowStockLevel: int - status: str - ReorderLevel: int - unit: str - pricePerUnit: str # API ส่งมาเป็น string - LastUpdate: str - display_name: str = field(init=False) - - def __post_init__(self): - self.display_name = f"{self.name} {self.size} {self.brand} ({self.unit})" - -@dataclass -class Ingredient: - recipeIngredientID: int - quantityNeed: str # API ส่งมาเป็น string (เช่น "1.000000") - material: Material - -@dataclass -class Recipe: - recipeID: int - outputItemID: int - outputItemType: str # คาดว่าจะเป็น "PRODUCT" หรือ "MATERIAL" - outputQuantity: int - ingredients: List[Ingredient] - -@dataclass -class MachineDetail: - MachineDetailID: int - outputRate: int - changOver: Optional[float] - recipes: List[Recipe] - -@dataclass -class Machine: - MachineID: int - name: str - type: str - lastMaintenanceDate: Optional[str] - status: str - notes: Optional[str] - machineDetails: List[MachineDetail] - -@dataclass -class StockConfig: - stockConfigID: int - itemID: int - itemType: str # "PRODUCT" หรือ "MATERIAL" - targetStockLevel: int - status: str - lastUpdated: str - -@dataclass -class CleanDataSet: - products: List[Product] - materials: List[Material] - machines: List[Machine] - stock_configs: List[StockConfig] - -# ======================== -# Loader Functions -# ======================== - -def load_products() -> List[Product]: - products_data = requests.get(URLS["products"]).json() - # JSON ควรมี: ProductID, brand, size, unit, status, lowStockLevel, quantityInStock, pricePerUnit - return [Product(**p) for p in products_data] - -def load_materials() -> List[Material]: - materials_data = requests.get(URLS["materials"]).json() - # JSON ควรมี: MaterialID, name, size, brand, quantityInStock, lowStockLevel, status, ReorderLevel, unit, pricePerUnit, LastUpdate - return [Material(**m) for m in materials_data] - -def load_machines() -> List[Machine]: - machines_data = requests.get(URLS["machines"]).json() - machines = [] - for m in machines_data: - machine_details = [] - for md in m.get("machineDetails", []): - recipes = [] - for r in md.get("recipes", []): - ingredients = [] - for ing in r.get("ingredients", []): - # สร้าง Material object จากข้อมูลใน ingredient - mat_obj = Material(**ing["material"]) - ingredient_obj = Ingredient( - recipeIngredientID=ing["recipeIngredientID"], - quantityNeed=ing["quantityNeed"], - material=mat_obj - ) - ingredients.append(ingredient_obj) - recipe_obj = Recipe( - recipeID=r["recipeID"], - outputItemID=r["outputItemID"], - outputItemType=r["outputItemType"], - outputQuantity=r["outputQuantity"], - ingredients=ingredients - ) - recipes.append(recipe_obj) - md_obj = MachineDetail( - MachineDetailID=md["MachineDetailID"], - outputRate=md["outputRate"], - changOver=md.get("changOver"), - recipes=recipes - ) - machine_details.append(md_obj) - machine_obj = Machine( - MachineID=m["MachineID"], - name=m["name"], - type=m["type"], - lastMaintenanceDate=m.get("lastMaintenanceDate"), - status=m["status"], - notes=m.get("notes"), - machineDetails=machine_details - ) - machines.append(machine_obj) - return machines - -def load_stock_configs() -> List[StockConfig]: - configs_data = requests.get(URLS["stock_configs"]).json() - # JSON ควรมี: stockConfigID, itemID, itemType, targetStockLevel, status, lastUpdated - return [StockConfig(**cfg) for cfg in configs_data] - -def load_clean_dataset() -> CleanDataSet: - products = load_products() - materials = load_materials() - machines = load_machines() - stock_configs = load_stock_configs() - return CleanDataSet( - products=products, - materials=materials, - machines=machines, - stock_configs=stock_configs - ) - -# ======================== -# ตัวอย่างการใช้งานและแสดงผล (Algorithm Sample) -# ======================== - -def main(): - print("Loading clean dataset...") - data = load_clean_dataset() - - # แสดง mapping ของ Products - print("\n=== Products Mapping ===") - for p in data.products: - print(f"ProductID: {p.ProductID} -> {p.name}") - - # แสดง mapping ของ Materials - print("\n=== Materials Mapping ===") - for m in data.materials: - print(f"MaterialID: {m.MaterialID} -> {m.display_name}") - - # แสดงสรุปของ Machine พร้อมรายละเอียด - print("\n=== Machines Summary ===") - for machine in data.machines: - print(f"\nMachineID: {machine.MachineID}, Name: {machine.name}, Type: {machine.type}, Status: {machine.status}") - for md in machine.machineDetails: - print(f" MachineDetailID: {md.MachineDetailID}, OutputRate: {md.outputRate}, ChangeOver: {md.changOver}") - for r in md.recipes: - # Map ชื่อ output item จาก Product หรือ Material ตาม outputItemType - if r.outputItemType.upper() == "PRODUCT": - output_name = next((p.name for p in data.products if p.ProductID == r.outputItemID), "Unknown") - elif r.outputItemType.upper() == "MATERIAL": - output_name = next((m.display_name for m in data.materials if m.MaterialID == r.outputItemID), "Unknown") - else: - output_name = "Unknown" - print(f" RecipeID: {r.recipeID}, Produces: {output_name} ({r.outputItemType}) x {r.outputQuantity}") - for ing in r.ingredients: - print(f" Ingredient: {ing.material.display_name} x {ing.quantityNeed}") - - # แสดงข้อมูล Stock Config ที่ map กับ Product/Material - print("\n=== Stock Configurations ===") - for sc in data.stock_configs: - if sc.itemType.upper() == "PRODUCT": - item_name = next((p.name for p in data.products if p.ProductID == sc.itemID), "Unknown") - elif sc.itemType.upper() == "MATERIAL": - item_name = next((m.display_name for m in data.materials if m.MaterialID == sc.itemID), "Unknown") - else: - item_name = "Unknown" - print(f"ConfigID: {sc.stockConfigID}, Item: {item_name} (ItemID: {sc.itemID}, {sc.itemType}), TargetStock: {sc.targetStockLevel}, Status: {sc.status}, LastUpdated: {sc.lastUpdated}") - -if __name__ == "__main__": - main() diff --git a/src/queues/AutoQueue/requirements.txt b/src/queues/AutoQueue/requirements.txt deleted file mode 100644 index 1af22b69..00000000 --- a/src/queues/AutoQueue/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -simpy -pandas -matplotlib -numpy -fastapi -uvicorn \ No newline at end of file diff --git a/src/queues/AutoQueue/testAllapi.py b/src/queues/AutoQueue/testAllapi.py deleted file mode 100644 index d4d4bc09..00000000 --- a/src/queues/AutoQueue/testAllapi.py +++ /dev/null @@ -1,132 +0,0 @@ -import requests -import sys -import os - -# ปรับ encoding สำหรับ Windows ให้รองรับภาษาไทยและ emoji -if os.name == "nt": - sys.stdout.reconfigure(encoding="utf-8") - -# กำหนด API endpoints -API_BASE = "http://localhost:4000" -URLS = { - "products": f"{API_BASE}/products", - "materials": f"{API_BASE}/materials", - "machines": f"{API_BASE}/machines", - "stock_configs": f"{API_BASE}/stock-config", -} - -# Dictionary สำหรับเก็บ mapping -# สำหรับ Products: key คือ ProductID, value คือ ชื่อผลิตภัณฑ์ -product_map = {} -# สำหรับ Materials: key คือ MaterialID, value คือ ชื่อวัสดุ -material_map = {} - -def preload_items(): - """โหลดข้อมูล Products และ Materials ก่อน เพื่อใช้ในการ map itemID""" - print("📥 กำลังโหลด Products...") - try: - products = requests.get(URLS["products"]).json() - for p in products: - # ตัวอย่างชื่อผลิตภัณฑ์: "A 350 ml (ขวด)" - name = f"{p['brand']} {p['size']} ({p['unit']})" - product_map[p['ProductID']] = name - except Exception as e: - print("❌ ไม่สามารถโหลด Products ได้:", e) - - print("📥 กำลังโหลด Materials...") - try: - materials = requests.get(URLS["materials"]).json() - for m in materials: - # ตัวอย่างชื่อวัสดุ: "Preform (ขวด) any any (ขวด)" - name = f"{m['name']} {m['size']} {m['brand']} ({m['unit']})" - material_map[m['MaterialID']] = name - except Exception as e: - print("❌ ไม่สามารถโหลด Materials ได้:", e) - -def log_item_mappings(): - """แสดงผล mapping ของ Products และ Materials เพื่อตรวจสอบ itemID กับ itemType""" - print("\n==== Mapping ของ Products ====") - for pid, name in product_map.items(): - print(f" - ProductID: {pid} -> {name}") - - print("\n==== Mapping ของ Materials ====") - for mid, name in material_map.items(): - print(f" - MaterialID: {mid} -> {name}") - -def log_machines(): - """แสดงข้อมูลของ Machines พร้อมรายละเอียด (machineDetails + recipes)""" - try: - machines = requests.get(URLS["machines"]).json() - except Exception as e: - print("❌ ไม่สามารถโหลด Machines ได้:", e) - return - - # แสดงข้อมูลสรุปของแต่ละเครื่อง - for i, machine in enumerate(machines, 1): - print(f"\n🚀 Machine #{i}") - print(f" - MachineID: {machine['MachineID']}") - print(f" - Name: {machine['name']}") - print(f" - Type: {machine['type']}") - print(f" - Status: {machine['status']}") - - # แสดงรายละเอียด machineDetails และ recipes - print("\n🔧 รายละเอียดเครื่องจักร (MachineDetails + Recipes):") - for machine in machines: - for detail in machine.get("machineDetails", []): - print(f"\n🛠️ MachineDetailID: {detail['MachineDetailID']}") - print(f" - Machine: {machine['name']} (ID {machine['MachineID']})") - print(f" - OutputRate: {detail['outputRate']} units/hr") - print(f" - ChangeOver: {detail['changOver']} min") - for recipe in detail.get("recipes", []): - output_id = recipe["outputItemID"] - output_type = recipe["outputItemType"] - # ตรวจสอบ itemType เพื่อ map ชื่อผลิตภัณฑ์หรือวัสดุ - if output_type.upper() == "PRODUCT": - output_name = product_map.get(output_id, "❓ไม่ทราบชื่อ") - elif output_type.upper() == "MATERIAL": - output_name = material_map.get(output_id, "❓ไม่ทราบชื่อ") - else: - output_name = "ไม่ทราบ" - print(f"\n 📦 RecipeID: {recipe['recipeID']}") - print(f" - ผลิต: {output_name} ({output_type})") - print(f" - จำนวนที่ผลิต: {recipe['outputQuantity']}") - print(" - วัตถุดิบ:") - for ing in recipe.get("ingredients", []): - mat = ing["material"] - # สามารถเพิ่มข้อมูลอื่นๆ ของวัสดุจาก mapping ถ้าต้องการ - print(f" · {mat['name']} ({mat['size']}, {mat['brand']}) x {ing['quantityNeed']}") - -def log_stock_configs(): - """แสดงข้อมูล Stock Config พร้อมแปล itemID และ itemType ให้เข้าใจง่าย""" - try: - configs = requests.get(URLS["stock_configs"]).json() - except Exception as e: - print("❌ ไม่สามารถโหลด Stock Configs ได้:", e) - return - - print("\n📦 Stock Configuration:") - for i, cfg in enumerate(configs, 1): - item_id = cfg["itemID"] - item_type = cfg["itemType"].upper() - # map ตาม itemType ว่าใช้ product_map หรือ material_map - if item_type == "PRODUCT": - name = product_map.get(item_id, "❓ไม่ทราบชื่อ") - elif item_type == "MATERIAL": - name = material_map.get(item_id, "❓ไม่ทราบชื่อ") - else: - name = "ไม่ทราบ" - print(f"\n🔧 Config #{i}") - print(f" - Item: {name} (ItemID: {item_id}, {item_type})") - print(f" - เป้าหมายสต๊อก: {cfg['targetStockLevel']}") - print(f" - สถานะ: {cfg['status']}") - print(f" - แก้ไขล่าสุด: {cfg['lastUpdated']}") - -if __name__ == "__main__": - # โหลดข้อมูลสินค้าและวัสดุก่อน - preload_items() - # แสดงผล mapping เพื่อตรวจสอบ - log_item_mappings() - # แสดงข้อมูลเครื่องจักรและสูตรการผลิต - log_machines() - # แสดงข้อมูล Stock Config - log_stock_configs() -- GitLab