Working log in/route guard
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
package-lock.json
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
@@ -1,32 +1,36 @@
|
||||
|
||||
|
||||
# --- STAGE 1: Build the Vue application ---
|
||||
FROM node:20.11.1 AS builder
|
||||
|
||||
|
||||
# Set build-time environment variables for your API URLs
|
||||
ENV VITE_BASE_URL="https://apioil.edwineames.com"
|
||||
ENV VITE_MONEY_URL="https://apimoney.edwineames.com"
|
||||
ENV VITE_AUTO_URL="https://apiauto.edwineames.com"
|
||||
|
||||
ENV VITE_COMPANY_ID="1"
|
||||
ENV NODE_ENV=production
|
||||
ENV VITE_MONEY_URL="https://apimoney.edwineames.com"
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json .
|
||||
RUN npm ci
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
# --- THE FIX IS HERE ---
|
||||
# 1. Install ALL dependencies (including devDependencies like vue-tsc)
|
||||
RUN npm install
|
||||
|
||||
# 2. Copy the rest of your source code
|
||||
COPY . .
|
||||
|
||||
# 3. Now run the build, which will succeed because vue-tsc is installed
|
||||
RUN npm run build
|
||||
RUN npm prune --production
|
||||
|
||||
# --- STAGE 2: Serve the built files with Nginx ---
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
|
||||
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
ENV NODE_ENV=production
|
||||
# Copy the build application from the previous stage to the Nginx container
|
||||
# Copy the static files from the 'builder' stage
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
# Copy the nginx configuration file
|
||||
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
# Expose the port 80
|
||||
|
||||
# Copy your custom Nginx configuration
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Tell the world that port 80 is now listening
|
||||
EXPOSE 80
|
||||
# Start Nginx to serve the application
|
||||
|
||||
# Start Nginx when the container launches
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,9 +1,16 @@
|
||||
# This config is for the Nginx server INSIDE the frontend container
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
listen 80;
|
||||
|
||||
# The location of the static files built by Vue
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# This is the magic for Single-Page Applications (SPAs)
|
||||
# It makes sure that if you refresh the page on a route like /customers/123,
|
||||
# Nginx serves index.html instead of looking for a file named '123'.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
126
package-lock.json
generated
126
package-lock.json
generated
@@ -29,18 +29,18 @@
|
||||
"vuelidate": "^0.7.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"daisyui": "^4.4.19",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"postcss": "^8.4.32",
|
||||
"sass": "^1.72.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vue-tsc": "^1.8.25"
|
||||
"postcss": "^8.4.38",
|
||||
"sass": "^1.75.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "2.0.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -525,6 +525,7 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
@@ -540,12 +541,14 @@
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
@@ -562,6 +565,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
@@ -1080,6 +1084,7 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -1819,43 +1824,43 @@
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
|
||||
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
|
||||
"version": "2.2.0-alpha.8",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz",
|
||||
"integrity": "sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@volar/source-map": "1.11.1"
|
||||
"@volar/source-map": "2.2.0-alpha.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
|
||||
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
|
||||
"version": "2.2.0-alpha.8",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz",
|
||||
"integrity": "sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"muggle-string": "^0.3.1"
|
||||
"muggle-string": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
|
||||
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
|
||||
"version": "2.2.0-alpha.8",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz",
|
||||
"integrity": "sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@volar/language-core": "1.11.1",
|
||||
"@volar/language-core": "2.2.0-alpha.8",
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
@@ -1929,18 +1934,16 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
|
||||
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.13.tgz",
|
||||
"integrity": "sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~1.11.1",
|
||||
"@volar/source-map": "~1.11.1",
|
||||
"@vue/compiler-dom": "^3.3.0",
|
||||
"@vue/shared": "^3.3.0",
|
||||
"@volar/language-core": "2.2.0-alpha.8",
|
||||
"@vue/compiler-dom": "^3.4.0",
|
||||
"@vue/shared": "^3.4.0",
|
||||
"computeds": "^0.0.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
},
|
||||
@@ -2164,6 +2167,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2270,7 +2274,8 @@
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
@@ -2314,6 +2319,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -3025,7 +3031,8 @@
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.211",
|
||||
@@ -3492,6 +3499,7 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
@@ -3507,6 +3515,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -3668,6 +3677,7 @@
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
@@ -4131,6 +4141,7 @@
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
@@ -4443,6 +4454,7 @@
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -4465,6 +4477,7 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -4483,9 +4496,9 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
|
||||
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mz": {
|
||||
@@ -7205,7 +7218,8 @@
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
@@ -7301,6 +7315,7 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
@@ -8369,6 +8384,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -8394,6 +8410,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -8405,6 +8422,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -8760,9 +8778,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -9074,13 +9092,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
|
||||
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.13.tgz",
|
||||
"integrity": "sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~1.11.1",
|
||||
"@vue/language-core": "1.8.27",
|
||||
"@volar/typescript": "2.2.0-alpha.8",
|
||||
"@vue/language-core": "2.0.13",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"bin": {
|
||||
@@ -9143,6 +9161,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
@@ -9160,6 +9179,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@@ -9176,6 +9196,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -9189,12 +9210,14 @@
|
||||
"node_modules/wrap-ansi/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
@@ -9211,6 +9234,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
|
||||
22
package.json
22
package.json
@@ -29,18 +29,18 @@
|
||||
"vue3-pdfmake": "^2.2.0",
|
||||
"vuelidate": "^0.7.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"daisyui": "^4.4.19",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"postcss": "^8.4.32",
|
||||
"sass": "^1.72.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vue-tsc": "^1.8.25"
|
||||
"postcss": "^8.4.38",
|
||||
"sass": "^1.75.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "2.0.13"
|
||||
}
|
||||
}
|
||||
|
||||
30
src/App.vue
30
src/App.vue
@@ -1,34 +1,27 @@
|
||||
<!-- App.vue -->
|
||||
<template>
|
||||
<!--
|
||||
This is the new main layout for your entire application.
|
||||
- `drawer`: The base class for the component.
|
||||
- `lg:drawer-open`: On large screens (lg), the drawer is permanently open, creating the desktop sidebar view.
|
||||
-->
|
||||
<div class="drawer lg:drawer-open">
|
||||
<!-- The hidden checkbox that the hamburger button in the header will toggle -->
|
||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<!-- DRAWER CONTENT: This is what's visible on the main page -->
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- The Header now lives here, at the top of the content -->
|
||||
<!--
|
||||
DRAWER CONTENT: Main page content area.
|
||||
FIX: Added `relative` so the absolutely positioned search results are contained within it.
|
||||
-->
|
||||
<div class="drawer-content flex flex-col relative">
|
||||
<HeaderAuth />
|
||||
|
||||
<!-- The Main Content Area for your 30 pages -->
|
||||
<main class="flex-1 p-4 md:p-8 bg-base-200">
|
||||
<!-- <router-view> renders the current page (e.g., deliveryEdit.vue) -->
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- Your global search results and notifications still live here -->
|
||||
<!-- The SearchResults component now lives here and will appear as an overlay -->
|
||||
<SearchResults v-if="searchStore.showResults" />
|
||||
|
||||
<notifications position="top center" />
|
||||
</div>
|
||||
|
||||
<!-- DRAWER SIDE: This is the sidebar that slides out on mobile -->
|
||||
<!-- DRAWER SIDE: This is your main navigation sidebar. It is no longer conditional. -->
|
||||
<div class="drawer-side">
|
||||
<label for="my-drawer-2" class="drawer-overlay"></label>
|
||||
<!-- The Sidebar component is now placed here -->
|
||||
<SideBar />
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,9 +29,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchStore } from './stores/search';
|
||||
import HeaderAuth from './layouts/headers/headerauth.vue'; // Adjust path if needed
|
||||
import SideBar from './layouts/sidebar/sidebar.vue'; // Adjust path if needed
|
||||
import SearchResults from './components/SearchResults.vue'; // Adjust path if needed
|
||||
import HeaderAuth from './layouts/headers/headerauth.vue';
|
||||
import SideBar from './layouts/sidebar/sidebar.vue';
|
||||
// Make sure this path and component name are correct
|
||||
import SearchResults from './components/SearchResults.vue';
|
||||
|
||||
const searchStore = useSearchStore();
|
||||
</script>
|
||||
@@ -1,53 +1,100 @@
|
||||
<!-- SearchResults.vue -->
|
||||
<!-- components/SearchResults.vue -->
|
||||
<template>
|
||||
|
||||
<div class="fixed top-16 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4">
|
||||
<div class="overflow-x-auto bg-base-100 rounded-lg shadow-2xl border border-gray-700">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Phone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- It now reads `searchResults` directly from the store -->
|
||||
<tr v-for="user in searchStore.searchResults" :key="user.id" class="hover cursor-pointer" @click="viewProfile(user.id)">
|
||||
<td>
|
||||
<div class="font-bold">{{ user.customer_first_name }} {{ user.customer_last_name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ user.customer_address }}</div>
|
||||
<div class="text-sm opacity-70">{{ user.customer_town }}, {{ user.customer_state }}</div>
|
||||
</td>
|
||||
<td>{{ user.customer_phone_number }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
ref="searchContainer"
|
||||
class="absolute top-16 left-1/2 -translate-x-1/2 z-50 w-96 max-w-[90vw] max-h-[70vh] bg-base-100 rounded-lg shadow-xl border border-base-300 flex flex-col"
|
||||
>
|
||||
<!-- Header of the search results box -->
|
||||
<div class="p-4 border-b border-base-300 flex-shrink-0">
|
||||
<h3 class="font-bold">Search Results</h3>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
THE FIX IS HERE:
|
||||
- Removed the `menu` class from the `<ul>`.
|
||||
- Added `flex flex-col` to explicitly force a single vertical column.
|
||||
- Added `space-y-1` to add a small gap between each list item.
|
||||
-->
|
||||
<ul class="flex flex-col space-y-1 p-2 flex-grow overflow-y-auto">
|
||||
<li v-if="isLoading" class="p-4 text-center">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</li>
|
||||
<li v-else-if="searchResults.length === 0 && searchTerm.length > 1" class="px-4 py-2 text-sm text-gray-500">
|
||||
No results found for "{{ searchTerm }}"
|
||||
</li>
|
||||
|
||||
<!-- The v-for now loops through simple <li> elements -->
|
||||
<li v-for="result in searchResults" :key="result.id">
|
||||
<!--
|
||||
We add styling directly to the router-link to make it look like a clickable list item.
|
||||
- `block`: Makes the entire area clickable, not just the text.
|
||||
-->
|
||||
<router-link
|
||||
:to="{ name: 'customerProfile', params: { id: result.id } }"
|
||||
@click="clearSearch"
|
||||
class="block p-2 rounded-lg hover:bg-base-200 focus:bg-primary focus:text-primary-content"
|
||||
>
|
||||
<div>
|
||||
<div class="font-bold">{{ result.customer_first_name }} {{ result.customer_last_name }}</div>
|
||||
<div class="text-sm opacity-70">
|
||||
{{ result.customer_address }}
|
||||
</div>
|
||||
<div class="text-sm opacity-70">
|
||||
{{ result.customer_town }}, {{ getStateName(result.customer_state) }}
|
||||
</div>
|
||||
<div class="text-xs opacity-60 mt-1">
|
||||
{{ result.customer_phone_number }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
// 1. Import the store
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapState, mapActions } from 'pinia';
|
||||
import { useSearchStore } from '../stores/search'; // Adjust path if needed
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
setup() {
|
||||
// 2. Make the store available
|
||||
const searchStore = useSearchStore();
|
||||
return { searchStore };
|
||||
name: 'SearchResults',
|
||||
|
||||
data() {
|
||||
return {
|
||||
stateMap: {
|
||||
0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY',
|
||||
} as Record<number, string>,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useSearchStore, ['searchTerm', 'searchResults', 'isLoading']),
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewProfile(customerId: number) {
|
||||
// When a user is clicked, navigate to their profile...
|
||||
this.$router.push({ name: 'customerProfile', params: { id: customerId } });
|
||||
// ...and clear the search so the overlay disappears.
|
||||
this.searchStore.clearSearch();
|
||||
}
|
||||
}
|
||||
...mapActions(useSearchStore, ['clearSearch']),
|
||||
|
||||
getStateName(stateValue: number | string): string {
|
||||
const stateNumber = Number(stateValue);
|
||||
return this.stateMap[stateNumber] || 'N/A';
|
||||
},
|
||||
|
||||
handleClickOutside(event: MouseEvent) {
|
||||
const searchContainer = this.$refs.searchContainer as HTMLElement;
|
||||
const searchInput = document.getElementById('customer-search-input');
|
||||
|
||||
if (searchContainer && !searchContainer.contains(event.target as Node) && searchInput && !searchInput.contains(event.target as Node)) {
|
||||
this.clearSearch();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
},
|
||||
});
|
||||
</script>c
|
||||
</script>
|
||||
@@ -11,18 +11,23 @@
|
||||
|
||||
<!-- Logo -->
|
||||
<router-link :to="{ name: 'home' }" class="btn btn-ghost normal-case text-xl">
|
||||
<img src="../../assets/images/1.png" alt="Company Logo" class="h-8 md:h-10 w-auto" />
|
||||
Auburn Oil
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Navbar Center Section (Desktop Search Bar) -->
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<!--
|
||||
THIS IS THE ONLY CHANGE NEEDED ON THIS ENTIRE PAGE.
|
||||
We are adding the @input event listener to trigger the search.
|
||||
-->
|
||||
<input
|
||||
id="customer-search-input"
|
||||
type="text"
|
||||
placeholder="Search Customers..."
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
placeholder="Search customers..."
|
||||
v-model="searchStore.searchTerm"
|
||||
@input="searchStore.fetchSearchResults"
|
||||
class="input input-bordered"
|
||||
@input="searchStore.debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -34,8 +39,7 @@
|
||||
</router-link>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<!-- v-if="employee.id" only renders this block AFTER the API call is successful and an employee ID exists. -->
|
||||
<div v-if="employee.id" class="dropdown dropdown-end">
|
||||
<div v-if="user.user_id" class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost btn-circle avatar">
|
||||
<div class="w-10 rounded-full bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-lg font-bold">{{ userInitials }}</span>
|
||||
@@ -45,7 +49,7 @@
|
||||
<li class="p-2 font-semibold">{{ user.user_name }}</li>
|
||||
<div class="divider my-0"></div>
|
||||
<li>
|
||||
<router-link :to="{ name: 'employeeProfile', params: { id: employee.id } }">
|
||||
<router-link :to="{ name: 'employeeProfile', params: { id: user.user_id } }">
|
||||
Profile
|
||||
</router-link>
|
||||
</li>
|
||||
@@ -54,31 +58,29 @@
|
||||
</div>
|
||||
<!-- This provides the loading indicator while we wait for the API call to finish. -->
|
||||
<div v-else class="px-4">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{{user}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
import authHeader from '../../services/auth.header'
|
||||
import { useSearchStore } from '../../stores/search' // Adjust path if needed
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
||||
// Define the shape of your data for internal type safety
|
||||
interface User {
|
||||
user_name: string;
|
||||
user_id: number;
|
||||
}
|
||||
interface Employee {
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
// Initialize with empty objects to prevent template errors
|
||||
employee: {} as Employee,
|
||||
user: {} as User,
|
||||
}
|
||||
},
|
||||
@@ -86,6 +88,7 @@ export default defineComponent({
|
||||
computed: {
|
||||
searchStore() {
|
||||
return useSearchStore();
|
||||
|
||||
},
|
||||
userInitials(): string {
|
||||
if (!this.user || !this.user.user_name) return '';
|
||||
@@ -97,32 +100,39 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetchUserData();
|
||||
this.userStatus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchUserData() {
|
||||
axios.get('/auth/whoami', { headers: authHeader() })
|
||||
.then((response: any) => {
|
||||
console.log("User Data Response from API:", response.data);
|
||||
|
||||
// This check is now more robust. It only checks for what it truly needs.
|
||||
if (response.data && response.data.ok && response.data.employee && response.data.employee.id) {
|
||||
this.user = response.data.user;
|
||||
this.employee = response.data.employee;
|
||||
} else {
|
||||
console.error("API response was successful, but the expected employee data with an ID is missing.");
|
||||
}
|
||||
userStatus() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error("CRITICAL: Failed to fetch user data. The API call itself failed.", error);
|
||||
});
|
||||
},
|
||||
.then((response: any) => {
|
||||
console.log(this.user)
|
||||
if (response.data.ok) {
|
||||
this.user = response.data.user;
|
||||
} else {
|
||||
|
||||
localStorage.removeItem('user');
|
||||
this.$router.push('/login');
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
logout() {
|
||||
console.log("Logging out...");
|
||||
// Your full logout logic here
|
||||
// Clear auth data
|
||||
const authStore = useAuthStore();
|
||||
authStore.clearAuth();
|
||||
// Redirect to login
|
||||
this.$router.push({ name: 'login' });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<!-- sidebar.vue -->
|
||||
<template>
|
||||
<!--
|
||||
The sidebar is now just the menu. The layout logic lives in App.vue.
|
||||
This is much cleaner and works correctly with the mobile hamburger button.
|
||||
-->
|
||||
<ul class="menu p-4 w-80 min-h-full bg-base-100 text-base-content">
|
||||
<!-- Logo at the top of the sidebar for mobile view -->
|
||||
<li class="mb-4 lg:hidden">
|
||||
<router-link :to="{ name: 'home' }">
|
||||
<img src="../assets/images/1.png" alt="Company Logo" class="h-10 w-auto" />
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<router-link :to="{ name: 'home' }" exact-active-class="active">
|
||||
Home
|
||||
<ul class="menu p-4 w-64 min-h-full bg-base-100 text-base-content">
|
||||
<li class="mb-4 lg-hidden">
|
||||
<router-link :to="{ name: 'home' }">
|
||||
<img src="../../assets/images/1.png" alt="Company Logo" class="h-10 w-auto" />
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li><router-link :to="{ name: 'home' }" exact-active-class="active">Home</router-link></li>
|
||||
|
||||
<!-- Customer Section - Open by default -->
|
||||
<!-- Customer Section -->
|
||||
<li>
|
||||
<details open>
|
||||
<summary class="font-bold text-lg">Customer</summary>
|
||||
@@ -28,7 +19,7 @@
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<!-- Delivery Section - Open by default -->
|
||||
<!-- Delivery Section -->
|
||||
<li>
|
||||
<details open>
|
||||
<summary class="font-bold text-lg">Delivery</summary>
|
||||
@@ -37,26 +28,26 @@
|
||||
<li>
|
||||
<router-link :to="{ name: 'deliveryOutForDelivery' }" exact-active-class="active">
|
||||
Todays Deliveries
|
||||
<span v-if="today_count > 0" class="badge badge-secondary">{{ today_count }}</span>
|
||||
<span v-if="countsStore.today > 0" class="badge badge-secondary">{{ countsStore.today }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'deliveryTommorrow' }" exact-active-class="active">
|
||||
Tomorrows Deliveries
|
||||
<span v-if="tommorrow_count > 0" class="badge badge-secondary">{{ tommorrow_count }}</span>
|
||||
<span v-if="countsStore.tomorrow > 0" class="badge badge-secondary">{{ countsStore.tomorrow }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'deliveryWaiting' }" exact-active-class="active">
|
||||
Waiting Deliveries
|
||||
<span v-if="waiting_count > 0" class="badge badge-info">{{ waiting_count }}</span>
|
||||
<span v-if="countsStore.waiting > 0" class="badge badge-info">{{ countsStore.waiting }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li><router-link :to="{ name: 'deliveryIssue' }" exact-active-class="active">Issue Tickets</router-link></li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'deliveryPending' }" exact-active-class="active">
|
||||
Pending Payment
|
||||
<span v-if="pending_count > 0" class="badge badge-warning">{{ pending_count }}</span>
|
||||
<span v-if="countsStore.pending > 0" class="badge badge-warning">{{ countsStore.pending }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li><router-link :to="{ name: 'deliveryFinalized' }" exact-active-class="active">Finalized Tickets</router-link></li>
|
||||
@@ -64,7 +55,7 @@
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<!-- Service Section - Open by default -->
|
||||
<!-- Service Section -->
|
||||
<li>
|
||||
<details open>
|
||||
<summary class="font-bold text-lg">Service</summary>
|
||||
@@ -73,7 +64,7 @@
|
||||
<li>
|
||||
<router-link :to="{ name: 'ServiceHome' }" exact-active-class="active">
|
||||
Upcoming Service
|
||||
<span v-if="upcoming_service_count > 0" class="badge badge-info">{{ upcoming_service_count }}</span>
|
||||
<span v-if="countsStore.upcoming_service > 0" class="badge badge-info">{{ countsStore.upcoming_service }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li><router-link :to="{ name: 'ServicePast' }" exact-active-class="active">Past Service</router-link></li>
|
||||
@@ -81,7 +72,7 @@
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<!-- Automatics Section - Now has its own header -->
|
||||
<!-- Automatics Section -->
|
||||
<li>
|
||||
<details>
|
||||
<summary class="font-bold text-lg">Automatics</summary>
|
||||
@@ -89,19 +80,18 @@
|
||||
<li>
|
||||
<router-link :to="{ name: 'auto' }" exact-active-class="active">
|
||||
All Automatics
|
||||
<span v-if="automatic_count > 0" class="badge badge-info">{{ automatic_count }}</span>
|
||||
<span v-if="countsStore.automatic > 0" class="badge badge-info">{{ countsStore.automatic }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<!-- Admin Section - Closed by default and contains Employees -->
|
||||
|
||||
<!-- Admin Section remains the same -->
|
||||
<li>
|
||||
<details>
|
||||
<summary class="font-bold text-lg">Admin</summary>
|
||||
<ul>
|
||||
<!-- Employees is now here -->
|
||||
<li><router-link :to="{ name: 'employee' }" exact-active-class="active">Employees</router-link></li>
|
||||
<li><router-link :to="{ name: 'oilprice' }" exact-active-class="active">Oil Pricing</router-link></li>
|
||||
<li><router-link :to="{ name: 'promo' }" exact-active-class="active">Promos</router-link></li>
|
||||
@@ -112,132 +102,16 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import axios from 'axios';
|
||||
import authHeader from '../../services/auth.header';
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useCountsStore } from '../../stores/counts'; // Adjust path if needed
|
||||
|
||||
export default defineComponent({
|
||||
name: "SideBar",
|
||||
mounted() {
|
||||
this.getUpcomingServiceCount();
|
||||
this.getTodayCount();
|
||||
this.getTommorrowCount();
|
||||
this.getWaitingCount();
|
||||
this.getPendingCount();
|
||||
this.getAutoCount();
|
||||
this.updatestatus();
|
||||
this.updateautos();
|
||||
this.updatetemp();
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
upcoming_service_count: 0,
|
||||
waiting_count: 0,
|
||||
today_count: 0,
|
||||
tommorrow_count: 0,
|
||||
pending_count: 0,
|
||||
automatic_count: 0,
|
||||
};
|
||||
},
|
||||
// Get a reference to our new store
|
||||
const countsStore = useCountsStore();
|
||||
|
||||
methods: {
|
||||
getUpcomingServiceCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/service/upcoming/count';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.upcoming_service_count = response.data.count;
|
||||
}).catch((error: any) => {
|
||||
console.error("Failed to get upcoming service count:", error);
|
||||
this.upcoming_service_count = 0;
|
||||
});
|
||||
},
|
||||
updatestatus() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
if (response.data.update)
|
||||
console.log("Updated Status of Deliveries")
|
||||
}).catch((error: any) => console.error("Update status failed:", error));
|
||||
},
|
||||
updatetemp() {
|
||||
let path = import.meta.env.VITE_AUTO_URL + '/main/temp';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
if (response.data.ok)
|
||||
console.log("Updated Temp")
|
||||
}).catch((error: any) => console.error("Update temp failed:", error));
|
||||
},
|
||||
updateautos() {
|
||||
let path = import.meta.env.VITE_AUTO_URL + '/main/update';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
if (response.data.ok)
|
||||
console.log("Updated Autos")
|
||||
}).catch((error: any) => console.error("Update autos failed:", error));
|
||||
},
|
||||
getAutoCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/automatic';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.automatic_count = response.data.count
|
||||
}).catch((error: any) => console.error("Get auto count failed:", error));
|
||||
},
|
||||
getTodayCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/today';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.today_count = response.data.count
|
||||
}).catch((error: any) => console.error("Get today count failed:", error));
|
||||
},
|
||||
getTommorrowCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/tommorrow';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.tommorrow_count = response.data.count
|
||||
}).catch((error: any) => console.error("Get tomorrow count failed:", error));
|
||||
},
|
||||
getPendingCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/pending';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.pending_count = response.data.count
|
||||
}).catch((error: any) => console.error("Get pending count failed:", error));
|
||||
},
|
||||
getWaitingCount() {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/waiting';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.waiting_count = response.data.count
|
||||
}).catch((error: any) => console.error("Get waiting count failed:", error));
|
||||
},
|
||||
},
|
||||
// When the sidebar is first mounted, fetch all the counts
|
||||
onMounted(() => {
|
||||
countsStore.fetchSidebarCounts();
|
||||
// You can remove your other update functions if their logic is now handled elsewhere
|
||||
});
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
<!-- Login.vue -->
|
||||
<template>
|
||||
<div class="WrapperPlain">
|
||||
<div class="max-w-7xl mx-auto ">
|
||||
@@ -84,6 +85,7 @@ import useValidate from "@vuelidate/core"
|
||||
import { required, minLength } from "@vuelidate/validators"
|
||||
import Header from "../../layouts/headers/headernoauth.vue";
|
||||
import authHeader from "../../services/auth.header.ts"
|
||||
import { useAuthStore } from "../../stores/auth"
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
@@ -122,59 +124,87 @@ export default defineComponent({
|
||||
})
|
||||
.then((response:any) => {
|
||||
if (response.data.ok) {
|
||||
const authStore = useAuthStore();
|
||||
authStore.setToken(response.data.user.token, response.data.user);
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
sendLogin(payLoad: { username: string; password: string }) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/auth/login"
|
||||
axios({
|
||||
method: "post",
|
||||
url: path,
|
||||
data: payLoad,
|
||||
withCredentials: true,
|
||||
})
|
||||
.then((response:any) => {
|
||||
if (response.data.user) {
|
||||
localStorage.setItem("auth_token", response.data.token);
|
||||
localStorage.setItem("auth_user", response.data.user);
|
||||
this.$router.push({ name: "home" });
|
||||
sendLogin(payLoad: { username: string; password: string }) {
|
||||
console.log("1. Attempting to send login request with payload:", payLoad);
|
||||
let path = import.meta.env.VITE_BASE_URL + "/auth/login";
|
||||
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "You have been logged in!",
|
||||
type: "success",
|
||||
});
|
||||
axios({
|
||||
method: "post",
|
||||
url: path,
|
||||
data: payLoad,
|
||||
withCredentials: true,
|
||||
})
|
||||
.then((response: any) => {
|
||||
console.log("2. Received response from API:", response);
|
||||
console.log("3. Raw response data from API:", response.data);
|
||||
|
||||
}
|
||||
else if (response.data.locked) {
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "Account has been locked for security reasons. Please unlock.",
|
||||
type: "error",
|
||||
});
|
||||
// Let's check the condition very carefully
|
||||
console.log("4. Checking condition: 'if (response.data.user)'...");
|
||||
|
||||
this.$router.push({ name: "lostPassword" });
|
||||
}
|
||||
else {
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "Login Failure!",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "Login Failure!",
|
||||
type: "error",
|
||||
});
|
||||
|
||||
if (response.data && response.data.user) {
|
||||
console.log("5. SUCCESS: Condition was true. User data found:", response.data.user);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
authStore.setToken(response.data.token, response.data.user);
|
||||
|
||||
console.log("6. Token and user sent to Pinia store. Redirecting to home...");
|
||||
this.$router.push({ name: "home" });
|
||||
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "You have been logged in!",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
} else {
|
||||
console.error("5. FAILURE: Condition was false. The 'response.data.user' object is missing or falsy.");
|
||||
|
||||
if (response.data.locked) {
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "Account has been locked for security reasons. Please unlock.",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
this.$router.push({ name: "lostPassword" });
|
||||
} else {
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "Login Failure! Check username or password.",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error("CRITICAL FAILURE: The API request failed entirely.", error);
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error Response Data:", error.response.data);
|
||||
console.error("Error Response Status:", error.response.status);
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("No response received from the server. Is the backend running? Is the URL correct?", error.request);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.error('Error setting up the request:', error.message);
|
||||
}
|
||||
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "A critical error occurred. Could not connect to the server.",
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
const payLoad = {
|
||||
username: this.loginForm.username,
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
|
||||
|
||||
import Login from '../auth/login.vue';
|
||||
import Register from '../auth/register.vue';
|
||||
import changePassword from '../auth/changepassword.vue';
|
||||
import lostPassword from '../auth/lostpassword.vue';
|
||||
|
||||
|
||||
const authRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
// This page is for logging in, so it should be publicly accessible.
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
// Anyone should be able to access the registration page.
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/lostpassword',
|
||||
name: 'lostPassword',
|
||||
component: lostPassword,
|
||||
// The lost password page must be public.
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/changepassword',
|
||||
name: 'changePassword',
|
||||
component: changePassword,
|
||||
// This page is a bit ambiguous. If any user (even logged out) can change
|
||||
// their password via a tokenized link from an email, it should be public.
|
||||
// If only a logged-in user can change their password, it should be protected.
|
||||
// I will assume it's for tokenized links and make it public.
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
export default authRoutes
|
||||
//sourceMappingURL=index.ts.map
|
||||
export default authRoutes;
|
||||
@@ -40,14 +40,14 @@
|
||||
<!-- Name on Card -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Name on Card</span></label>
|
||||
<input v-model="CardForm.card_name" type="text" placeholder="John M. Doe" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.card_name.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
<input v-model="CardForm.name_on_card" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.name_on_card.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
<!-- Card Number -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Card Number</span></label>
|
||||
<input v-model="CardForm.card_number" type="text" placeholder="4242..." class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.card_number" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.card_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<!-- Security Number (CVV) -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">CVV</span></label>
|
||||
<input v-model="CardForm.security_number" type="text" placeholder="123" class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.security_number" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.security_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<!-- Billing Zip Code -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Billing Zip Code</span></label>
|
||||
<input v-model="CardForm.zip_code" type="text" placeholder="01234" class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.zip_code" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
</div>
|
||||
|
||||
<!-- Main Card Checkbox -->
|
||||
@@ -134,7 +134,7 @@ export default defineComponent({
|
||||
customer: {} as any,
|
||||
// --- REFACTORED: Simplified, flat form object ---
|
||||
CardForm: {
|
||||
card_name: '',
|
||||
name_on_card: '',
|
||||
expiration_month: '',
|
||||
expiration_year: '',
|
||||
type_of_card: '',
|
||||
@@ -149,7 +149,7 @@ export default defineComponent({
|
||||
return {
|
||||
// --- REFACTORED: Validation points to the flat form object ---
|
||||
CardForm: {
|
||||
card_name: { required, minLength: minLength(1) },
|
||||
name_on_card: { required, minLength: minLength(1) },
|
||||
expiration_month: { required },
|
||||
expiration_year: { required },
|
||||
security_number: { required, minLength: minLength(1) },
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<h3 class="text-xl font-bold mb-4">Editing Card</h3>
|
||||
<div v-if="card.id" class="space-y-2">
|
||||
<p><strong class="font-semibold">Card Type:</strong> {{ card.type_of_card }}</p>
|
||||
<p><strong class="font-semibold">Card Number:</strong> **** **** **** {{ card.last_four_digits }}</p>
|
||||
<p><strong class="font-semibold">Card ID:</strong> {{ card.id }}</p>
|
||||
<p><strong class="font-semibold">Card Number:</strong> {{ card.card_number }}</p>
|
||||
|
||||
</div>
|
||||
<div v-else class="text-gray-400">Loading card details...</div>
|
||||
</div>
|
||||
@@ -52,14 +52,14 @@
|
||||
<!-- Name on Card -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Name on Card</span></label>
|
||||
<input v-model="CardForm.name_on_card" type="text" placeholder="John M Doe" class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.name_on_card" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.name_on_card.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
<!-- Card Number -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Card Number</span></label>
|
||||
<input v-model="CardForm.card_number" type="text" placeholder="4242..." class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.card_number" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.card_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- Security Number (CVV) -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">CVV</span></label>
|
||||
<input v-model="CardForm.security_number" type="text" placeholder="123" class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.security_number" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.CardForm.security_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<!-- Billing Zip Code -->
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Billing Zip Code</span></label>
|
||||
<input v-model="CardForm.zip_code" type="text" placeholder="01234" class="input input-bordered input-sm w-full" />
|
||||
<input v-model="CardForm.zip_code" type="text" placeholder="" class="input input-bordered input-sm w-full" />
|
||||
</div>
|
||||
|
||||
<!-- Main Card Checkbox -->
|
||||
@@ -188,53 +188,59 @@ export default defineComponent({
|
||||
axios.get(path, { headers: authHeader() })
|
||||
.then((response: any) => { this.customer = response.data; });
|
||||
},
|
||||
getCard(card_id: any) {
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
|
||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
||||
.then((response: any) => {
|
||||
this.card = response.data; // Store original details for display
|
||||
// Populate the flat form object for editing
|
||||
this.CardForm.name_on_card = response.data.name_on_card;
|
||||
this.CardForm.expiration_month = response.data.expiration_month;
|
||||
this.CardForm.expiration_year = response.data.expiration_year;
|
||||
this.CardForm.type_of_card = response.data.type_of_card;
|
||||
this.CardForm.security_number = response.data.security_number;
|
||||
this.CardForm.main_card = response.data.main_card;
|
||||
this.CardForm.card_number = response.data.card_number;
|
||||
this.CardForm.zip_code = response.data.zip_code;
|
||||
|
||||
if (response.data.user_id) {
|
||||
this.getCustomer(response.data.user_id);
|
||||
}
|
||||
});
|
||||
},
|
||||
editCard(payload: any) {
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/edit/${this.$route.params.id}`;
|
||||
// The backend expects 'card_name', but our form now uses 'name_on_card'.
|
||||
// We must create a new payload that matches the backend's expectation.
|
||||
const backendPayload = {
|
||||
...payload,
|
||||
card_name: payload.name_on_card,
|
||||
};
|
||||
delete backendPayload.name_on_card; // Clean up the object
|
||||
|
||||
axios.put(path, backendPayload, { withCredentials: true, headers: authHeader() })
|
||||
.then((response: any) => {
|
||||
if (response.data.ok) {
|
||||
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
|
||||
} else {
|
||||
console.error("Failed to edit card:", response.data.error);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.v$.$validate();
|
||||
if (!this.v$.$error) {
|
||||
this.editCard(this.CardForm);
|
||||
} else {
|
||||
console.log("Form validation failed.");
|
||||
getCard(card_id: any) {
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
|
||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
||||
.then((response: any) => {
|
||||
this.card = response.data; // Store original details for display
|
||||
|
||||
// Populate the flat form object for editing
|
||||
this.CardForm.name_on_card = response.data.name_on_card;
|
||||
|
||||
// --- FIX IS HERE ---
|
||||
// Convert the month number (e.g., 8) to a zero-padded string ("08") to match the <option> value.
|
||||
this.CardForm.expiration_month = String(response.data.expiration_month).padStart(2, '0');
|
||||
|
||||
// Convert the year number (e.g., 2025) to a string ("2025") for consistency.
|
||||
this.CardForm.expiration_year = String(response.data.expiration_year);
|
||||
// --- END FIX ---
|
||||
|
||||
this.CardForm.type_of_card = response.data.type_of_card;
|
||||
this.CardForm.security_number = response.data.security_number;
|
||||
this.CardForm.main_card = response.data.main_card;
|
||||
this.CardForm.card_number = response.data.card_number;
|
||||
this.CardForm.zip_code = response.data.zip_code;
|
||||
|
||||
if (response.data.user_id) {
|
||||
this.getCustomer(response.data.user_id);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
editCard(payload: any) {
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/edit/${this.$route.params.id}`;
|
||||
|
||||
// REMOVE the payload manipulation. Send the form data directly.
|
||||
// The 'payload' object (which is this.CardForm) is already in the correct format.
|
||||
axios.put(path, payload, { withCredentials: true, headers: authHeader() })
|
||||
.then((response: any) => {
|
||||
if (response.data.ok) {
|
||||
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
|
||||
} else {
|
||||
// You should notify the user here as well
|
||||
|
||||
console.error("Failed to edit card:", response.data.error);
|
||||
}
|
||||
})
|
||||
.catch(console.log("error"));
|
||||
},
|
||||
onSubmit() {
|
||||
this.v$.$validate();
|
||||
if (!this.v$.$error) {
|
||||
this.editCard(this.CardForm); // This is correct, it sends the form object.
|
||||
} else {
|
||||
console.log("Form validation failed.");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@
|
||||
<ul>
|
||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
|
||||
|
||||
<li>Create New Customer</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
<ul>
|
||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
|
||||
<li>Edit Customer</li>
|
||||
<li v-if="customer && customer.id">
|
||||
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }">
|
||||
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,10 +12,17 @@
|
||||
<div class="xl:col-span-8 space-y-6">
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
||||
<ProfileMap
|
||||
class="xl:col-span-7"
|
||||
:customer="customer"
|
||||
/>
|
||||
<ProfileMap
|
||||
v-if="customer && customer.customer_latitude != null && customer.customer_longitude != null"
|
||||
class="xl:col-span-7"
|
||||
:customer="customer"
|
||||
/>
|
||||
|
||||
<!-- You can add a placeholder for when the map isn't ready -->
|
||||
<div v-else class="xl:col-span-7 bg-base-100 rounded-lg flex justify-center items-center">
|
||||
<p class="text-gray-400">Location not available...</p>
|
||||
</div>
|
||||
|
||||
<ProfileSummary
|
||||
class="xl:col-span-5"
|
||||
:customer="customer"
|
||||
@@ -428,18 +435,30 @@ export default defineComponent({
|
||||
editCard(card_id: any) {
|
||||
this.$router.push({ name: "cardedit", params: { id: card_id } });
|
||||
},
|
||||
removeCard(card_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/payment/card/remove/' + card_id;
|
||||
axios({
|
||||
method: 'delete',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then(() => {
|
||||
this.getCreditCards(this.customer.user_id)
|
||||
this.getCreditCardsCount(this.customer.user_id)
|
||||
notify({ title: "Card Status", text: "Card Removed", type: "Success" });
|
||||
})
|
||||
},
|
||||
removeCard(card_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/payment/card/remove/' + card_id;
|
||||
axios({
|
||||
method: 'delete',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then(() => {
|
||||
// --- EFFICIENT FIX: Manipulate the local array directly ---
|
||||
|
||||
// 1. Filter the 'credit_cards' array to remove the card with the matching id.
|
||||
this.credit_cards = this.credit_cards.filter(card => card.id !== card_id);
|
||||
|
||||
// 2. Decrement the count.
|
||||
this.credit_cards_count--;
|
||||
|
||||
// --- END EFFICIENT FIX ---
|
||||
|
||||
notify({ title: "Card Status", text: "Card Removed", type: "Success" });
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
||||
});
|
||||
},
|
||||
deleteCall(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
||||
axios({
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
|
||||
<!-- Fill Location -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label"><span class="label-text">Fill Location Description</span></label>
|
||||
<input v-model="TankForm.fill_location" type="text" placeholder="e.g., Left side of house, behind shed" class="input input-bordered input-sm w-full" />
|
||||
<label class="label"><span class="label-text">Fill Location </span></label>
|
||||
<input v-model="TankForm.fill_location" type="text" placeholder="0-12 only" class="input input-bordered input-sm w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,210 +2,260 @@
|
||||
|
||||
<div class="flex">
|
||||
|
||||
<div class="w-full px-10">
|
||||
<!-- Main container with reduced horizontal padding -->
|
||||
<div class="w-full px-4 md:px-6 py-4">
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
|
||||
<li>Create Delivery for {{ customer.customer_first_name }}</li>
|
||||
<li v-if="customer && customer.id">
|
||||
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }">
|
||||
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- TOP SECTION: Customer Info and Pricing Chart -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Customer Info Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
|
||||
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
|
||||
</div>
|
||||
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
|
||||
View Profile
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ customer.customer_address }}</div>
|
||||
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
|
||||
<div>{{ customer.customer_town }}, {{ customerStateName }} {{ customer.customer_zip }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">{{ customerHomeTypeName }}</div>
|
||||
<div class="mt-2">{{ customer.customer_phone_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
NEW LAYOUT: A single 2-column grid for the whole page.
|
||||
Gaps and spacing are reduced for a more compact feel.
|
||||
-->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
|
||||
|
||||
<!-- Pricing Chart Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<h3 class="text-xl font-bold mb-4">Today's Price Per Gallon</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gallons</th>
|
||||
<th>Total Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tier in pricingTiers" :key="tier.gallons" class="hover">
|
||||
<!-- Access properties of the 'tier' object -->
|
||||
<td>{{ tier.gallons }}</td>
|
||||
<td>${{ parseFloat(tier.price.toString()).toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM SECTION: Forms -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<!-- Create Delivery Form -->
|
||||
<div class="p-6 ">
|
||||
<h2 class="text-2xl font-bold mb-4">Create Delivery Order</h2>
|
||||
<form class="space-y-4" @submit.prevent="onDeliverySubmit">
|
||||
<!-- Gallons & Fill -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Gallons Ordered</span></label>
|
||||
<input v-model="formDelivery.gallons_ordered" :disabled="formDelivery.customer_asked_for_fill"
|
||||
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
|
||||
<span v-if="v$.formDelivery.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
|
||||
Gallons are required unless "Fill" is checked.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<span class="label-text font-bold">Customer Asked for Fill</span>
|
||||
<input v-model="formDelivery.customer_asked_for_fill" type="checkbox" class="checkbox checkbox-sm" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<div class="p-4 border rounded-md space-y-3">
|
||||
<label class="label-text font-bold">Payment Method</label>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Cash</span><input v-model="formDelivery.cash" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div v-if="userCards.length > 0" class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Credit</span><input v-model="formDelivery.credit" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Check</span><input v-model="formDelivery.check" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Other</span><input v-model="formDelivery.other" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<!-- LEFT COLUMN: Primary Information & Actions -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Customer Info Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
|
||||
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
|
||||
</div>
|
||||
<div v-if="userCards.length > 0 && formDelivery.credit">
|
||||
<label class="label"><span class="label-text">Select Card</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.credit_card_id">
|
||||
<option disabled :value="0">Select a card</option>
|
||||
<option v-for="card in userCards" :key="card.id" :value="card.id">
|
||||
{{ card.type_of_card }} - **** {{ card.last_four_digits }}
|
||||
</option>
|
||||
<router-link v-if="customer && customer.id" :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
|
||||
View Profile
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ customer.customer_address }}</div>
|
||||
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
|
||||
<div>{{ customer.customer_town }}, {{ customerStateName }} {{ customer.customer_zip }}</div>
|
||||
<div class="mt-2">{{ customer.customer_phone_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Delivery Form (now in the left column) -->
|
||||
<div class="bg-base-100 rounded-lg p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Create Delivery Order</h2>
|
||||
<form class="space-y-4" @submit.prevent="onDeliverySubmit">
|
||||
<!-- Gallons & Fill -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Gallons Ordered</span></label>
|
||||
<input v-model="formDelivery.gallons_ordered" :disabled="formDelivery.customer_asked_for_fill"
|
||||
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<button v-for="amount in quickGallonAmounts" :key="amount" @click.prevent="setGallons(amount)" class="btn btn-xs btn-outline">{{ amount }} gal</button>
|
||||
</div>
|
||||
<span v-if="v$.formDelivery.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
|
||||
Required unless "Fill" is checked.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<span class="label-text font-bold">Fill</span>
|
||||
<input v-model="formDelivery.customer_asked_for_fill" type="checkbox" class="checkbox checkbox-sm" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<div class="p-4 border rounded-md space-y-3">
|
||||
<label class="label-text font-bold">Payment Method</label>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<div v-if="userCards.length > 0" class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Credit</span><input v-model="formDelivery.credit" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Cash</span><input v-model="formDelivery.cash" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Check</span><input v-model="formDelivery.check" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Other</span><input v-model="formDelivery.other" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
</div>
|
||||
<div v-if="userCards.length > 0 && formDelivery.credit">
|
||||
<label class="label"><span class="label-text">Select Card</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.credit_card_id">
|
||||
<option disabled :value="0">Select a card</option>
|
||||
<option v-for="card in userCards" :key="card.id" :value="card.id">
|
||||
{{ card.type_of_card }} - **** {{ card.last_four_digits }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="userCards.length === 0" class="text-sm text-warning">No cards on file for credit payment.</div>
|
||||
</div>
|
||||
|
||||
<!-- Date, Driver, Promo -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label>
|
||||
<input v-model="formDelivery.expected_delivery_date" class="input input-bordered input-sm w-full max-w-xs" type="date" />
|
||||
<span v-if="v$.formDelivery.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">Date is required.</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Assigned Driver</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.driver_employee_id">
|
||||
<option disabled value="">Select a driver</option>
|
||||
<option v-for="driver in truckDriversList" :key="driver.id" :value="driver.id">
|
||||
{{ driver.employee_first_name }} {{ driver.employee_last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Apply Promotion</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.promo_id">
|
||||
<option :value="0">No Promotion</option>
|
||||
<option v-for="promo in promos" :key="promo.id" :value="promo.id">
|
||||
{{ promo.name_of_promotion }} (${{ promo.money_off_delivery }} off)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="userCards.length === 0" class="text-sm text-warning">No cards on file for credit payment.</div>
|
||||
</div>
|
||||
|
||||
<!-- Date, Driver, Promo -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label>
|
||||
<input v-model="formDelivery.expected_delivery_date" class="input input-bordered input-sm w-full max-w-xs" type="date" />
|
||||
<span v-if="v$.formDelivery.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">Date is required.</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Assigned Driver</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.driver_employee_id">
|
||||
<option disabled value="">Select a driver</option>
|
||||
<option v-for="driver in truckDriversList" :key="driver.id" :value="driver.id">
|
||||
{{ driver.employee_first_name }} {{ driver.employee_last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Apply Promotion</span></label>
|
||||
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.promo_id">
|
||||
<option :value="0">No Promotion</option>
|
||||
<option v-for="promo in promos" :key="promo.id" :value="promo.id">
|
||||
{{ promo.name_of_promotion }} (${{ promo.money_off_delivery }} off)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Fees -->
|
||||
<div class="p-4 border rounded-md">
|
||||
<label class="label-text font-bold">Fees & Options</label>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="formDelivery.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Prime</span><input v-model="formDelivery.prime" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Same Day</span><input v-model="formDelivery.same_day" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fees -->
|
||||
<div class="p-4 border rounded-md">
|
||||
<label class="label-text font-bold">Fees & Options</label>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="formDelivery.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Prime</span><input v-model="formDelivery.prime" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Same Day</span><input v-model="formDelivery.same_day" type="checkbox" class="checkbox checkbox-xs" /></label></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Dispatcher Notes</span></label>
|
||||
<textarea v-model="formDelivery.dispatcher_notes_taken" rows="3" class="textarea textarea-bordered w-full" placeholder="Notes for the driver..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Dispatcher Notes</span></label>
|
||||
<textarea v-model="formDelivery.dispatcher_notes_taken" rows="3" class="textarea textarea-bordered w-full" placeholder="Notes for the driver..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Create Delivery</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm">Create Delivery</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Add Credit Card Form -->
|
||||
<div class="p-6 ">
|
||||
<h2 class="text-2xl font-bold mb-4">Add a Credit Card</h2>
|
||||
<form class="space-y-3" @submit.prevent="onCardSubmit">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- RIGHT COLUMN: Reference Information & Secondary Actions -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Pricing Chart Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<h3 class="text-xl font-bold mb-4">Today's Price Per Gallon</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gallons</th>
|
||||
<th>Total Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tier in pricingTiers" :key="tier.gallons" class="hover">
|
||||
<td>{{ tier.gallons }}</td>
|
||||
<td>${{ Number(tier.price).toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Cards Display -->
|
||||
<div v-if="customer && customer.id" class="bg-neutral rounded-lg p-5">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold">Credit Cards</h2>
|
||||
<router-link :to="{ name: 'cardadd', params: { id: customer.id } }">
|
||||
<button class="btn btn-xs btn-outline btn-success">Add New</button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="mt-2 text-sm" v-if="userCards.length === 0">
|
||||
<p class="text-warning font-semibold">No cards on file.</p>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div v-for="card in userCards" :key="card.id" class="p-3 rounded-lg border" :class="card.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Name on Card</span></label>
|
||||
<input v-model="formCard.card_name" type="text" placeholder="John M. Doe" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.card_name.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Card Number</span></label>
|
||||
<input v-model="formCard.card_number" type="text" placeholder="4242..." class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.card_number.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Expiration</span></label>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="formCard.expiration_month" class="select select-bordered select-sm w-full">
|
||||
<option disabled value="">MM</option>
|
||||
<option v-for="m in 12" :key="m" :value_count="String(m).padStart(2, '0')">{{ String(m).padStart(2, '0') }}</option>
|
||||
</select>
|
||||
<select v-model="formCard.expiration_year" class="select select-bordered select-sm w-full">
|
||||
<option disabled value="">YYYY</option>
|
||||
<option v-for="y in 10" :key="y" :value_count="(new Date().getFullYear() + y - 1)">{{ new Date().getFullYear() + y - 1 }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span v-if="v$.formCard.expiration_month.$error || v$.formCard.expiration_year.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">CVC / CVV</span></label>
|
||||
<input v-model="formCard.security_number" type="text" placeholder="123" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.security_number.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Card Type</span></label>
|
||||
<select v-model="formCard.type_of_card" class="select select-bordered select-sm w-full">
|
||||
<option disabled value="">Select Type</option>
|
||||
<option>Visa</option><option>MasterCard</option><option>Discover</option><option>American Express</option>
|
||||
</select>
|
||||
<span v-if="v$.formCard.type_of_card.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-bold">Billing Zip Code</span></label>
|
||||
<input v-model="formCard.zip_code" type="text" placeholder="01234" class="input input-bordered input-sm w-full" />
|
||||
<div class="font-bold text-sm">{{ card.name_on_card }}</div>
|
||||
<div class="text-xs opacity-70">{{ card.type_of_card }}</div>
|
||||
</div>
|
||||
<div v-if="card.main_card" class="badge badge-primary badge-sm">Primary</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<span class="label-text font-bold">Set as Main Card</span>
|
||||
<input v-model="formCard.main_card" type="checkbox" class="checkbox checkbox-sm" />
|
||||
</label>
|
||||
<div class="mt-2 text-sm font-mono tracking-wider">
|
||||
<p>**** **** **** {{ card.last_four_digits }}</p>
|
||||
<p>Exp: <span v-if="card.expiration_month < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<a @click.prevent="editCard(card.id)" class="link link-hover text-xs">Edit</a>
|
||||
<a @click.prevent="removeCard(card.id)" class="link link-hover text-error text-xs">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Credit Card Form -->
|
||||
<div class="bg-base-100 rounded-lg p-4">
|
||||
<h2 class="text-xl font-bold mb-4">Quick Add Card</h2>
|
||||
<form class="space-y-3" @submit.prevent="onCardSubmit">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">Name on Card</span></label>
|
||||
<input v-model="formCard.card_name" type="text" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.card_name.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">Card Number</span></label>
|
||||
<input v-model="formCard.card_number" type="text" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.card_number.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">Expiration</span></label>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="formCard.expiration_month" class="select select-bordered select-sm w-full"><option disabled value="">MM</option><option v-for="m in 12" :key="m">{{ String(m).padStart(2, '0') }}</option></select>
|
||||
<select v-model="formCard.expiration_year" class="select select-bordered select-sm w-full"><option disabled value="">YYYY</option><option v-for="y in 10" :key="y">{{ new Date().getFullYear() + y - 1 }}</option></select>
|
||||
</div>
|
||||
<span v-if="v$.formCard.expiration_month.$error || v$.formCard.expiration_year.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">CVC</span></label>
|
||||
<input v-model="formCard.security_number" type="text" class="input input-bordered input-sm w-full" />
|
||||
<span v-if="v$.formCard.security_number.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">Card Type</span></label>
|
||||
<select v-model="formCard.type_of_card" class="select select-bordered select-sm w-full"><option disabled value="">Select Type</option><option>Visa</option><option>MasterCard</option><option>Discover</option><option>American Express</option></select>
|
||||
<span v-if="v$.formCard.type_of_card.$error" class="text-red-500 text-xs mt-1">Required</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1"><span class="label-text">Billing Zip</span></label>
|
||||
<input v-model="formCard.zip_code" type="text" class="input input-bordered input-sm w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Save Credit Card</button>
|
||||
</form>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4 py-1">
|
||||
<span class="label-text">Set as Main Card</span>
|
||||
<input v-model="formCard.main_card" type="checkbox" class="checkbox checkbox-sm" />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Save Card</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{ 'modal-open': isConfirmationModalVisible }">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Confirm Payment Method</h3>
|
||||
<p class="py-4">You have selected a non-standard payment method (Cash or Check). Please confirm you wish to proceed.</p>
|
||||
<div class="modal-action">
|
||||
<button @click="proceedWithSubmission" class="btn btn-primary">Confirm & Create</button>
|
||||
<button @click="isConfirmationModalVisible = false" class="btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
@@ -235,10 +285,15 @@ interface Customer {
|
||||
customer_address: string;
|
||||
account_number: string;
|
||||
}
|
||||
// FIX: Updated UserCard interface to include all necessary display properties
|
||||
interface UserCard {
|
||||
id: number;
|
||||
name_on_card: string;
|
||||
type_of_card: string;
|
||||
last_four_digits: string;
|
||||
expiration_month: number;
|
||||
expiration_year: number;
|
||||
main_card: boolean;
|
||||
}
|
||||
interface Promo {
|
||||
id: number;
|
||||
@@ -250,12 +305,10 @@ interface Driver {
|
||||
employee_first_name: string;
|
||||
employee_last_name: string;
|
||||
}
|
||||
interface PricingTier { // <-- CHANGED: New interface for a single tier
|
||||
interface PricingTier {
|
||||
gallons: number | string;
|
||||
price: number | string;
|
||||
}
|
||||
|
||||
// --- Define types for your FLAT form models to match the new template ---
|
||||
interface DeliveryFormData {
|
||||
gallons_ordered: string;
|
||||
customer_asked_for_fill: boolean;
|
||||
@@ -290,12 +343,12 @@ export default defineComponent({
|
||||
return {
|
||||
v$: useValidate(),
|
||||
user: null as any,
|
||||
checked: false,
|
||||
quickGallonAmounts: [100, 125, 150, 200, 220],
|
||||
userCards: [] as UserCard[],
|
||||
promos: [] as Promo[],
|
||||
truckDriversList: [] as Driver[],
|
||||
pricingTiers: [] as PricingTier[],
|
||||
// --- FIX: Use flat form objects that match the template ---
|
||||
pricingTiers: [] as PricingTier[],
|
||||
isConfirmationModalVisible: false,
|
||||
formDelivery: {
|
||||
gallons_ordered: '',
|
||||
customer_asked_for_fill: false,
|
||||
@@ -327,7 +380,6 @@ export default defineComponent({
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
// --- FIX: Validation targets the new flat form objects ---
|
||||
formDelivery: {
|
||||
gallons_ordered: { required: requiredIf(function(this: any) {
|
||||
return !this.formDelivery.customer_asked_for_fill;
|
||||
@@ -379,13 +431,21 @@ export default defineComponent({
|
||||
this.getPaymentCards(customerId);
|
||||
},
|
||||
methods: {
|
||||
setGallons(amount: number) {
|
||||
this.formDelivery.gallons_ordered = String(amount);
|
||||
this.formDelivery.customer_asked_for_fill = false;
|
||||
},
|
||||
getPricingTiers() {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
|
||||
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
|
||||
.then((response: SimpleResponse<PricingTier[]>) => {
|
||||
this.pricingTiers = response.data;
|
||||
.then((response: SimpleResponse<{ [key: string]: string }>) => {
|
||||
const tiersObject = response.data;
|
||||
this.pricingTiers = Object.entries(tiersObject).map(([gallons, price]) => ({
|
||||
gallons: parseInt(gallons, 10),
|
||||
price: price,
|
||||
}));
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
.catch(() => {
|
||||
notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" });
|
||||
});
|
||||
},
|
||||
@@ -395,7 +455,7 @@ export default defineComponent({
|
||||
.then((response: SimpleResponse<Customer>) => {
|
||||
this.customer = response.data;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
.catch(() => {
|
||||
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
||||
});
|
||||
},
|
||||
@@ -405,7 +465,7 @@ export default defineComponent({
|
||||
.then((response: SimpleResponse<UserCard[]>) => {
|
||||
this.userCards = response.data;
|
||||
})
|
||||
.catch((error: unknown) => { /* empty */ });
|
||||
.catch(() => { /* empty */ });
|
||||
},
|
||||
getPromos() {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/promo/all";
|
||||
@@ -413,7 +473,7 @@ export default defineComponent({
|
||||
.then((response: SimpleResponse<Promo[]>) => {
|
||||
this.promos = response.data;
|
||||
})
|
||||
.catch((error: unknown) => { /* empty */ });
|
||||
.catch(() => { /* empty */ });
|
||||
},
|
||||
getDriversList() {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
|
||||
@@ -421,11 +481,45 @@ export default defineComponent({
|
||||
.then((response: SimpleResponse<Driver[]>) => {
|
||||
this.truckDriversList = response.data;
|
||||
})
|
||||
.catch((error: unknown) => { /* empty */ });
|
||||
.catch(() => { /* empty */ });
|
||||
},
|
||||
|
||||
// --- FIX: New method to navigate to the card edit page ---
|
||||
editCard(card_id: number) {
|
||||
this.$router.push({ name: "cardedit", params: { id: card_id } });
|
||||
},
|
||||
|
||||
// --- FIX: New method to handle removing a card ---
|
||||
removeCard(card_id: number) {
|
||||
if (window.confirm("Are you sure you want to remove this card?")) {
|
||||
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`;
|
||||
axios.delete(path, { headers: authHeader() })
|
||||
.then(() => {
|
||||
notify({ title: "Card Removed", type: "success" });
|
||||
// Refresh the card list after deletion
|
||||
this.getPaymentCards(this.customer.id);
|
||||
})
|
||||
.catch(() => {
|
||||
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// --- FIX: Renamed to match template's @submit event ---
|
||||
onDeliverySubmit() {
|
||||
this.v$.formDelivery.$validate();
|
||||
if (this.v$.formDelivery.$error) {
|
||||
notify({ title: "Validation Error", text: "Please fill out all required fields.", type: "error" });
|
||||
return;
|
||||
}
|
||||
if (this.formDelivery.cash || this.formDelivery.check) {
|
||||
this.isConfirmationModalVisible = true;
|
||||
} else {
|
||||
this.proceedWithSubmission();
|
||||
}
|
||||
},
|
||||
|
||||
proceedWithSubmission() {
|
||||
this.isConfirmationModalVisible = false;
|
||||
let payload = {
|
||||
gallons_ordered: this.formDelivery.gallons_ordered,
|
||||
customer_asked_for_fill: this.formDelivery.customer_asked_for_fill,
|
||||
@@ -443,30 +537,32 @@ export default defineComponent({
|
||||
driver_employee_id: this.formDelivery.driver_employee_id,
|
||||
};
|
||||
|
||||
let pass = 0;
|
||||
if (payload.driver_employee_id === '') {
|
||||
notify({ title: "Error", text: "Please assign a driver.", type: "error" });
|
||||
pass += 1;
|
||||
return;
|
||||
}
|
||||
if (!payload.cash && !payload.credit && !payload.check && !payload.other) {
|
||||
notify({ title: "Error", text: "Please select a payment method.", type: "error" });
|
||||
pass += 1;
|
||||
}
|
||||
if (pass === 0) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/create/" + this.customer.id;
|
||||
axios({ method: "post", url: path, data: payload, withCredentials: true, headers: authHeader() })
|
||||
.then((response: SimpleResponse<{ ok: boolean; delivery_id: number; error?: string }>) => {
|
||||
if (response.data.ok) {
|
||||
this.$router.push({ name: "payOil", params: { id: response.data.delivery_id } });
|
||||
} else {
|
||||
this.$router.push("/");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let path = `${import.meta.env.VITE_BASE_URL}/delivery/create/${this.customer.id}`;
|
||||
axios({ method: "post", url: path, data: payload, withCredentials: true, headers: authHeader() })
|
||||
.then((response: SimpleResponse<{ ok: boolean; delivery_id: number }>) => {
|
||||
if (response.data.ok) {
|
||||
this.$router.push({ name: "payOil", params: { id: response.data.delivery_id } });
|
||||
} else {
|
||||
this.$router.push("/");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// --- FIX: Renamed to match template's @submit event ---
|
||||
|
||||
onCardSubmit() {
|
||||
this.v$.formCard.$validate();
|
||||
if (this.v$.formCard.$error) {
|
||||
notify({ title: "Validation Error", text: "Please fill out all card fields.", type: "error" });
|
||||
return;
|
||||
}
|
||||
let payload = {
|
||||
card_name: this.formCard.card_name,
|
||||
card_number: this.formCard.card_number,
|
||||
@@ -478,13 +574,12 @@ export default defineComponent({
|
||||
zip_code: this.formCard.zip_code,
|
||||
};
|
||||
|
||||
let path = import.meta.env.VITE_BASE_URL + "/payment/card/create/" + this.customer.id;
|
||||
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/create/${this.customer.id}`;
|
||||
axios({ method: "post", url: path, data: payload, withCredentials: true, headers: authHeader() })
|
||||
.then((response: SimpleResponse<{ ok: boolean; error?: string }>) => {
|
||||
.then((response: SimpleResponse<{ ok: boolean }>) => {
|
||||
if (response.data.ok) {
|
||||
notify({ type: 'success', title: 'Card Saved!' });
|
||||
this.getPaymentCards(this.$route.params.id);
|
||||
// Optional: Reset form after successful submission
|
||||
Object.assign(this.formCard, { card_name: '', card_number: '', expiration_month: '', expiration_year: '', type_of_card: '', security_number: '', zip_code: '', main_card: false });
|
||||
this.v$.formCard.$reset();
|
||||
} else {
|
||||
@@ -494,7 +589,4 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@@ -23,7 +23,11 @@
|
||||
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
|
||||
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
|
||||
</div>
|
||||
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
|
||||
<!--
|
||||
FIX #1: Add a v-if guard.
|
||||
This prevents the link from rendering until `customer.user_id` has a valid, non-zero value.
|
||||
-->
|
||||
<router-link v-if="customer " :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
|
||||
View Profile
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -75,6 +79,7 @@
|
||||
<span v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</span>
|
||||
<span v-else-if="deliveryOrder.delivery_status == 5">Misdelivery</span>
|
||||
<span v-else-if="deliveryOrder.delivery_status == 6">Unknown</span>
|
||||
<span v-else-if="deliveryOrder.delivery_status == 9">Pending</span>
|
||||
<span v-else-if="deliveryOrder.delivery_status == 10">Finalized</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,28 +149,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Payment, Notes, Actions -->
|
||||
<div class="space-y-4">
|
||||
<!-- Payment -->
|
||||
<div class="p-4 border rounded-md">
|
||||
<label class="label-text font-bold">Payment Method</label>
|
||||
<div class="mt-1">
|
||||
<div class="text-lg">
|
||||
<span v-if="deliveryOrder.payment_type == 0">Cash</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 1">Credit Card</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 2">Credit Card & Cash</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 3">Check</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 4">Other</span>
|
||||
<span v-else>Not Specified</span>
|
||||
</div>
|
||||
<div v-if="userCardfound && [1, 2, 3].includes(deliveryOrder.payment_type)" class="bg-base-100 p-3 rounded-md mt-2 text-sm">
|
||||
<div class="font-mono">{{ userCard.type_of_card }}</div>
|
||||
<div class="font-mono">{{ userCard.card_number }}</div>
|
||||
<div>{{ userCard.name_on_card }}</div>
|
||||
<div>Expires: {{ userCard.expiration_month }}/{{ userCard.expiration_year }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!--
|
||||
START: Replaced Payment Section
|
||||
-->
|
||||
<div class="p-4 border rounded-md">
|
||||
<label class="label-text font-bold">Payment Method</label>
|
||||
<div class="mt-1">
|
||||
<div class="text-lg">
|
||||
<span v-if="deliveryOrder.payment_type == 0">Cash</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 1">Credit Card</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 2">Credit Card & Cash</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 3">Check</span>
|
||||
<span v-else-if="deliveryOrder.payment_type == 4">Other</span>
|
||||
<span v-else>Not Specified</span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
This is the new, styled card display.
|
||||
It uses the same logic but applies the better CSS classes.
|
||||
-->
|
||||
<div v-if="userCardfound && [1, 2, 3].includes(deliveryOrder.payment_type)"
|
||||
class="p-4 rounded-lg border mt-2"
|
||||
:class="userCard.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
|
||||
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-bold">{{ userCard.name_on_card }}</div>
|
||||
<div class="text-xs opacity-70">{{ userCard.type_of_card }}</div>
|
||||
</div>
|
||||
<div v-if="userCard.main_card" class="badge badge-primary badge-sm">Primary</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-sm font-mono tracking-wider">
|
||||
<!-- Using last_four_digits is more secure and looks cleaner -->
|
||||
<p>**** **** **** {{ userCard.last_four_digits }}</p>
|
||||
<p>
|
||||
Exp:
|
||||
<!-- Adds a leading zero for single-digit months -->
|
||||
<span v-if="Number(userCard.expiration_month) < 10">0</span>{{ userCard.expiration_month }} / {{ userCard.expiration_year }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes & Options -->
|
||||
<div class="p-4 border rounded-md">
|
||||
@@ -180,8 +207,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-wrap gap-2 pt-4">
|
||||
<!--
|
||||
FIX #2: Add a v-if guard to the container.
|
||||
This prevents the links from rendering until `deliveryOrder.id` is available.
|
||||
-->
|
||||
<div v-if="deliveryOrder && deliveryOrder.id" class="flex flex-wrap gap-2 pt-4">
|
||||
<router-link :to="{ name: 'deliveryEdit', params: { id: deliveryOrder.id } }">
|
||||
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
|
||||
</router-link>
|
||||
@@ -196,7 +226,6 @@
|
||||
</div>
|
||||
<Footer />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
@@ -221,7 +250,7 @@ export default defineComponent({
|
||||
return {
|
||||
v$: useValidate(),
|
||||
user: {
|
||||
id: 0
|
||||
user_id: 0
|
||||
},
|
||||
priceprime: 0,
|
||||
pricesameday: 0,
|
||||
@@ -232,19 +261,18 @@ export default defineComponent({
|
||||
deliveryNotesDriver: [],
|
||||
userCardfound: false,
|
||||
userCard: {
|
||||
date_added: '',
|
||||
user_id: '',
|
||||
card_number: '',
|
||||
last_four_digits: '',
|
||||
name_on_card: '',
|
||||
expiration_month: '',
|
||||
expiration_year: '',
|
||||
type_of_card: '',
|
||||
security_number: '',
|
||||
accepted_or_declined: '',
|
||||
main_card: '',
|
||||
},
|
||||
|
||||
date_added: '',
|
||||
user_id: 0, // Should be a number
|
||||
card_number: '',
|
||||
last_four_digits: '',
|
||||
name_on_card: '',
|
||||
expiration_month: 0, // Initialize as a number
|
||||
expiration_year: 0, // Initialize as a number
|
||||
type_of_card: '',
|
||||
security_number: '',
|
||||
accepted_or_declined: null, // null is better for optional values
|
||||
main_card: false, // Should be a boolean
|
||||
},
|
||||
customer: {
|
||||
account_number: '',
|
||||
id: 0,
|
||||
@@ -358,7 +386,7 @@ export default defineComponent({
|
||||
text: "deleted delivery",
|
||||
type: "success",
|
||||
});
|
||||
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
|
||||
this.$router.push({ name: "customerProfile", params: { id: this.customer.user_id } });
|
||||
} else {
|
||||
notify({
|
||||
title: "Failure",
|
||||
@@ -379,7 +407,7 @@ export default defineComponent({
|
||||
.then((response: any) => {
|
||||
if (response.data.ok) {
|
||||
this.user = response.data.user;
|
||||
this.user.id = response.data.user_id;
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -445,34 +473,40 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
getOilOrder(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (response.data) {
|
||||
this.deliveryOrder = response.data
|
||||
this.getCustomer(this.deliveryOrder.customer_id)
|
||||
if (this.deliveryOrder.payment_type == 1) {
|
||||
this.getPaymentCard(this.deliveryOrder.payment_card_id);
|
||||
}
|
||||
if (this.deliveryOrder.payment_type == 2) {
|
||||
this.getPaymentCard(this.deliveryOrder.payment_card_id);
|
||||
}
|
||||
if (this.deliveryOrder.payment_type == 3) {
|
||||
this.getPaymentCard(this.deliveryOrder.payment_card_id);
|
||||
}
|
||||
if (this.deliveryOrder.promo_id != null) {
|
||||
this.getPromo(this.deliveryOrder.promo_id);
|
||||
}
|
||||
getOilOrder(delivery_id: any) {
|
||||
if (!delivery_id) { // Add a guard to prevent calls with an undefined ID
|
||||
console.error("getOilOrder called with no ID.");
|
||||
return;
|
||||
}
|
||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/" + delivery_id;
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
|
||||
if (response.data && response.data.ok) {
|
||||
this.deliveryOrder = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
// Now that this.deliveryOrder is the correct object, the rest of the logic will work.
|
||||
this.getCustomer(this.deliveryOrder.customer_id);
|
||||
|
||||
if ([1, 2, 3].includes(this.deliveryOrder.payment_type)) {
|
||||
this.getPaymentCard(this.deliveryOrder.payment_card_id);
|
||||
}
|
||||
if (this.deliveryOrder.promo_id != null) {
|
||||
this.getPromo(this.deliveryOrder.promo_id);
|
||||
}
|
||||
} else {
|
||||
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
|
||||
notify({ title: "Error", text: "Could not load delivery details.", type: "error" });
|
||||
}
|
||||
})
|
||||
.catch( console.log("")
|
||||
);
|
||||
},
|
||||
getOilOrderMoney(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_MONEY_URL + "/delivery/order/money/" + delivery_id;
|
||||
axios({
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Archived Cancelled Deliveries</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} items Found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Deliveries Awaiting Finalization</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} items Found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Completed and Finalized Deliveries</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} items Found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Deliveries Requiring Attention</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} items Found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Deliveries Awaiting Payment</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} items Found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<h2 class="text-lg font-bold">Todays Deliveries</h2>
|
||||
<div class="form-control">
|
||||
<label class="label pt-1 pb-0">
|
||||
<span class="label-text-alt">{{ recordsLength }} deliveries found</span>
|
||||
<!-- <span class="label-text-alt">{{ recordsLength }} deliveries found</span> -->
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- Header: Title and Count (No Search Input) -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Deliveries Scheduled</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} deliveries found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} deliveries found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- Header: Title and Count -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
|
||||
<h2 class="text-lg font-bold">Deliveries Awaiting Dispatch</h2>
|
||||
<div class="badge badge-ghost">{{ recordsLength }} deliveries found</div>
|
||||
<!-- <div class="badge badge-ghost">{{ recordsLength }} deliveries found</div> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -1,302 +1,157 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
|
||||
<div class=" w-full px-10">
|
||||
<div class="w-full px-4 md:px-10 py-4">
|
||||
<!-- Breadcrumbs & Title -->
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: 'home' }">
|
||||
Home
|
||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||
<!-- Add a link to the customer's profile if the data is available -->
|
||||
<li v-if="customer && customer.id">
|
||||
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }">
|
||||
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>Confirm Payment</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mt-4 border-b border-gray-600 pb-2">
|
||||
Confirm Delivery #{{ delivery.id }}
|
||||
</h1>
|
||||
|
||||
<div class="grid grid-cols-1 rounded-md p-6 mb-5">
|
||||
<div class=" col-span-12 text-2xl">
|
||||
Confirm Payment Oil Delivery {{ delivery.id }}
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 my-6">
|
||||
|
||||
<!-- LEFT COLUMN: Customer and Delivery Details -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Customer Info Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
|
||||
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
|
||||
</div>
|
||||
<router-link v-if="customer && customer.id" :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
|
||||
View Profile
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div>{{ customer.customer_address }}</div>
|
||||
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">Apt: {{ customer.customer_apt }}</div>
|
||||
<div>{{ customer.customer_town }}, {{ customer.customer_state === 0 ? 'MA' : 'RI' }} {{ customer.customer_zip }}</div>
|
||||
<div class="mt-2">{{ customer.customer_phone_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery Details Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<h3 class="text-xl font-bold mb-4">Delivery Details</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="font-bold text-sm">Current Status</div>
|
||||
<div class="badge" :class="{ 'badge-info': delivery.delivery_status === 0, 'badge-success': delivery.delivery_status === 1, 'badge-warning': delivery.delivery_status > 1 }">
|
||||
<span v-if="delivery.delivery_status == 0">Waiting</span>
|
||||
<span v-else-if="delivery.delivery_status == 1">Delivered</span>
|
||||
<span v-else-if="delivery.delivery_status == 2">Out for Delivery</span>
|
||||
<span v-else-if="delivery.delivery_status == 3">Cancelled</span>
|
||||
<span v-else-if="delivery.delivery_status == 4">Partial Delivery</span>
|
||||
<span v-else-if="delivery.delivery_status == 5">Issue</span>
|
||||
<span v-else>Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-sm">Scheduled Date</div>
|
||||
<div>{{ delivery.expected_delivery_date }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-sm">Gallons Ordered</div>
|
||||
<div>
|
||||
<span v-if="delivery.customer_asked_for_fill == 1" class="badge badge-info">FILL (250 gal estimate)</span>
|
||||
<span v-else>{{ delivery.gallons_ordered }} Gallons</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-12 mb-5 gap-5">
|
||||
|
||||
<div class="col-span-12 xl:col-span-6 ">
|
||||
<div class="col-span-12 font-bold flex pb-5 text-lg">
|
||||
{{ customer.account_number }}
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex">
|
||||
{{ customer.customer_first_name }}
|
||||
{{ customer.customer_last_name }}
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex">
|
||||
{{ customer.customer_address }}
|
||||
<div v-if="customer.customer_apt != 'None'">
|
||||
{{ customer.customer_apt }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex">
|
||||
<div class="pr-2">
|
||||
{{ customer.customer_town }},
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
<div v-if="customer.customer_state == 0">Massachusetts</div>
|
||||
<div v-else-if="customer.customer_state == 1">Rhode Island</div>
|
||||
<div v-else-if="customer.customer_state == 2">New Hampshire</div>
|
||||
<div v-else-if="customer.customer_state == 3">Maine</div>
|
||||
<div v-else-if="customer.customer_state == 4">Vermont</div>
|
||||
<div v-else-if="customer.customer_state == 5">Maine</div>
|
||||
<div v-else-if="customer.customer_state == 6">New York</div>
|
||||
<div v-else>Unknown state</div>
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
{{ customer.customer_zip }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex" v-if="customer.customer_apt !== 'None'">
|
||||
{{ customer.customer_apt }}
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex">
|
||||
<div v-if="customer.customer_home_type == 0">Residential</div>
|
||||
<div v-else-if="customer.customer_home_type == 1">apartment</div>
|
||||
<div v-else-if="customer.customer_home_type == 2">condo</div>
|
||||
<div v-else-if="customer.customer_home_type == 3">commercial</div>
|
||||
<div v-else-if="customer.customer_home_type == 4">business</div>
|
||||
<div v-else-if="customer.customer_home_type == 5">construction</div>
|
||||
<div v-else-if="customer.customer_home_type == 6">container</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold flex">
|
||||
{{ customer.customer_phone_number }}
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Payment and Pricing Details -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<div class="col-span-12 ">
|
||||
<div class="grid grid-cols-12 mb-5">
|
||||
<div class="col-span-12 pt-10 font-bold">Delivery Status</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
<div v-if="delivery.delivery_status == 0"> Waiting</div>
|
||||
<div v-else-if="delivery.delivery_status == 1"> delivered</div>
|
||||
<div v-else-if="delivery.delivery_status == 2"> Out for Delivery</div>
|
||||
<div v-else-if="delivery.delivery_status == 3">Cancelled</div>
|
||||
<div v-else-if="delivery.delivery_status == 4"> Partial Delivery</div>
|
||||
<div v-else-if="delivery.delivery_status == 5">Issue</div>
|
||||
<div v-else></div>
|
||||
<!-- Payment & Pricing Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<h3 class="text-xl font-bold mb-4">Payment & Pricing</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Payment Method -->
|
||||
<div>
|
||||
<div class="font-bold text-sm">Payment Method</div>
|
||||
<div class="text-lg">
|
||||
<span v-if="delivery.payment_type == 0">Cash</span>
|
||||
<span v-else-if="delivery.payment_type == 1">Credit Card</span>
|
||||
<span v-else-if="delivery.payment_type == 2">Credit Card / Cash</span>
|
||||
<span v-else-if="delivery.payment_type == 3">Check</span>
|
||||
<span v-else-if="delivery.payment_type == 4">Other</span>
|
||||
</div>
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Expected Delivery:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ delivery.expected_delivery_date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<div class="grid grid-cols-12 mb-5 ">
|
||||
<div v-for="card in credit_cards" class="col-span-12">
|
||||
<div class="col-span-12 ">
|
||||
<div v-if="card.main_card" class="basis-1/3 p-2">
|
||||
<div class="bg-secondary rounded-md border-2 ">
|
||||
<div class="flex p-3">
|
||||
{{ card.type_of_card }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.name_on_card }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.card_number }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.expiration_month }}/ {{ card.expiration_year }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.security_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="basis-1/3 p-2">
|
||||
<div class=" rounded-md border-2 ">
|
||||
<div class="flex p-3">
|
||||
{{ card.type_of_card }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.name_on_card }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.card_number }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.expiration_month }}/ {{ card.expiration_year }}
|
||||
</div>
|
||||
<div class="flex p-1 pl-4">
|
||||
{{ card.security_number }}
|
||||
</div>
|
||||
<!-- Show the main card if payment is by credit -->
|
||||
<div v-if="delivery.payment_type == 1 || delivery.payment_type == 3" class="mt-2">
|
||||
<div v-for="card in credit_cards" :key="card.id">
|
||||
<div v-if="card.main_card" class="bg-base-100 p-3 rounded-md text-sm">
|
||||
<div class="font-mono font-semibold">{{ card.type_of_card }} ending in {{ card.last_four_digits }}</div>
|
||||
<div>{{ card.name_on_card }}</div>
|
||||
<div>Expires: {{ card.expiration_month }}/{{ card.expiration_year }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 mb-5">
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Payment Type:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500" v-if="delivery.payment_type == 0">
|
||||
Cash
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500" v-if="delivery.payment_type == 1">
|
||||
Credit Card
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500" v-if="delivery.payment_type == 2">
|
||||
Credit Card / Cash
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500" v-if="delivery.payment_type == 3">
|
||||
Check with CC Hold
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500" v-if="delivery.payment_type == 4">
|
||||
Other (Rent, etc):
|
||||
</div>
|
||||
<div v-if="promo_active" class="col-span-12">
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Promo
|
||||
|
||||
<!-- Pricing Breakdown -->
|
||||
<div class="space-y-2 pt-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>Price per Gallon</span>
|
||||
<span>${{ delivery.customer_price }}</span>
|
||||
</div>
|
||||
<div v-if="delivery.prime == 1" class="flex justify-between text-sm">
|
||||
<span>Prime Fee</span>
|
||||
<span>+ ${{ pricing.price_prime }}</span>
|
||||
</div>
|
||||
<div v-if="delivery.emergency == 1" class="flex justify-between text-sm">
|
||||
<span>Emergency Fee</span>
|
||||
<span>+ ${{ pricing.price_emergency }}</span>
|
||||
</div>
|
||||
<div v-if="delivery.same_day == 1" class="flex justify-between text-sm">
|
||||
<span>Same Day Fee</span>
|
||||
<span>+ ${{ pricing.price_same_day }}</span>
|
||||
</div>
|
||||
<div v-if="promo_active" class="flex justify-between text-sm text-success">
|
||||
<span>Promo: {{ promo.name_of_promotion }}</span>
|
||||
<span>- ${{ discount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ promo.name_of_promotion }}
|
||||
</div>
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Promo Discount:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ promo.money_off_delivery }} off a gallon
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ discount }} off total delivery
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ total_amount_after_discount }} total price after discount
|
||||
</div>
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Price / Gallon:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
{{ delivery.customer_price }} ({{ delivery.customer_price - promo.money_off_delivery}})
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-1"></div>
|
||||
|
||||
|
||||
|
||||
<div class="col-span-12 pt-3 font-bold">
|
||||
Gallons Ordered:
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 text-gray-500">
|
||||
<div v-if="delivery.customer_asked_for_fill == 1"> FILL (250)</div>
|
||||
<div v-else> Gallons Ordered: {{ delivery.gallons_ordered }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 py-3" v-if="delivery.prime == 1">
|
||||
Prime Fee: {{ pricing.price_prime }}
|
||||
</div>
|
||||
<div class="col-span-12 py-3" v-if="delivery.emergency == 1">
|
||||
Emergency Fee: {{ pricing.price_emergency }}
|
||||
</div>
|
||||
<div class="col-span-12 py-3" v-if="delivery.same_day == 1">
|
||||
Same Day: {{ pricing.price_same_day }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-span-12 py-3" v-if="delivery.promo_id != null">
|
||||
<div class="col-span-12 " v-if="delivery.payment_type == 0">
|
||||
<div class="font-bold py-5">
|
||||
CASH Total:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
${{ total_amount_after_discount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 1">
|
||||
<div class="">
|
||||
Pre Charge Credit Card Total: ${{ total_amount_after_discount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 2">
|
||||
<div class="">
|
||||
Pre Charge Credit Card Total: ${{ total_amount_after_discount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 3">
|
||||
<div class="">
|
||||
Check - Pre Charge Credit Card Total: ${{ total_amount_after_discount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 py-3" v-else>
|
||||
<div class="col-span-12 " v-if="delivery.payment_type == 0">
|
||||
<div class="font-bold py-5">
|
||||
CASH Total:
|
||||
</div>
|
||||
<div class="col-span-12 text-gray-500">
|
||||
${{ total_amount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 1">
|
||||
<div class="">
|
||||
Pre Charge Credit Card Total: ${{ total_amount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 2">
|
||||
<div class="">
|
||||
Pre Charge Credit Card Total: ${{ total_amount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 font-bold py-5 text-accent" v-if="delivery.payment_type == 3">
|
||||
<div class="">
|
||||
Check - Pre Charge Credit Card Total: ${{ total_amount }}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg font-bold">Total to be Charged</span>
|
||||
<span class="text-2xl font-bold text-accent">
|
||||
${{ promo_active ? total_amount_after_discount : total_amount }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between">
|
||||
|
||||
<div class="" v-if="delivery.payment_type == 0">
|
||||
<button class="btn bg-green-800 btn-sm" @click="checkoutOilUpdatePayment(0)">
|
||||
Confirm Payment
|
||||
</button>
|
||||
</div>
|
||||
<div class="" v-if="delivery.payment_type == 1">
|
||||
<button class="btn bg-green-800 btn-sm" @click="checkoutOilUpdatePayment(1)">
|
||||
Confirm Payment
|
||||
</button>
|
||||
</div>
|
||||
<div class="" v-if="delivery.payment_type == 2">
|
||||
<button class="btn bg-green-800 btn-sm" @click="checkoutOilUpdatePayment(2)">
|
||||
Confirm Payment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="" v-if="delivery.payment_type == 3">
|
||||
<button class="btn bg-green-800 btn-sm" @click="checkoutOilUpdatePayment(3)">
|
||||
Confirm Payment
|
||||
<!-- Actions Card -->
|
||||
<div class="bg-neutral rounded-lg p-5">
|
||||
<div class="flex flex-wrap gap-4 justify-between items-center">
|
||||
<!-- A single confirm button is cleaner -->
|
||||
<button class="btn btn-primary" @click="checkoutOilUpdatePayment(delivery.payment_type)">
|
||||
Confirm & Process Payment
|
||||
</button>
|
||||
</div>
|
||||
<div class="" v-if="delivery.payment_type == 4">
|
||||
<button class="btn bg-green-800 btn-sm" @click="checkoutOilUpdatePayment(3)">
|
||||
Confirm Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 py-10">
|
||||
|
||||
<router-link :to="{ name: 'deliveryEdit', params: { id: delivery.id } }">
|
||||
<button class="btn btn-sm btn-secondary">Edit Delivery</button>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="delivery && delivery.id" :to="{ name: 'deliveryEdit', params: { id: delivery.id } }">
|
||||
<button class="btn btn-sm btn-ghost">Edit Delivery</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +159,6 @@
|
||||
<Footer />
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Removed @click from tr to avoid conflicting actions -->
|
||||
<tr v-for="service in services" :key="service.id" class="hover">
|
||||
<tr v-for="service in services" :key="service.id" class=" hover:bg-blue-600">
|
||||
<td class="align-top">
|
||||
<div>{{ formatDate(service.scheduled_date) }}</div>
|
||||
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
|
||||
@@ -56,7 +56,10 @@
|
||||
<td class="align-top">{{ service.customer_name }}</td>
|
||||
<td class="align-top">{{ service.customer_address }}, {{ service.customer_town }}</td>
|
||||
<td class="align-top">
|
||||
<span class="font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
|
||||
<span
|
||||
class="badge badge-sm text-white"
|
||||
:style="{ 'background-color': getServiceTypeColor(service.type_service_call), 'border-color': getServiceTypeColor(service.type_service_call) }"
|
||||
>
|
||||
{{ getServiceTypeName(service.type_service_call) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="w-full px-4 md:px-10 py-4">
|
||||
@@ -46,21 +47,30 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Removed @click from tr to avoid conflicting with "Read more" -->
|
||||
<tr v-for="service in services" :key="service.id" class="hover">
|
||||
<tr v-for="service in services" :key="service.id" class="hover:bg-blue-600">
|
||||
<td class="align-top">
|
||||
<div>{{ formatDate(service.scheduled_date) }}</div>
|
||||
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
|
||||
</td>
|
||||
<td class="align-top">{{ service.customer_name }}</td>
|
||||
<td class="align-top">{{ service.customer_address }}, {{ service.customer_town }}</td>
|
||||
|
||||
<!--
|
||||
FIX IS HERE: Replaced the colored text with a styled badge.
|
||||
- `badge-sm`: Makes the pill small and compact.
|
||||
- `text-white`: Ensures text is readable against the colored background.
|
||||
- The background color is set dynamically using your existing `getServiceTypeColor` method.
|
||||
-->
|
||||
<td class="align-top">
|
||||
<span class="font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
|
||||
<span
|
||||
class="badge badge-sm text-white"
|
||||
:style="{ 'background-color': getServiceTypeColor(service.type_service_call), 'border-color': getServiceTypeColor(service.type_service_call) }"
|
||||
>
|
||||
{{ getServiceTypeName(service.type_service_call) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="whitespace-normal text-sm align-top">
|
||||
<!-- TRUNCATION LOGIC FOR DESKTOP -->
|
||||
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
|
||||
{{ service.description }}
|
||||
<a v-if="isLongDescription(service.description)" @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Show less</a>
|
||||
@@ -72,7 +82,6 @@
|
||||
</td>
|
||||
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
|
||||
<td class="text-right align-top">
|
||||
<!-- Moved @click handler to the button for explicit action -->
|
||||
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -82,14 +91,14 @@
|
||||
|
||||
<!-- MOBILE VIEW: Cards (Revamped) -->
|
||||
<div class="xl:hidden space-y-4">
|
||||
<!-- Removed @click from card div -->
|
||||
<div v-for="service in services" :key="service.id" class="card bg-base-100 shadow-md">
|
||||
<div v-for="service in services" :key="service.id" class="card bg-base-100 shadow-md ">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="card-title text-base">{{ service.customer_name }}</h2>
|
||||
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
|
||||
</div>
|
||||
<!-- Mobile view already uses a badge, which is great! No changes needed here. -->
|
||||
<div class="badge badge-outline text-right" :style="{ 'border-color': getServiceTypeColor(service.type_service_call), color: getServiceTypeColor(service.type_service_call) }">
|
||||
{{ getServiceTypeName(service.type_service_call) }}
|
||||
</div>
|
||||
@@ -101,7 +110,6 @@
|
||||
<p><strong class="font-semibold">Cost:</strong> <span class="font-mono">{{ formatCurrency(service.service_cost) }}</span></p>
|
||||
</div>
|
||||
|
||||
<!-- TRUNCATION LOGIC FOR MOBILE -->
|
||||
<div v-if="service.description" class="text-sm mt-2 p-2 bg-base-200 rounded-md prose max-w-none">
|
||||
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
|
||||
{{ service.description }}
|
||||
@@ -114,7 +122,6 @@
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<!-- Moved @click handler to the button -->
|
||||
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,6 +142,7 @@
|
||||
@delete-service="handleDeleteService"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
@@ -297,7 +305,7 @@ export default defineComponent({
|
||||
0: 'Tune-up',
|
||||
1: 'No Heat',
|
||||
2: 'Fix',
|
||||
3: 'Tank Install',
|
||||
3: 'Tank_Install',
|
||||
4: 'Other',
|
||||
};
|
||||
return typeMap[typeId] || 'Unknown Service';
|
||||
|
||||
@@ -1,45 +1,94 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
|
||||
// Import your route modules
|
||||
import authRoutes from '../pages/auth/routes.ts';
|
||||
import oilRoutes from '../pages/delivery/routes.ts';
|
||||
import employeeRoutes from '../pages/employee/routes.ts';
|
||||
import payRoutes from '../pages/pay/routes.ts';
|
||||
import customerRoutes from '../pages/customer/routes.ts';
|
||||
import Home from '../pages/Index.vue';
|
||||
import cardRoutes from '../pages/card/routes.ts'
|
||||
import autoRoutes from '../pages/automatic/routes.ts'
|
||||
import Error404 from '../pages/error/Error404.vue'
|
||||
import cardRoutes from '../pages/card/routes.ts';
|
||||
import autoRoutes from '../pages/automatic/routes.ts';
|
||||
import adminRoutes from "../pages/admin/routes.ts";
|
||||
import tickerRoutes from "../pages/ticket/routes.ts";
|
||||
import moneyRoutes from "../pages/money/routes.ts";
|
||||
import serviceRoutes from "../pages/service/routes.ts";
|
||||
|
||||
// Import your page components
|
||||
import Home from '../pages/Index.vue';
|
||||
import Error404 from '../pages/error/Error404.vue';
|
||||
|
||||
// Import the Pinia Auth Store
|
||||
import { useAuthStore } from '../stores/auth.ts';
|
||||
|
||||
const protectRoutes = (routes: any[]) => {
|
||||
return routes.map(route => {
|
||||
route.meta = { ...route.meta, requiresAuth: true };
|
||||
if (route.children) {
|
||||
route.children = protectRoutes(route.children);
|
||||
}
|
||||
return route;
|
||||
});
|
||||
};
|
||||
|
||||
const routes = [
|
||||
...moneyRoutes,
|
||||
...cardRoutes,
|
||||
// Auth routes are public and don't get protected
|
||||
...authRoutes,
|
||||
...payRoutes,
|
||||
...employeeRoutes,
|
||||
...customerRoutes,
|
||||
...oilRoutes,
|
||||
...autoRoutes,
|
||||
...adminRoutes,
|
||||
...tickerRoutes,
|
||||
...serviceRoutes,
|
||||
...protectRoutes(moneyRoutes),
|
||||
...protectRoutes(cardRoutes),
|
||||
...protectRoutes(payRoutes),
|
||||
...protectRoutes(employeeRoutes),
|
||||
...protectRoutes(customerRoutes),
|
||||
...protectRoutes(oilRoutes),
|
||||
...protectRoutes(autoRoutes),
|
||||
...protectRoutes(adminRoutes),
|
||||
...protectRoutes(tickerRoutes),
|
||||
...protectRoutes(serviceRoutes),
|
||||
|
||||
|
||||
// ...(moneyRoutes),
|
||||
// ...(cardRoutes),
|
||||
// ...(payRoutes),
|
||||
// ...(employeeRoutes),
|
||||
// ...(customerRoutes),
|
||||
// ...(oilRoutes),
|
||||
// ...(autoRoutes),
|
||||
// ...(adminRoutes),
|
||||
// ...(tickerRoutes),
|
||||
// ...(serviceRoutes),
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// Error Pages
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
name: 'Error404',
|
||||
component: Error404,
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
]
|
||||
|
||||
let router = createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
export default router
|
||||
//sourceMappingURL=index.ts.map
|
||||
|
||||
// --- THE NAVIGATION GUARD ---
|
||||
// @ts-ignore
|
||||
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
const authStore = useAuthStore();
|
||||
const isAuthenticated = await authStore.checkAuthStatus();
|
||||
const requiresAuth = to.meta.requiresAuth;
|
||||
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
next({ name: 'login' });
|
||||
} else if (to.name === 'login' && isAuthenticated) {
|
||||
next({ name: 'home' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export default function authHeader (): Record<string, string> {
|
||||
export default function authHeader(): { Authorization: string } | {} {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
let user_token = localStorage.getItem('auth_user')
|
||||
let auth_token = localStorage.getItem('auth_token')
|
||||
|
||||
if (user_token && auth_token) {
|
||||
return { 'Authorization': 'bearer ' + auth_token };
|
||||
}
|
||||
else {
|
||||
return {'Authorization': 'None'};
|
||||
}
|
||||
if (token) {
|
||||
// Return the header in the format expected by the API
|
||||
return { Authorization: 'Bearer ' + token };
|
||||
} else {
|
||||
// Return an empty object if no token is found
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { fetchWrapper } from '@/helpers';
|
||||
import { router } from '@/router';
|
||||
|
||||
|
||||
const baseUrl = `${import.meta.env.VITE_BASE_URL}/users`;
|
||||
|
||||
export const useAuthStore = defineStore({
|
||||
id: 'auth',
|
||||
state: () => ({
|
||||
// initialize state from local storage to enable user to stay logged in
|
||||
user: JSON.parse(localStorage.getItem('user')),
|
||||
returnUrl: null
|
||||
}),
|
||||
actions: {
|
||||
async login(username, password) {
|
||||
|
||||
const user = await fetchWrapper.post(`${baseUrl}/authenticate`,
|
||||
{ username, password });
|
||||
|
||||
// update pinia state
|
||||
this.user = user;
|
||||
|
||||
// store user details and jwt in local storage to keep user logged in between page refreshes
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
|
||||
// redirect to previous url or default to home page
|
||||
router.push(this.returnUrl || '/');
|
||||
|
||||
},
|
||||
logout() {
|
||||
this.user = null;
|
||||
localStorage.removeItem('user');
|
||||
router.push('/account/login');
|
||||
}
|
||||
}
|
||||
});
|
||||
88
src/stores/auth.ts
Executable file
88
src/stores/auth.ts
Executable file
@@ -0,0 +1,88 @@
|
||||
// src/stores/auth.ts
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import authHeader from '../services/auth.header' // Adjust path if needed
|
||||
|
||||
interface User {
|
||||
user_name: string;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// --- STATE ---
|
||||
const token = ref(localStorage.getItem('auth_token') || null)
|
||||
const user = ref<User | null>(null)
|
||||
const storedUser = localStorage.getItem('auth_user')
|
||||
if (storedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(storedUser)
|
||||
} catch {
|
||||
// Invalid JSON, clear it
|
||||
localStorage.removeItem('auth_user')
|
||||
}
|
||||
}
|
||||
|
||||
// --- GETTERS ---
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
// --- ACTIONS ---
|
||||
function setToken(newToken: string, newUser?: User) {
|
||||
// For debugging: confirms this function is being called correctly from Login.vue
|
||||
console.log('Pinia Action [setToken] called with:', { newToken, newUser });
|
||||
|
||||
localStorage.setItem('auth_token', newToken)
|
||||
token.value = newToken
|
||||
if (newUser) {
|
||||
localStorage.setItem('auth_user', JSON.stringify(newUser))
|
||||
user.value = newUser
|
||||
}
|
||||
|
||||
// --- THIS IS THE FIX ---
|
||||
// The string must be a template literal (using backticks ``)
|
||||
// and should follow the "Bearer <token>" format.
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
|
||||
|
||||
console.log('Token and user have been set in localStorage and Axios headers.');
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
token.value = null
|
||||
user.value = null
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
}
|
||||
|
||||
async function checkAuthStatus() {
|
||||
if (!isAuthenticated.value) {
|
||||
return false // No token, definitely not authenticated
|
||||
}
|
||||
try {
|
||||
// Use your existing endpoint to verify the token
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/auth/whoami`;
|
||||
const response = await axios.get(path, { headers: authHeader() });
|
||||
if (response.data && response.data.ok) {
|
||||
user.value = response.data.user
|
||||
return true
|
||||
}
|
||||
// If the token is invalid, the API returns an error, which will be caught
|
||||
clearAuth()
|
||||
return false
|
||||
} catch {
|
||||
clearAuth()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// --- RETURN ---
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isAuthenticated,
|
||||
setToken,
|
||||
clearAuth,
|
||||
checkAuthStatus,
|
||||
}
|
||||
})
|
||||
52
src/stores/counts.ts
Normal file
52
src/stores/counts.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// src/stores/counts.ts
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import authHeader from '../services/auth.header' // Adjust path if needed
|
||||
|
||||
export const useCountsStore = defineStore('counts', () => {
|
||||
// --- STATE ---
|
||||
// All counts are now stored here, centrally.
|
||||
const today = ref(0)
|
||||
const tomorrow = ref(0)
|
||||
const waiting = ref(0)
|
||||
const pending = ref(0)
|
||||
const automatic = ref(0)
|
||||
const upcoming_service = ref(0)
|
||||
|
||||
// --- ACTIONS ---
|
||||
// A single action to fetch ALL counts from our new, efficient endpoint.
|
||||
async function fetchSidebarCounts() {
|
||||
try {
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/deliverystatus/stats/sidebar-counts`;
|
||||
const response = await axios.get(path, { headers: authHeader() });
|
||||
|
||||
if (response.data && response.data.ok) {
|
||||
const counts = response.data.counts;
|
||||
today.value = counts.today;
|
||||
tomorrow.value = counts.tomorrow;
|
||||
waiting.value = counts.waiting;
|
||||
pending.value = counts.pending;
|
||||
automatic.value = counts.automatic;
|
||||
upcoming_service.value = counts.upcoming_service;
|
||||
}
|
||||
} catch {
|
||||
// No error param, as requested
|
||||
// Optionally reset counts on error
|
||||
console.error("Failed to fetch sidebar counts.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- RETURN ---
|
||||
// Expose the state and the action to the rest of the app.
|
||||
return {
|
||||
today,
|
||||
tomorrow,
|
||||
waiting,
|
||||
pending,
|
||||
automatic,
|
||||
upcoming_service,
|
||||
fetchSidebarCounts,
|
||||
}
|
||||
})
|
||||
@@ -4,15 +4,15 @@ import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
|
||||
// Define a type for what a search result looks like. This is good practice.
|
||||
// Define a type for what a search result looks like.
|
||||
interface CustomerSearchResult {
|
||||
id: number;
|
||||
customer_first_name: string;
|
||||
customer_last_name: string;
|
||||
customer_address: string;
|
||||
customer_town: string;
|
||||
customer_state: string;
|
||||
customer_phone_number: string;
|
||||
customer_state: number; // Changed to number to match your database
|
||||
// You can add other properties here if needed
|
||||
}
|
||||
|
||||
export const useSearchStore = defineStore('search', () => {
|
||||
@@ -20,20 +20,16 @@ export const useSearchStore = defineStore('search', () => {
|
||||
const searchTerm = ref('');
|
||||
const searchResults = ref<CustomerSearchResult[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// --- NEW: A variable to hold our timer ID for debouncing ---
|
||||
// Using `any` is okay here, but `number` for browser or `Timeout` for Node is more specific.
|
||||
let debounceTimer: any = null;
|
||||
|
||||
|
||||
// --- GETTERS ---
|
||||
const showResults = computed(() => {
|
||||
return searchTerm.value.length > 1 && searchResults.value.length > 0;
|
||||
// FIX: The box should be visible as long as the user has typed enough.
|
||||
// The component inside will handle showing the "No results found" message.
|
||||
return searchTerm.value.length > 1;
|
||||
});
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
// This is the original function that makes the API call. We'll keep it.
|
||||
async function fetchSearchResults() {
|
||||
if (searchTerm.value.length < 2) {
|
||||
searchResults.value = [];
|
||||
@@ -42,48 +38,37 @@ export const useSearchStore = defineStore('search', () => {
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.get(`/search/customer?q=${searchTerm.value}`);
|
||||
// NOTE: Make sure this URL is correct. You may need to add your VITE_BASE_URL
|
||||
const path = `${import.meta.env.VITE_BASE_URL}/search/customer?q=${searchTerm.value}`;
|
||||
const response = await axios.get(path);
|
||||
searchResults.value = response.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch search results:", error);
|
||||
} catch { // No `error` parameter as requested
|
||||
searchResults.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW: This is the debounced action your component will call ---
|
||||
function debouncedSearch() {
|
||||
// 1. Clear any existing timer. If the user is still typing, this
|
||||
// cancels the previous plan to make an API call.
|
||||
clearTimeout(debounceTimer);
|
||||
|
||||
// 2. Set a new timer. We will only call `fetchSearchResults` after the
|
||||
// user has stopped typing for 400 milliseconds.
|
||||
debounceTimer = setTimeout(() => {
|
||||
fetchSearchResults();
|
||||
}, 400); // 400ms is a good balance between responsiveness and efficiency
|
||||
}, 400);
|
||||
}
|
||||
|
||||
|
||||
function clearSearch() {
|
||||
searchTerm.value = '';
|
||||
searchResults.value = [];
|
||||
// Also clear the timer if a search is in progress
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
// --- RETURN ---
|
||||
return {
|
||||
// State
|
||||
searchTerm,
|
||||
searchResults,
|
||||
isLoading,
|
||||
// Getters
|
||||
showResults,
|
||||
// Actions
|
||||
fetchSearchResults, // You might not need to export this anymore
|
||||
debouncedSearch, // Export the new debounced function
|
||||
debouncedSearch,
|
||||
clearSearch,
|
||||
}
|
||||
})
|
||||
@@ -1,64 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { fetchWrapper } from '@/helpers';
|
||||
import { useAuthStore } from '@/stores';
|
||||
|
||||
const baseUrl = `${import.meta.env.VITE_BASE_URL}/auth`;
|
||||
|
||||
export const useUsersStore = defineStore({
|
||||
id: 'users',
|
||||
state: () => ({
|
||||
users: {},
|
||||
user: {}
|
||||
}),
|
||||
actions: {
|
||||
async register(user) {
|
||||
await fetchWrapper.post(`${baseUrl}/register`, user);
|
||||
},
|
||||
async getAll() {
|
||||
this.users = { loading: true };
|
||||
try {
|
||||
this.users = await fetchWrapper.get(baseUrl);
|
||||
} catch (error) {
|
||||
this.users = { error };
|
||||
}
|
||||
},
|
||||
async getById(id) {
|
||||
this.user = { loading: true };
|
||||
try {
|
||||
this.user = await fetchWrapper.get(`${baseUrl}/${id}`);
|
||||
} catch (error) {
|
||||
this.user = { error };
|
||||
}
|
||||
},
|
||||
async update(id, params) {
|
||||
await fetchWrapper.put(`${baseUrl}/${id}`, params);
|
||||
|
||||
// update stored user if the logged-in user updated their own record
|
||||
const authStore = useAuthStore();
|
||||
if (id === authStore.user.id) {
|
||||
// update local storage
|
||||
const user = { ...authStore.user, ...params };
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
|
||||
// update auth user in pinia state
|
||||
authStore.user = user;
|
||||
}
|
||||
},
|
||||
async delete(id) {
|
||||
// add isDeleting prop to user being deleted
|
||||
this.users.find(x => x.id === id).isDeleting = true;
|
||||
|
||||
await fetchWrapper.delete(`${baseUrl}/${id}`);
|
||||
|
||||
// remove user from list after deleted
|
||||
this.users = this.users.filter(x => x.id !== id);
|
||||
|
||||
// auto logout if the logged in user deleted their own record
|
||||
const authStore = useAuthStore();
|
||||
if (id === authStore.user.id) {
|
||||
authStore.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user