Compare commits
8 Commits
45164faa9d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c072ae68ba | |||
| 7fba8295c9 | |||
| d36ec79f02 | |||
| 46f818c027 | |||
| 6eea8a9d21 | |||
| 2ee0063eb5 | |||
| 885800a10d | |||
| 597f7ddc55 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -137,3 +137,5 @@ dist
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
data
|
data
|
||||||
|
|
||||||
|
public/uploads
|
||||||
177
.vscode/launch.json
vendored
177
.vscode/launch.json
vendored
@ -2,45 +2,166 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Next.js: debug server-side",
|
"name": "Next.js: Start Development Server",
|
||||||
"type": "node-terminal",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run dev",
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal"
|
"env": {
|
||||||
},
|
"NODE_ENV": "development"
|
||||||
{
|
|
||||||
"name": "Next.js: debug client-side",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}",
|
|
||||||
"sourceMapPathOverrides": {
|
|
||||||
"webpack://_N_E/*": "${webRoot}/*"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Next.js: debug full stack",
|
"name": "Debug Send Emails (No Previous Emails)",
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
|
||||||
"command": "npm run dev",
|
|
||||||
"serverReadyAction": {
|
|
||||||
"pattern": "started server on .+, url: (https?://.+)",
|
|
||||||
"uriFormat": "%s",
|
|
||||||
"action": "debugWithChrome"
|
|
||||||
},
|
|
||||||
"console": "integratedTerminal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Next.js: attach to server",
|
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "attach",
|
"request": "launch",
|
||||||
"port": 9229,
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--require",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/src/scripts/send-emails-to-customers.ts",
|
||||||
|
"--count=0"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.scripts.json",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Send Emails (1 Previous Email)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--require",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/src/scripts/send-emails-to-customers.ts",
|
||||||
|
"--count=1"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.scripts.json",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Send Emails (With LMStudio Config)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--require",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/src/scripts/send-emails-to-customers.ts",
|
||||||
|
"--count=0",
|
||||||
|
"--lmstudio-url=http://localhost:1234/v1/chat/completions",
|
||||||
|
"--model=local-model",
|
||||||
|
"--temperature=0.7"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.scripts.json",
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"GMAIL_USER": "your.email@gmail.com",
|
||||||
|
"GMAIL_APP_PASSWORD": "your-app-password"
|
||||||
|
},
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Send Emails (Dry Run)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--require",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/src/scripts/send-emails-to-customers.ts",
|
||||||
|
"--count=0",
|
||||||
|
"--dry-run=true"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.scripts.json",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Import Customers from CSV",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--require",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/src/scripts/import-customers.ts",
|
||||||
|
"${input:csvFilePath}"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.scripts.json",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "csvFilePath",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Path to the CSV file to import",
|
||||||
|
"default": "./data/customers.csv"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
6
doc/prompts/14. Gmail sender
Normal file
6
doc/prompts/14. Gmail sender
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Please do following
|
||||||
|
- checkout to new brach features/gmail_sender
|
||||||
|
- create email.ts
|
||||||
|
- create function to send email usimg gmail smtp
|
||||||
|
- creadentials are in .env
|
||||||
|
- the function receives CustomerId and save to the Record
|
||||||
136
package-lock.json
generated
136
package-lock.json
generated
@ -18,11 +18,15 @@
|
|||||||
"@editorjs/marker": "^1.4.0",
|
"@editorjs/marker": "^1.4.0",
|
||||||
"@editorjs/paragraph": "^2.11.7",
|
"@editorjs/paragraph": "^2.11.7",
|
||||||
"@editorjs/quote": "^2.7.6",
|
"@editorjs/quote": "^2.7.6",
|
||||||
|
"@types/axios": "^0.14.4",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"axios": "^1.8.4",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.13.0",
|
"mysql2": "^3.13.0",
|
||||||
"next": "15.2.2",
|
"next": "15.2.2",
|
||||||
|
"nodemailer": "^6.10.0",
|
||||||
"pg": "^8.14.0",
|
"pg": "^8.14.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@ -1497,6 +1501,15 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/axios": {
|
||||||
|
"version": "0.14.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz",
|
||||||
|
"integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==",
|
||||||
|
"deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/bcrypt": {
|
"node_modules/@types/bcrypt": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
||||||
@ -1550,12 +1563,19 @@
|
|||||||
"version": "20.17.24",
|
"version": "20.17.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
|
||||||
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
|
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
||||||
|
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.10",
|
"version": "19.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
||||||
@ -2326,6 +2346,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@ -2361,6 +2386,16 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||||
|
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@ -2561,7 +2596,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -2726,6 +2760,17 @@
|
|||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -2921,6 +2966,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delegates": {
|
"node_modules/delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
@ -2983,7 +3036,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@ -3135,7 +3187,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -3145,7 +3196,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -3183,7 +3233,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@ -3196,7 +3245,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -3827,6 +3875,25 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@ -3871,6 +3938,20 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@ -3899,7 +3980,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -3979,7 +4059,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@ -4004,7 +4083,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@ -4119,7 +4197,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -4198,7 +4275,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -4211,7 +4287,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@ -4233,7 +4308,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@ -5484,7 +5558,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -5514,6 +5587,25 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
@ -5937,6 +6029,14 @@
|
|||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz",
|
||||||
|
"integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
@ -6520,6 +6620,11 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
@ -8133,7 +8238,6 @@
|
|||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unique-filename": {
|
"node_modules/unique-filename": {
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"create-test-user": "npx ts-node -P tsconfig.scripts.json src/scripts/create-test-user.ts",
|
"create-test-user": "npx ts-node -P tsconfig.scripts.json src/scripts/create-test-user.ts",
|
||||||
"reset-database": "npx ts-node -P tsconfig.scripts.json src/scripts/reset-database.ts",
|
"reset-database": "npx ts-node -P tsconfig.scripts.json src/scripts/reset-database.ts",
|
||||||
"import-customers": "npx ts-node -P tsconfig.scripts.json src/scripts/import-customers.ts"
|
"import-customers": "npx ts-node -P tsconfig.scripts.json src/scripts/import-customers.ts",
|
||||||
|
"send-emails": "npx ts-node -P tsconfig.scripts.json src/scripts/send-emails-to-customers.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@editorjs/code": "^2.9.3",
|
"@editorjs/code": "^2.9.3",
|
||||||
@ -22,11 +23,15 @@
|
|||||||
"@editorjs/marker": "^1.4.0",
|
"@editorjs/marker": "^1.4.0",
|
||||||
"@editorjs/paragraph": "^2.11.7",
|
"@editorjs/paragraph": "^2.11.7",
|
||||||
"@editorjs/quote": "^2.7.6",
|
"@editorjs/quote": "^2.7.6",
|
||||||
|
"@types/axios": "^0.14.4",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"axios": "^1.8.4",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.13.0",
|
"mysql2": "^3.13.0",
|
||||||
"next": "15.2.2",
|
"next": "15.2.2",
|
||||||
|
"nodemailer": "^6.10.0",
|
||||||
"pg": "^8.14.0",
|
"pg": "^8.14.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ interface Customer {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
city?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
modifiedAt: string;
|
modifiedAt: string;
|
||||||
}
|
}
|
||||||
@ -23,6 +24,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
city: '',
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(!!id); // Only loading if editing
|
const [isLoading, setIsLoading] = useState(!!id); // Only loading if editing
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -49,6 +51,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
name: customer.name,
|
name: customer.name,
|
||||||
url: customer.url || '',
|
url: customer.url || '',
|
||||||
email: customer.email,
|
email: customer.email,
|
||||||
|
city: customer.city || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -169,7 +172,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-4">
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
@ -185,6 +188,21 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="city">
|
||||||
|
City
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
|
id="city"
|
||||||
|
type="text"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="City (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
|
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
|
||||||
|
|||||||
@ -91,6 +91,12 @@ export default async function CustomerDetail({ params }: { params: { id: string
|
|||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">City</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{customer.city || <span className="text-gray-400">-</span>}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
<dt className="text-sm font-medium text-gray-500">Created</dt>
|
<dt className="text-sm font-medium text-gray-500">Created</dt>
|
||||||
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ interface Customer {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
city?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
modifiedAt: string;
|
modifiedAt: string;
|
||||||
}
|
}
|
||||||
@ -44,6 +45,7 @@ export default function AdminCustomers() {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
city: '',
|
||||||
hasEmail: false
|
hasEmail: false
|
||||||
});
|
});
|
||||||
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
||||||
@ -74,6 +76,7 @@ export default function AdminCustomers() {
|
|||||||
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
||||||
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
||||||
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
||||||
|
if (debouncedFilters.city) params.append('city', debouncedFilters.city);
|
||||||
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
||||||
|
|
||||||
const response = await fetch(`/api/customers?${params.toString()}`);
|
const response = await fetch(`/api/customers?${params.toString()}`);
|
||||||
@ -116,6 +119,7 @@ export default function AdminCustomers() {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
city: '',
|
||||||
hasEmail: false
|
hasEmail: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -238,6 +242,31 @@ export default function AdminCustomers() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="city-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search by City
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="city-filter"
|
||||||
|
value={filters.city}
|
||||||
|
onChange={(e) => handleFilterChange('city', e.target.value)}
|
||||||
|
className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||||
|
placeholder="Enter city..."
|
||||||
|
/>
|
||||||
|
{filters.city && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('city', '')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
Email Filter
|
Email Filter
|
||||||
@ -256,12 +285,13 @@ export default function AdminCustomers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(filters.name || filters.email || filters.url || filters.hasEmail) && (
|
{(filters.name || filters.email || filters.url || filters.city || filters.hasEmail) && (
|
||||||
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Active filters: {[
|
Active filters: {[
|
||||||
filters.name && 'Name',
|
filters.name && 'Name',
|
||||||
filters.email && 'Email',
|
filters.email && 'Email',
|
||||||
filters.url && 'URL',
|
filters.url && 'URL',
|
||||||
|
filters.city && 'City',
|
||||||
filters.hasEmail && 'Has Email'
|
filters.hasEmail && 'Has Email'
|
||||||
].filter(Boolean).join(', ')}
|
].filter(Boolean).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +351,12 @@ export default function AdminCustomers() {
|
|||||||
>
|
>
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
City
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
||||||
@ -380,6 +416,9 @@ export default function AdminCustomers() {
|
|||||||
{customer.email}
|
{customer.email}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{customer.city || <span className="text-gray-400 dark:text-gray-500">-</span>}
|
||||||
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{new Date(customer.createdAt).toLocaleDateString()}
|
{new Date(customer.createdAt).toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
@ -399,7 +438,7 @@ export default function AdminCustomers() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="py-4 pl-4 pr-3 text-sm text-gray-500 dark:text-gray-400 text-center">
|
<td colSpan={8} className="py-4 pl-4 pr-3 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||||
No customers found. Create your first customer!
|
No customers found. Create your first customer!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const { name, url, email } = data;
|
const { name, url, email, city } = data;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name || !email) {
|
if (!name || !email) {
|
||||||
@ -69,6 +69,7 @@ export async function PUT(
|
|||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = url || '';
|
customer.url = url || '';
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = city || null;
|
||||||
|
|
||||||
// Save the updated customer
|
// Save the updated customer
|
||||||
const updatedCustomer = await customerRepository.save(customer);
|
const updatedCustomer = await customerRepository.save(customer);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const nameFilter = url.searchParams.get('name');
|
const nameFilter = url.searchParams.get('name');
|
||||||
const emailFilter = url.searchParams.get('email');
|
const emailFilter = url.searchParams.get('email');
|
||||||
const urlFilter = url.searchParams.get('url');
|
const urlFilter = url.searchParams.get('url');
|
||||||
|
const cityFilter = url.searchParams.get('city');
|
||||||
const hasEmailFilter = url.searchParams.get('hasEmail');
|
const hasEmailFilter = url.searchParams.get('hasEmail');
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
@ -34,6 +35,9 @@ export async function GET(request: NextRequest) {
|
|||||||
if (urlFilter) {
|
if (urlFilter) {
|
||||||
queryBuilder = queryBuilder.andWhere('LOWER(customer.url) LIKE LOWER(:url)', { url: `%${urlFilter}%` });
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.url) LIKE LOWER(:url)', { url: `%${urlFilter}%` });
|
||||||
}
|
}
|
||||||
|
if (cityFilter) {
|
||||||
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.city) LIKE LOWER(:city)', { city: `%${cityFilter}%` });
|
||||||
|
}
|
||||||
if (hasEmailFilter === 'true') {
|
if (hasEmailFilter === 'true') {
|
||||||
queryBuilder = queryBuilder.andWhere('customer.email IS NOT NULL AND customer.email != :emptyString', { emptyString: '' });
|
queryBuilder = queryBuilder.andWhere('customer.email IS NOT NULL AND customer.email != :emptyString', { emptyString: '' });
|
||||||
}
|
}
|
||||||
@ -80,7 +84,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const customerRepository = dataSource.getRepository(Customer);
|
const customerRepository = dataSource.getRepository(Customer);
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const { name, url, email } = data;
|
const { name, url, email, city } = data;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name || !email) {
|
if (!name || !email) {
|
||||||
@ -95,6 +99,7 @@ export async function POST(request: NextRequest) {
|
|||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = url || '';
|
customer.url = url || '';
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = city || null;
|
||||||
|
|
||||||
const savedCustomer = await customerRepository.save(customer);
|
const savedCustomer = await customerRepository.save(customer);
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export class Customer {
|
|||||||
@Column()
|
@Column()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
city: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
91
src/lib/email.ts
Normal file
91
src/lib/email.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { ContactRecord } from './database/entities/ContactRecord';
|
||||||
|
import { Customer } from './database/entities/Customer';
|
||||||
|
import { getDataSource } from './database';
|
||||||
|
|
||||||
|
interface SendEmailResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmail(
|
||||||
|
customerId: string,
|
||||||
|
subject: string,
|
||||||
|
body: string,
|
||||||
|
htmlBody?: string
|
||||||
|
): Promise<SendEmailResult> {
|
||||||
|
try {
|
||||||
|
// Get data source
|
||||||
|
const dataSource = await getDataSource();
|
||||||
|
|
||||||
|
// Get customer details
|
||||||
|
const customer = await dataSource.getRepository(Customer).findOne({
|
||||||
|
where: { id: customerId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customer) {
|
||||||
|
throw new Error(`Customer with ID ${customerId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: 'smtp.gmail.com',
|
||||||
|
port: 587, // TLS port
|
||||||
|
secure: false, // true for 465 (SSL), false for other ports like 587 (TLS)
|
||||||
|
auth: {
|
||||||
|
user: process.env.GMAIL_USER || 'ken@catsai-agency.com',
|
||||||
|
pass: process.env.GMAIL_APP_PASSWORD || 'xkptiigonuhxkgma', // "xkpt iigo nuhx kgma"
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
// Do not fail on invalid certificates
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: process.env.GMAIL_USER || 'ken@catsai-agency.com',
|
||||||
|
to: customer.email,
|
||||||
|
subject: subject,
|
||||||
|
text: body
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create contact record
|
||||||
|
const contactRecord = new ContactRecord();
|
||||||
|
contactRecord.customer = customer;
|
||||||
|
contactRecord.customerId = customer.id;
|
||||||
|
contactRecord.contactType = 'EMAIL';
|
||||||
|
contactRecord.notes = `Email sent successfully. Subject: ${subject}. Body: ${body}`;
|
||||||
|
|
||||||
|
// Save contact record
|
||||||
|
await dataSource.getRepository(ContactRecord).save(contactRecord);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
// Create contact record for failed attempt
|
||||||
|
if (error instanceof Error) {
|
||||||
|
try {
|
||||||
|
const dataSource = await getDataSource();
|
||||||
|
const customer = await dataSource.getRepository(Customer).findOne({
|
||||||
|
where: { id: customerId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customer) {
|
||||||
|
const contactRecord = new ContactRecord();
|
||||||
|
contactRecord.customer = customer;
|
||||||
|
contactRecord.customerId = customer.id;
|
||||||
|
contactRecord.contactType = 'EMAIL';
|
||||||
|
contactRecord.notes = `Failed to send email: ${error.message}`;
|
||||||
|
|
||||||
|
await dataSource.getRepository(ContactRecord).save(contactRecord);
|
||||||
|
}
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Failed to save error record:', dbError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ import { Customer } from '../lib/database/entities/Customer';
|
|||||||
interface CustomerCSVRow {
|
interface CustomerCSVRow {
|
||||||
City: string;
|
City: string;
|
||||||
Name: string;
|
Name: string;
|
||||||
'Website URL': string;
|
URL: string;
|
||||||
Email: string;
|
Email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,59 +28,78 @@ async function importCustomers(csvFilePath: string): Promise<void> {
|
|||||||
|
|
||||||
console.log(`Found ${records.length} records in CSV file`);
|
console.log(`Found ${records.length} records in CSV file`);
|
||||||
|
|
||||||
// Track processed emails to skip duplicates
|
// Get existing customers for update
|
||||||
const processedEmails = new Set<string>();
|
const existingCustomers = await customerRepository.find();
|
||||||
// Track existing names to ensure uniqueness
|
const customersByEmail = new Map<string, Customer>();
|
||||||
const existingNames = new Set<string>(
|
const customersByName = new Map<string, Customer>();
|
||||||
(await customerRepository.find()).map(customer => customer.name)
|
|
||||||
);
|
// Create lookup maps for faster access
|
||||||
|
existingCustomers.forEach(customer => {
|
||||||
|
if (customer.email) {
|
||||||
|
customersByEmail.set(customer.email.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
customersByName.set(customer.name.toLowerCase(), customer);
|
||||||
|
});
|
||||||
|
|
||||||
let importedCount = 0;
|
let importedCount = 0;
|
||||||
let skippedDuplicateEmail = 0;
|
let updatedCount = 0;
|
||||||
let skippedDuplicateName = 0;
|
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const email = record.Email === 'null' ? '' : record.Email;
|
const email = record.Email === 'null' ? '' : record.Email;
|
||||||
const name = record.Name;
|
const name = record.Name;
|
||||||
|
|
||||||
// Skip if email is already processed (not empty and already seen)
|
let customer: Customer;
|
||||||
if (email && processedEmails.has(email)) {
|
let isUpdate = false;
|
||||||
console.log(`Skipping record with duplicate email: ${email}`);
|
|
||||||
skippedDuplicateEmail++;
|
// Check if customer exists by email or name
|
||||||
continue;
|
if (email && customersByEmail.has(email.toLowerCase())) {
|
||||||
|
// Update existing customer by email
|
||||||
|
customer = customersByEmail.get(email.toLowerCase())!;
|
||||||
|
isUpdate = true;
|
||||||
|
console.log(`Updating customer with email: ${email}`);
|
||||||
|
} else if (customersByName.has(name.toLowerCase())) {
|
||||||
|
// Update existing customer by name
|
||||||
|
customer = customersByName.get(name.toLowerCase())!;
|
||||||
|
isUpdate = true;
|
||||||
|
console.log(`Updating customer with name: ${name}`);
|
||||||
|
} else {
|
||||||
|
// Create new customer
|
||||||
|
customer = new Customer();
|
||||||
|
console.log(`Creating new customer: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if name already exists in database
|
// Update customer fields
|
||||||
if (existingNames.has(name)) {
|
|
||||||
console.log(`Skipping record with duplicate name: ${name}`);
|
|
||||||
skippedDuplicateName++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to processed sets
|
|
||||||
if (email) {
|
|
||||||
processedEmails.add(email);
|
|
||||||
}
|
|
||||||
existingNames.add(name);
|
|
||||||
|
|
||||||
// Create new customer
|
|
||||||
const customer = new Customer();
|
|
||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = record['Website URL'] === 'null' ? '' : record['Website URL'];
|
customer.url = record.URL === 'null' ? '' : record.URL;
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = record.City === 'null' ? '' : record.City;
|
||||||
|
|
||||||
// Save to database
|
try {
|
||||||
await customerRepository.save(customer);
|
// Save to database
|
||||||
importedCount++;
|
await customerRepository.save(customer);
|
||||||
|
|
||||||
console.log(`Imported customer: ${name}`);
|
if (isUpdate) {
|
||||||
|
updatedCount++;
|
||||||
|
console.log(`Updated customer: ${name}`);
|
||||||
|
} else {
|
||||||
|
importedCount++;
|
||||||
|
console.log(`Imported customer: ${name}`);
|
||||||
|
|
||||||
|
// Add to lookup maps for future reference
|
||||||
|
if (email) {
|
||||||
|
customersByEmail.set(email.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
customersByName.set(name.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error saving customer: ${name}`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Import summary:');
|
console.log('Import summary:');
|
||||||
console.log(`- Total records in CSV: ${records.length}`);
|
console.log(`- Total records in CSV: ${records.length}`);
|
||||||
console.log(`- Successfully imported: ${importedCount}`);
|
console.log(`- Successfully imported (new): ${importedCount}`);
|
||||||
console.log(`- Skipped (duplicate email): ${skippedDuplicateEmail}`);
|
console.log(`- Successfully updated: ${updatedCount}`);
|
||||||
console.log(`- Skipped (duplicate name): ${skippedDuplicateName}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing customers:', error);
|
console.error('Error importing customers:', error);
|
||||||
|
|||||||
270
src/scripts/send-emails-to-customers.ts
Normal file
270
src/scripts/send-emails-to-customers.ts
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { getDataSource } from '../lib/database';
|
||||||
|
import { Customer } from '../lib/database/entities/Customer';
|
||||||
|
import { ContactRecord } from '../lib/database/entities/ContactRecord';
|
||||||
|
import { sendEmail } from '../lib/email';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Get command line arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const emailCountArg = args.find(arg => arg.startsWith('--count='));
|
||||||
|
const lmStudioUrlArg = args.find(arg => arg.startsWith('--lmstudio-url='));
|
||||||
|
const modelArg = args.find(arg => arg.startsWith('--model='));
|
||||||
|
const temperatureArg = args.find(arg => arg.startsWith('--temperature='));
|
||||||
|
const dryRunArg = args.find(arg => arg.startsWith('--dry-run='));
|
||||||
|
|
||||||
|
// Parse arguments with defaults
|
||||||
|
const defaultEmailCount = 0;
|
||||||
|
const emailCount = emailCountArg
|
||||||
|
? parseInt(emailCountArg.split('=')[1], 10)
|
||||||
|
: defaultEmailCount;
|
||||||
|
|
||||||
|
// LMStudio API settings
|
||||||
|
const lmStudioUrl = lmStudioUrlArg
|
||||||
|
? lmStudioUrlArg.split('=')[1]
|
||||||
|
: 'http://localhost:1234/v1/chat/completions'; // Default LMStudio API endpoint
|
||||||
|
const model = modelArg
|
||||||
|
? modelArg.split('=')[1]
|
||||||
|
: 'local-model'; // Default model name
|
||||||
|
const temperature = temperatureArg
|
||||||
|
? parseFloat(temperatureArg.split('=')[0.3])
|
||||||
|
: 0.7; // Default temperature
|
||||||
|
|
||||||
|
// Dry run mode - generate content but don't send emails or create records
|
||||||
|
const dryRun = dryRunArg
|
||||||
|
? dryRunArg.split('=')[1].toLowerCase() === 'true'
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (isNaN(emailCount) || emailCount < 0) {
|
||||||
|
console.error('Error: Email count must be a non-negative number');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate email content using LMStudio API
|
||||||
|
async function generateEmailContent(
|
||||||
|
customer: Customer,
|
||||||
|
emailCount: number
|
||||||
|
): Promise<{ subject: string; body: string }> {
|
||||||
|
try {
|
||||||
|
console.log(`Generating email content for ${customer.name} using LMStudio API...`);
|
||||||
|
|
||||||
|
// Create a prompt based on customer info and email count
|
||||||
|
let prompt = '';
|
||||||
|
if (emailCount === 0) {
|
||||||
|
prompt = `Please generate cold email to promote custom generative AI video service.
|
||||||
|
|
||||||
|
Customer name : ${customer.name}
|
||||||
|
Customer city: ${customer.city}
|
||||||
|
|
||||||
|
Give me in following format { subject: "Short subject in local language" body: "Short email body in local language in plain text format" }
|
||||||
|
|
||||||
|
Please start with like "Hi I'm Ken from Cat's AI Ltd"
|
||||||
|
Please make short proposal how generative AI video can help to the museum
|
||||||
|
Please say something specific for the museum or the city
|
||||||
|
Subject should be something like "AI video for museums". 10 words max.
|
||||||
|
Please add in the end this email is generated by AI so please forgive me if something is wrong.
|
||||||
|
|
||||||
|
Please be short and clear
|
||||||
|
Please translate to local language.
|
||||||
|
Please include these sample videos to the email
|
||||||
|
https://www.youtube.com/watch?v=bfYzRBJEI7A
|
||||||
|
https://www.youtube.com/watch?v=XLI1oyy3TF0
|
||||||
|
https://www.youtube.com/watch?v=fX4BnZaDCxg`;
|
||||||
|
}/* else if (emailCount === 1) {
|
||||||
|
prompt = `Generate a follow-up email for customer ${customer.name} from ${customer.url}.
|
||||||
|
This is the second contact with them. The email should reference a previous welcome email and provide more value.
|
||||||
|
Return ONLY a JSON object with 'subject' and 'body' fields. The body should be in HTML format.`;
|
||||||
|
} else {
|
||||||
|
prompt = `Generate a relationship-building email for a regular customer named ${customer.name} from ${customer.url}.
|
||||||
|
This is contact number ${emailCount + 1} with them. The email should be personalized and provide specific value.
|
||||||
|
Return ONLY a JSON object with 'subject' and 'body' fields. The body should be in HTML format.`;
|
||||||
|
} */
|
||||||
|
|
||||||
|
// Call LMStudio API
|
||||||
|
const response = await axios.post(
|
||||||
|
lmStudioUrl,
|
||||||
|
{
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are an expert email copywriter who creates engaging, professional emails. Output should be only JSON' },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
],
|
||||||
|
temperature: temperature,
|
||||||
|
max_tokens: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse the response to extract the JSON
|
||||||
|
const assistantMessage = response.data.choices[0].message.content;
|
||||||
|
|
||||||
|
// Try to extract JSON from the response
|
||||||
|
try {
|
||||||
|
// Look for JSON object in the response
|
||||||
|
const jsonMatch = assistantMessage.match(/\{[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
const jsonStr = jsonMatch[0];
|
||||||
|
const emailContent = JSON.parse(jsonStr);
|
||||||
|
|
||||||
|
if (emailContent.subject && emailContent.body) {
|
||||||
|
return {
|
||||||
|
subject: emailContent.subject,
|
||||||
|
body: emailContent.body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we couldn't parse JSON or it doesn't have required fields
|
||||||
|
throw new Error('Could not parse valid JSON from LMStudio response');
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Failed to parse JSON from LMStudio response, using fallback content');
|
||||||
|
console.warn('LMStudio response:', assistantMessage);
|
||||||
|
|
||||||
|
// Fallback content
|
||||||
|
return {
|
||||||
|
subject: `Enhance Engagement with AI-Powered Video`,
|
||||||
|
body: `Hi\n\nI'm Ken from Cat's AI Ltd.\n\nWe help museums like yours create engaging video content using cutting-edge generative AI. Imagine bringing historical artifacts to life, creating immersive virtual tours, or developing captivating educational videos – all without extensive production costs.\n\nGiven the rich cultural landscape of the USA and Default Museum’s important role within it, we believe AI video could significantly boost visitor engagement and online reach.\n\nHere are a few examples of what's possible:\nhttps://www.youtube.com/watch?v=bfYzRBJEI7A\nhttps://www.youtube.com/watch?v=XLI1oyy3TF0\nhttps://www.youtube.com/watch?v=fX4BnZaDCxg\n\nWould you be open to a quick chat about how AI video can benefit Default Museum?\n\nBest regards,\nKen (Cat's AI Ltd)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred';
|
||||||
|
|
||||||
|
console.error('Error calling LMStudio API:', errorMessage);
|
||||||
|
|
||||||
|
// Fallback content in case of API error
|
||||||
|
return {
|
||||||
|
subject: `Enhance Engagement with AI-Powered Video`,
|
||||||
|
body: `Hi\n\nI'm Ken from Cat's AI Ltd.\n\nWe help museums like yours create engaging video content using cutting-edge generative AI. Imagine bringing historical artifacts to life, creating immersive virtual tours, or developing captivating educational videos – all without extensive production costs.\n\nGiven the rich cultural landscape of the USA and Default Museum’s important role within it, we believe AI video could significantly boost visitor engagement and online reach.\n\nHere are a few examples of what's possible:\nhttps://www.youtube.com/watch?v=bfYzRBJEI7A\nhttps://www.youtube.com/watch?v=XLI1oyy3TF0\nhttps://www.youtube.com/watch?v=fX4BnZaDCxg\n\nWould you be open to a quick chat about how AI video can benefit Default Museum?\n\nBest regards,\nKen (Cat's AI Ltd)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('Initializing database connection...');
|
||||||
|
const dataSource = await getDataSource();
|
||||||
|
|
||||||
|
console.log(`Finding customers with exactly ${emailCount} email contact records...`);
|
||||||
|
|
||||||
|
let customersQuery = dataSource
|
||||||
|
.getRepository(Customer)
|
||||||
|
.createQueryBuilder('customer');
|
||||||
|
|
||||||
|
if (emailCount === 0) {
|
||||||
|
// Find customers with no email contact records
|
||||||
|
customersQuery = customersQuery
|
||||||
|
.leftJoin(
|
||||||
|
ContactRecord,
|
||||||
|
'contact',
|
||||||
|
'contact.customerId = customer.id AND contact.contactType = :contactType',
|
||||||
|
{ contactType: 'EMAIL' }
|
||||||
|
)
|
||||||
|
.where('contact.id IS NULL')
|
||||||
|
.andWhere('customer.email IS NOT NULL')
|
||||||
|
.andWhere('customer.email != :emptyEmail', { emptyEmail: '' })
|
||||||
|
.limit(100);
|
||||||
|
} else {
|
||||||
|
// Find customers with exactly N email contact records
|
||||||
|
customersQuery = customersQuery
|
||||||
|
.leftJoin(
|
||||||
|
ContactRecord,
|
||||||
|
'contact',
|
||||||
|
'contact.customerId = customer.id AND contact.contactType = :contactType',
|
||||||
|
{ contactType: 'EMAIL' }
|
||||||
|
)
|
||||||
|
.where('customer.email IS NOT NULL')
|
||||||
|
.andWhere('customer.email != :emptyEmail', { emptyEmail: '' })
|
||||||
|
.groupBy('customer.id')
|
||||||
|
.having('COUNT(contact.id) = :count', { count: emailCount })
|
||||||
|
.limit(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredCustomers = await customersQuery.getMany();
|
||||||
|
|
||||||
|
console.log(`Found ${filteredCustomers.length} customers with exactly ${emailCount} email contact records.`);
|
||||||
|
|
||||||
|
// Exit if no customers found
|
||||||
|
if (filteredCustomers.length === 0) {
|
||||||
|
console.log(`No customers with exactly ${emailCount} email contact records found. Exiting.`);
|
||||||
|
await dataSource.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for confirmation before sending emails
|
||||||
|
console.log('The following customers will receive emails:');
|
||||||
|
filteredCustomers.forEach((customer, index) => {
|
||||||
|
console.log(`${index + 1}. ${customer.name} (${customer.email})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\nSending emails to customers...');
|
||||||
|
|
||||||
|
// Send emails to each customer
|
||||||
|
for (const customer of filteredCustomers) {
|
||||||
|
console.log(`Processing email for ${customer.name} (${customer.email})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate email content using LMStudio
|
||||||
|
const emailContent = await generateEmailContent(customer, emailCount);
|
||||||
|
|
||||||
|
console.log(`Generated subject: ${emailContent.subject}`);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(`[DRY RUN] Would send email to ${customer.name} (${customer.email})`);
|
||||||
|
console.log(`[DRY RUN] Subject: ${emailContent.subject}`);
|
||||||
|
console.log(`[DRY RUN] Body: ${emailContent.body.replace(/<[^>]*>/g, '')}`);
|
||||||
|
console.log(`[DRY RUN] No email sent and no contact record created.`);
|
||||||
|
} else {
|
||||||
|
console.log(`Sending email to ${customer.name}...`);
|
||||||
|
|
||||||
|
const result = await sendEmail(
|
||||||
|
customer.id,
|
||||||
|
emailContent.subject,
|
||||||
|
emailContent.body.replace(/<[^>]*>/g, ''), // Plain text version (strip HTML)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ Email sent successfully to ${customer.email}`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Failed to send email to ${customer.email}: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred';
|
||||||
|
|
||||||
|
console.error(`❌ Error sending email to ${customer.email}:`, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay between emails to avoid rate limiting
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Email sending process completed.');
|
||||||
|
await dataSource.destroy();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred';
|
||||||
|
|
||||||
|
console.error('An error occurred:', errorMessage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the main function
|
||||||
|
main().catch(error => {
|
||||||
|
const errorMessage = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred';
|
||||||
|
|
||||||
|
console.error('Unhandled error:', errorMessage);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user