diff --git a/GIT_BRANCH_SETUP.md b/GIT_BRANCH_SETUP.md new file mode 100644 index 0000000..4480e3f --- /dev/null +++ b/GIT_BRANCH_SETUP.md @@ -0,0 +1,299 @@ +# Git Branch Setup Guide + +## Creating Branches: akhib and dundu + +### Quick Commands + +```bash +# Create and push akhib branch +git checkout -b akhib +git push -u origin akhib + +# Create and push dundu branch +git checkout -b dundu +git push -u origin dundu + +# Go back to main branch +git checkout main +``` + +## Detailed Step-by-Step + +### Step 1: Check Your Current Branch + +```bash +git branch +``` + +This shows which branch you're currently on (marked with *). + +### Step 2: Create akhib Branch + +```bash +# Create new branch from current position +git checkout -b akhib + +# Verify you're on the new branch +git branch + +# Push to remote and set upstream +git push -u origin akhib +``` + +### Step 3: Create dundu Branch + +```bash +# Go back to main first +git checkout main + +# Create new branch from main +git checkout -b dundu + +# Push to remote and set upstream +git push -u origin dundu +``` + +### Step 4: Verify All Branches Exist + +```bash +# List local branches +git branch + +# List all branches (local + remote) +git branch -a +``` + +You should see: +``` +* main + akhib + dundu + remotes/origin/akhib + remotes/origin/dundu + remotes/origin/main +``` + +## Branch Workflow for Team + +### For Developer Working on akhib Branch + +```bash +# Switch to akhib branch +git checkout akhib + +# Make sure it's up to date +git pull origin akhib + +# Make changes, then commit +git add . +git commit -m "Your commit message" + +# Push changes +git push origin akhib +``` + +### For Developer Working on dundu Branch + +```bash +# Switch to dundu branch +git checkout dundu + +# Make sure it's up to date +git pull origin dundu + +# Make changes, then commit +git add . +git commit -m "Your commit message" + +# Push changes +git push origin dundu +``` + +## Merging Changes + +### Merge akhib into main + +```bash +# Switch to main +git checkout main + +# Pull latest changes +git pull origin main + +# Merge akhib branch +git merge akhib + +# Push merged changes +git push origin main +``` + +### Merge dundu into main + +```bash +# Switch to main +git checkout main + +# Pull latest changes +git pull origin main + +# Merge dundu branch +git merge dundu + +# Push merged changes +git push origin main +``` + +## Branch Protection (Recommended) + +After creating branches, consider setting up branch protection rules on GitHub/GitLab: + +1. **Protect main branch** - Require pull requests +2. **Require reviews** - At least 1 approval before merging +3. **Require tests** - Ensure CI/CD passes + +## Common Branch Commands + +```bash +# List all branches +git branch -a + +# Switch branches +git checkout branch-name + +# Create new branch +git checkout -b new-branch-name + +# Delete local branch +git branch -d branch-name + +# Delete remote branch +git push origin --delete branch-name + +# Rename current branch +git branch -m new-name + +# See which branch you're on +git branch + +# Update all branches from remote +git fetch --all + +# See commits difference between branches +git log main..akhib +``` + +## Branch Strategy Recommendation + +### Main Branch +- **Purpose:** Production-ready code +- **Protection:** Require pull requests +- **Who commits:** No direct commits (only via PR) + +### akhib Branch +- **Purpose:** Akhib's development work +- **Protection:** Optional +- **Who commits:** Akhib (and approved team members) + +### dundu Branch +- **Purpose:** Dundu's development work +- **Protection:** Optional +- **Who commits:** Dundu (and approved team members) + +## Workflow Example + +```mermaid +graph LR + A[main] --> B[akhib branch] + A --> C[dundu branch] + B --> D[Pull Request to main] + C --> E[Pull Request to main] + D --> A + E --> A +``` + +**Recommended Flow:** +1. Developer creates feature on their branch (akhib/dundu) +2. Commits and pushes regularly +3. When ready, creates Pull Request to main +4. Team reviews the PR +5. After approval, merge to main +6. Delete feature branch (optional) or keep for next feature + +## Syncing Branches + +### Keep akhib up to date with main + +```bash +git checkout akhib +git pull origin main +git push origin akhib +``` + +### Keep dundu up to date with main + +```bash +git checkout dundu +git pull origin main +git push origin dundu +``` + +## Troubleshooting + +### Error: "branch already exists" + +```bash +# If branch exists locally, just switch to it +git checkout akhib + +# If you want to recreate it +git branch -d akhib +git checkout -b akhib +``` + +### Error: "failed to push some refs" + +```bash +# Pull first, then push +git pull origin akhib +git push origin akhib +``` + +### Merge Conflicts + +```bash +# When merge conflict occurs +git status # See conflicted files + +# Edit conflicted files manually +# Look for <<<<<<, ======, >>>>>> markers + +# After fixing conflicts +git add . +git commit -m "Resolve merge conflicts" +git push +``` + +## Quick Reference + +| Command | Description | +|---------|-------------| +| `git checkout -b akhib` | Create akhib branch | +| `git push -u origin akhib` | Push and track akhib | +| `git checkout akhib` | Switch to akhib | +| `git branch` | List local branches | +| `git branch -a` | List all branches | +| `git merge akhib` | Merge akhib into current | +| `git pull origin akhib` | Update akhib from remote | + +## Setting Up Branch Protection (GitHub) + +1. Go to: Settings → Branches → Add rule +2. Branch name pattern: `main` +3. Enable: + - ✅ Require pull request reviews before merging + - ✅ Require status checks to pass + - ✅ Require branches to be up to date +4. Save changes + +Now your branches are ready for team collaboration! 🎉 + diff --git a/QUICK_START_FOR_TEAM.md b/QUICK_START_FOR_TEAM.md new file mode 100644 index 0000000..e54ee9f --- /dev/null +++ b/QUICK_START_FOR_TEAM.md @@ -0,0 +1,220 @@ +# Quick Start Guide for Team Members + +## 📋 For Akhib and Dundu + +### Step 1: Accept Invitation ✉️ + +Check your email for GitHub invitation and click **Accept invitation**. + +### Step 2: Clone Repository 📥 + +```bash +# Open terminal/command prompt and run: +git clone https://github.com/YOUR_USERNAME/frappe-frontend.git +cd frappe-frontend +``` + +### Step 3: Install Dependencies 📦 + +```bash +npm install +``` + +This will take a few minutes. Wait for it to complete. + +### Step 4: Checkout Your Branch 🌿 + +**For Akhib:** +```bash +git checkout akhib +``` + +**For Dundu:** +```bash +git checkout dundu +``` + +### Step 5: Start Development 🚀 + +```bash +npm run dev +``` + +Open browser: http://localhost:3000 + +--- + +## 📝 Daily Workflow + +### Morning (Start Work) + +```bash +# Get latest changes +git pull origin akhib # or dundu + +# Start dev server +npm run dev +``` + +### During Work (Save Changes) + +```bash +# Check what changed +git status + +# Add all changes +git add . + +# Commit with message +git commit -m "Your description here" + +# Push to remote +git push origin akhib # or dundu +``` + +### Evening (End of Day) + +```bash +# Make sure everything is saved +git status + +# Push if needed +git push origin akhib # or dundu +``` + +--- + +## 🆘 Common Issues + +### Issue: "Permission denied" + +**Solution:** Make sure you accepted the GitHub invitation. + +### Issue: "npm install" fails + +**Solution:** +```bash +# Delete node_modules +rm -rf node_modules +npm cache clean --force +npm install +``` + +### Issue: Branch doesn't exist + +**Solution:** +```bash +git fetch origin +git checkout akhib # or dundu +``` + +### Issue: "Cannot push to remote" + +**Solution:** +```bash +# Pull first, then push +git pull origin akhib +git push origin akhib +``` + +--- + +## 🎯 Important Commands + +| Command | What it does | +|---------|--------------| +| `git status` | See what changed | +| `git add .` | Stage all changes | +| `git commit -m "msg"` | Save changes | +| `git push` | Upload to GitHub | +| `git pull` | Download from GitHub | +| `npm run dev` | Start dev server | +| `npm install` | Install packages | + +--- + +## 📞 Need Help? + +1. Check if dev server is running: http://localhost:3000 +2. Check terminal for error messages +3. Try restarting: Stop server (Ctrl+C) and run `npm run dev` again +4. Contact team lead + +--- + +## ✅ Checklist for First Day + +- [ ] Accepted GitHub invitation +- [ ] Cloned repository +- [ ] Ran `npm install` successfully +- [ ] Switched to my branch (akhib or dundu) +- [ ] Started dev server (`npm run dev`) +- [ ] Saw application in browser +- [ ] Made test change +- [ ] Committed and pushed test change +- [ ] Saw my change on GitHub + +--- + +## 🎨 Project Structure + +``` +frappe-frontend/ +├── src/ +│ ├── pages/ # All pages (Login, Dashboard, etc) +│ ├── components/ # Reusable components +│ ├── services/ # API calls +│ ├── hooks/ # Custom React hooks +│ └── contexts/ # React contexts (Theme, etc) +├── public/ # Static files +└── package.json # Project dependencies +``` + +--- + +## 🌟 Best Practices + +1. **Commit often** - Don't wait until end of day +2. **Write clear messages** - "Fixed login bug" not "fixed stuff" +3. **Pull before push** - Always get latest changes first +4. **Test before commit** - Make sure it works +5. **Ask questions** - Better to ask than break things! + +--- + +## 🔐 Git Config (One-time Setup) + +```bash +# Set your name and email +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" + +# Check settings +git config --list +``` + +--- + +## 💻 VS Code Extensions (Recommended) + +- **ES7+ React/Redux/React-Native snippets** +- **GitLens** - Better Git integration +- **Prettier** - Code formatter +- **ESLint** - Code quality +- **Auto Rename Tag** - HTML/JSX helper + +--- + +## 🎓 Learning Resources + +- **React:** https://react.dev/learn +- **TypeScript:** https://www.typescriptlang.org/docs/ +- **Git Basics:** https://git-scm.com/book/en/v2 +- **Tailwind CSS:** https://tailwindcss.com/docs + +--- + +**Remember:** Your branch (akhib/dundu) is YOUR workspace. Feel free to experiment! + +Good luck! 🚀 + diff --git a/index.html b/index.html index f15a777..af7a0f5 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + frappe-frontend diff --git a/package-lock.json b/package-lock.json index c061cce..3480585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/react-router-dom": "^5.3.3", "axios": "^1.12.2", + "lucide-react": "^0.553.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", @@ -77,6 +78,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1482,6 +1484,7 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1491,6 +1494,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1572,6 +1576,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1824,6 +1829,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2051,6 +2057,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -2307,18 +2314,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2484,6 +2479,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3191,18 +3187,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3294,280 +3278,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, - "license": "MPL-2.0", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3621,6 +3331,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.553.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz", + "integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3971,6 +3690,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4166,6 +3886,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4175,6 +3896,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4716,6 +4438,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4775,6 +4498,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4868,6 +4592,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4961,6 +4686,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index e3d872c..a8c6cca 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@types/react-router-dom": "^5.3.3", "axios": "^1.12.2", + "lucide-react": "^0.553.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", diff --git a/public/seera-logo.png b/public/seera-logo.png new file mode 100644 index 0000000..a978072 Binary files /dev/null and b/public/seera-logo.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ba32ed3..cf594da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,12 @@ import UsersList from './pages/UsersList'; import EventsList from './pages/EventsList'; import AssetList from './pages/AssetList'; import AssetDetail from './pages/AssetDetail'; +import WorkOrderList from './pages/WorkOrderList'; +import WorkOrderDetail from './pages/WorkOrderDetail'; +import AssetMaintenanceList from './pages/AssetMaintenanceList'; +import AssetMaintenanceDetail from './pages/AssetMaintenanceDetail'; +import PPMList from './pages/PPMList'; +import PPMDetail from './pages/PPMDetail'; import Sidebar from './components/Sidebar'; // Layout with Sidebar @@ -71,6 +77,72 @@ const App: React.FC = () => { } /> + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + void; + placeholder?: string; + disabled?: boolean; + filters?: Record; +} + +const LinkField: React.FC = ({ + label, + doctype, + value, + onChange, + placeholder, + disabled = false, + filters = {}, +}) => { + const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]); + const [searchText, setSearchText] = useState(''); + const [isDropdownOpen, setDropdownOpen] = useState(false); + const containerRef = useRef(null); + + // Fetch link options from ERPNext with filters + const searchLink = async (text: string = '') => { + try { + const params = new URLSearchParams({ + doctype, + txt: text, + }); + + // Add filters if provided + if (filters && Object.keys(filters).length > 0) { + // Convert filters to JSON string for Frappe API + params.append('filters', JSON.stringify(filters)); + } + + const response = await apiService.apiCall<{ value: string; description?: string }[]>( + `/api/method/frappe.desk.search.search_link?${params.toString()}` + ); + setSearchResults(response || []); + } catch (error) { + console.error(`Error fetching ${doctype} links:`, error); + setSearchResults([]); + } + }; + + // Fetch default options when dropdown opens or filters change + useEffect(() => { + if (isDropdownOpen) { + searchLink(searchText || ''); + } + }, [isDropdownOpen, filters]); // Re-fetch when filters change + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + + !disabled && setDropdownOpen(true)} + onChange={(e) => { + const text = e.target.value; + setSearchText(text); + searchLink(text); + onChange(text); + }} + /> + + {isDropdownOpen && searchResults.length > 0 && !disabled && ( +
    + {searchResults.map((item, idx) => ( +
  • { + onChange(item.value); + setDropdownOpen(false); + }} + className={`px-3 py-2 cursor-pointer + text-gray-900 dark:text-gray-100 + hover:bg-blue-500 dark:hover:bg-blue-600 hover:text-white + ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`} + > + {item.value} + {item.description && ( + + {item.description} + + )} +
  • + ))} +
+ )} + + {/* Show message when no results found */} + {isDropdownOpen && searchResults.length === 0 && !disabled && ( +
+ No results found +
+ )} +
+ ); +}; + +export default LinkField; + + +// import React, { useState, useEffect, useRef } from 'react'; +// import apiService from '../services/apiService'; // ✅ your ApiService + +// interface LinkFieldProps { +// label: string; +// doctype: string; +// value: string; +// onChange: (value: string) => void; +// placeholder?: string; +// disabled?: boolean; +// filters?: Record +// } + +// const LinkField: React.FC = ({ +// label, +// doctype, +// value, +// onChange, +// placeholder, +// disabled = false, +// }) => { +// const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]); +// const [searchText, setSearchText] = useState(''); +// const [isDropdownOpen, setDropdownOpen] = useState(false); +// const containerRef = useRef(null); + +// // Fetch link options from ERPNext +// const searchLink = async (text: string = '') => { +// try { +// const params = new URLSearchParams({ doctype, txt: text }); +// const response = await apiService.apiCall<{ value: string; description?: string }[]>( +// `/api/method/frappe.desk.search.search_link?${params.toString()}` +// ); +// setSearchResults(response || []); +// } catch (error) { +// console.error(`Error fetching ${doctype} links:`, error); +// } +// }; + +// // Fetch default options when dropdown opens +// useEffect(() => { +// if (isDropdownOpen) searchLink(''); +// }, [isDropdownOpen]); + +// // Close dropdown when clicking outside +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent) => { +// if (containerRef.current && !containerRef.current.contains(event.target as Node)) { +// setDropdownOpen(false); +// } +// }; +// document.addEventListener('mousedown', handleClickOutside); +// return () => document.removeEventListener('mousedown', handleClickOutside); +// }, []); + +// return ( +//
+// + +// !disabled && setDropdownOpen(true)} +// onChange={(e) => { +// const text = e.target.value; +// setSearchText(text); +// searchLink(text); +// onChange(text); +// }} +// /> + +// {isDropdownOpen && searchResults.length > 0 && !disabled && ( +//
    +// {searchResults.map((item, idx) => ( +//
  • { +// onChange(item.value); +// setDropdownOpen(false); +// }} +// className={`px-3 py-2 cursor-pointer +// text-gray-900 dark:text-gray-100 +// hover:bg-blue-500 dark:hover:bg-blue-600 +// ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`} +// > +// {item.value} +// {item.description && ( +// {item.description} +// )} +//
  • +// ))} +//
+// )} +//
+// ); +// }; + +// export default LinkField; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f492784..705e2f1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,23 +2,23 @@ import React, { useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useTheme } from '../contexts/ThemeContext'; import { - FaTools, - FaBox, - FaWrench, - FaCog, - FaUsers, - FaChartBar, - FaBuilding, - FaTruck, - FaFileContract, - FaInfoCircle, - FaBars, - FaTimes, - FaHome, - FaMoon, - FaSun, - FaSignOutAlt -} from 'react-icons/fa'; + LayoutDashboard, + Package, + Wrench, + Users, + BarChart3, + Building2, + Truck, + FileText, + MapPin, + Menu, + X, + Moon, + Sun, + LogOut, + ClipboardList, + Calendar +} from 'lucide-react'; interface SidebarLink { id: string; @@ -75,80 +75,87 @@ const Sidebar: React.FC = ({ userEmail }) => { { id: 'dashboard', title: 'Dashboard', - icon: , + icon: , path: '/dashboard', visible: true }, { id: 'assets', title: 'Assets', - icon: , + icon: , path: '/assets', visible: showAsset }, { id: 'work-orders', title: 'Work Orders', - icon: , + icon: , path: '/work-orders', visible: showGeneralWO }, { - id: 'ppm', - title: 'PPM', - icon: , - path: '/ppm', + id: 'maintenance', + title: 'Asset Maintenance', + icon: , + path: '/maintenance', visible: showPreventiveMaintenance }, { - id: 'inventory', - title: 'Inventory', - icon: , - path: '/inventory', - visible: showInventory + id: 'ppm', + title: 'PPM', + icon: , + path: '/ppm', + visible: showPreventiveMaintenance }, - { - id: 'vendors', - title: 'Vendors', - icon: , - path: '/vendors', - visible: showSupplierDashboard - }, - { - id: 'dashboard-view', - title: 'Dashboard', - icon: , - path: '/dashboard-view', - visible: showProjectDashboard - }, - { - id: 'sites', - title: 'Sites', - icon: , - path: '/sites', - visible: showSiteDashboards - }, - { - id: 'active-map', - title: 'Active Map', - icon: , - path: '/active-map', - visible: showSiteInfo - }, - { - id: 'users', - title: 'Users', - icon: , - path: '/users', - visible: showAMTeam - }, - { - id: 'account', - title: 'Account', - icon: , - path: '/account', - visible: showSLA - } + // { + // id: 'inventory', + // title: 'Inventory', + // icon: , + // path: '/inventory', + // visible: showInventory + // }, + // { + // id: 'vendors', + // title: 'Vendors', + // icon: , + // path: '/vendors', + // visible: showSupplierDashboard + // }, + // { + // id: 'dashboard-view', + // title: 'Dashboard', + // icon: , + // path: '/dashboard-view', + // visible: showProjectDashboard + // }, + // { + // id: 'sites', + // title: 'Sites', + // icon: , + // path: '/sites', + // visible: showSiteDashboards + // }, + // { + // id: 'active-map', + // title: 'Active Map', + // icon: , + // path: '/active-map', + // visible: showSiteInfo + // }, + // { + // id: 'users', + // title: 'Users', + // icon: , + // path: '/users', + // visible: showAMTeam + // }, + // { + // id: 'account', + // title: 'Account', + // icon: , + // path: '/account', + // visible: showSLA + // } ]; const visibleLinks = links.filter(link => link.visible); @@ -175,18 +182,51 @@ const Sidebar: React.FC = ({ userEmail }) => { {/* Sidebar Header */}
{!isCollapsed && ( -
-
- AL +
+
+ {/* Seera Arabia Logo */} + Seera Arabia { + // Fallback to SVG if image not found + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + + + + +
-

Asset Lite

+

Seera Arabia

+
+ )} + {isCollapsed && ( +
+ Seera Arabia { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + + + + +
)}
@@ -211,7 +251,7 @@ const Sidebar: React.FC = ({ userEmail }) => { `} title={isCollapsed ? link.title : ''} > - {link.icon} + {link.icon} {!isCollapsed && ( {link.title} )} @@ -227,7 +267,7 @@ const Sidebar: React.FC = ({ userEmail }) => { className="w-full flex items-center justify-center px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors text-gray-700 dark:text-gray-300" title={isCollapsed ? (theme === 'light' ? 'Dark Mode' : 'Light Mode') : ''} > - {theme === 'light' ? : } + {theme === 'light' ? : } {!isCollapsed && ( {theme === 'light' ? 'Dark Mode' : 'Light Mode'} @@ -241,7 +281,7 @@ const Sidebar: React.FC = ({ userEmail }) => { className="w-full flex items-center justify-center px-4 py-2 rounded-lg bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400" title={isCollapsed ? 'Logout' : ''} > - + {!isCollapsed && ( Logout )} @@ -260,7 +300,7 @@ const Sidebar: React.FC = ({ userEmail }) => { {!isCollapsed && (
- Asset Lite v1.0 + Seera Arabia AMS v1.0
)}
diff --git a/src/config/api.ts b/src/config/api.ts index 2717047..afc9602 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -40,6 +40,35 @@ const API_CONFIG: ApiConfig = { GET_ASSET_STATS: '/api/method/asset_lite.api.asset_api.get_asset_stats', SEARCH_ASSETS: '/api/method/asset_lite.api.asset_api.search_assets', + // Work Order Management + GET_WORK_ORDERS: '/api/method/asset_lite.api.work_order_api.get_work_orders', + GET_WORK_ORDER_DETAILS: '/api/method/asset_lite.api.work_order_api.get_work_order_details', + CREATE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.create_work_order', + UPDATE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.update_work_order', + DELETE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.delete_work_order', + UPDATE_WORK_ORDER_STATUS: '/api/method/asset_lite.api.work_order_api.update_work_order_status', + + // Asset Maintenance Management + GET_ASSET_MAINTENANCE_LOGS: '/api/method/asset_lite.api.asset_maintenance_api.get_asset_maintenance_logs', + GET_ASSET_MAINTENANCE_LOG_DETAILS: '/api/method/asset_lite.api.asset_maintenance_api.get_asset_maintenance_log_details', + CREATE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.create_asset_maintenance_log', + UPDATE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.update_asset_maintenance_log', + DELETE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.delete_asset_maintenance_log', + UPDATE_MAINTENANCE_STATUS: '/api/method/asset_lite.api.asset_maintenance_api.update_maintenance_status', + GET_MAINTENANCE_LOGS_BY_ASSET: '/api/method/asset_lite.api.asset_maintenance_api.get_maintenance_logs_by_asset', + GET_OVERDUE_MAINTENANCE_LOGS: '/api/method/asset_lite.api.asset_maintenance_api.get_overdue_maintenance_logs', + + // PPM (Asset Maintenance) Management + GET_ASSET_MAINTENANCES: '/api/method/asset_lite.api.ppm_api.get_asset_maintenances', + GET_ASSET_MAINTENANCE_DETAILS: '/api/method/asset_lite.api.ppm_api.get_asset_maintenance_details', + CREATE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.create_asset_maintenance', + UPDATE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.update_asset_maintenance', + DELETE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.delete_asset_maintenance', + GET_MAINTENANCE_TASKS: '/api/method/asset_lite.api.ppm_api.get_maintenance_tasks', + GET_SERVICE_COVERAGE: '/api/method/asset_lite.api.ppm_api.get_service_coverage', + GET_MAINTENANCES_BY_ASSET: '/api/method/asset_lite.api.ppm_api.get_maintenances_by_asset', + GET_ACTIVE_SERVICE_CONTRACTS: '/api/method/asset_lite.api.ppm_api.get_active_service_contracts', + // Authentication LOGIN: '/api/method/login', LOGOUT: '/api/method/logout', diff --git a/src/hooks/useAssetMaintenance.ts b/src/hooks/useAssetMaintenance.ts new file mode 100644 index 0000000..2741139 --- /dev/null +++ b/src/hooks/useAssetMaintenance.ts @@ -0,0 +1,288 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import assetMaintenanceService from '../services/assetMaintenanceService'; +import type { AssetMaintenanceLog, MaintenanceFilters, CreateMaintenanceData } from '../services/assetMaintenanceService'; + +/** + * Hook to fetch list of asset maintenance logs with filters and pagination + */ +export function useAssetMaintenanceLogs( + filters?: MaintenanceFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + const [logs, setLogs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refetchTrigger, setRefetchTrigger] = useState(0); + const hasAttemptedRef = useRef(false); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchLogs = async () => { + try { + setLoading(true); + + const response = await assetMaintenanceService.getMaintenanceLogs(filters, undefined, limit, offset, orderBy); + + if (!isCancelled) { + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + setHasMore(response.has_more); + setError(null); + } + } catch (err) { + if (!isCancelled) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch maintenance logs'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy asset_maintenance_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setLogs([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchLogs(); + + return () => { + isCancelled = true; + }; + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + setRefetchTrigger(prev => prev + 1); + }, []); + + return { logs, totalCount, hasMore, loading, error, refetch }; +} + +/** + * Hook to fetch a single maintenance log by name + */ +export function useMaintenanceLogDetails(logName: string | null) { + const [log, setLog] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchLog = useCallback(async () => { + if (!logName) { + setLog(null); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const data = await assetMaintenanceService.getMaintenanceLogDetails(logName); + setLog(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance log details'); + } finally { + setLoading(false); + } + }, [logName]); + + useEffect(() => { + fetchLog(); + }, [fetchLog]); + + const refetch = useCallback(() => { + fetchLog(); + }, [fetchLog]); + + return { log, loading, error, refetch }; +} + +/** + * Hook to manage maintenance log operations + */ +export function useMaintenanceMutations() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createLog = async (logData: CreateMaintenanceData) => { + try { + setLoading(true); + setError(null); + + console.log('[useMaintenanceMutations] Creating maintenance log:', logData); + const response = await assetMaintenanceService.createMaintenanceLog(logData); + console.log('[useMaintenanceMutations] Create response:', response); + + if (response.success) { + return response.asset_maintenance_log; + } else { + const backendError = (response as any).error || 'Failed to create maintenance log'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useMaintenanceMutations] Create error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to create maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateLog = async (logName: string, logData: Partial) => { + try { + setLoading(true); + setError(null); + + console.log('[useMaintenanceMutations] Updating maintenance log:', logName, logData); + const response = await assetMaintenanceService.updateMaintenanceLog(logName, logData); + console.log('[useMaintenanceMutations] Update response:', response); + + if (response.success) { + return response.asset_maintenance_log; + } else { + const backendError = (response as any).error || 'Failed to update maintenance log'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useMaintenanceMutations] Update error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to update maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const deleteLog = async (logName: string) => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.deleteMaintenanceLog(logName); + + if (!response.success) { + throw new Error('Failed to delete maintenance log'); + } + + return response; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateStatus = async (logName: string, maintenanceStatus?: string, workflowState?: string) => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.updateMaintenanceStatus(logName, maintenanceStatus, workflowState); + + if (response.success) { + return response.asset_maintenance_log; + } else { + throw new Error('Failed to update maintenance status'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update status'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { createLog, updateLog, deleteLog, updateStatus, loading, error }; +} + +/** + * Hook to fetch maintenance logs for a specific asset + */ +export function useAssetMaintenanceHistory(assetName: string | null) { + const [logs, setLogs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchHistory = useCallback(async () => { + if (!assetName) { + setLogs([]); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.getMaintenanceLogsByAsset(assetName); + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance history'); + } finally { + setLoading(false); + } + }, [assetName]); + + useEffect(() => { + fetchHistory(); + }, [fetchHistory]); + + return { logs, totalCount, loading, error, refetch: fetchHistory }; +} + +/** + * Hook to fetch overdue maintenance logs + */ +export function useOverdueMaintenanceLogs() { + const [logs, setLogs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchOverdue = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.getOverdueMaintenanceLogs(); + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch overdue maintenance'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchOverdue(); + }, [fetchOverdue]); + + return { logs, totalCount, loading, error, refetch: fetchOverdue }; +} + diff --git a/src/hooks/usePPM.ts b/src/hooks/usePPM.ts new file mode 100644 index 0000000..dca0d66 --- /dev/null +++ b/src/hooks/usePPM.ts @@ -0,0 +1,174 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import ppmService from '../services/ppmService'; +import type { AssetMaintenance, PPMFilters, CreatePPMData } from '../services/ppmService'; + +/** + * Hook to fetch list of asset maintenances (PPM schedules) with filters and pagination + */ +export function usePPMs( + filters?: PPMFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + const [ppms, setPPMs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refetchTrigger, setRefetchTrigger] = useState(0); + const hasAttemptedRef = useRef(false); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchPPMs = async () => { + try { + setLoading(true); + + const response = await ppmService.getAssetMaintenances(filters, undefined, limit, offset, orderBy); + + if (!isCancelled) { + setPPMs(response.asset_maintenances); + setTotalCount(response.total_count); + setHasMore(response.has_more); + setError(null); + } + } catch (err) { + if (!isCancelled) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch PPM schedules'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy ppm_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setPPMs([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchPPMs(); + + return () => { + isCancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + setRefetchTrigger(prev => prev + 1); + }, []); + + return { ppms, totalCount, hasMore, loading, error, refetch }; +} + +/** + * Hook to fetch a single PPM schedule by name + */ +export function usePPMDetails(ppmName: string | null) { + const [ppm, setPPM] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchPPM = useCallback(async () => { + if (!ppmName) { + setPPM(null); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const data = await ppmService.getAssetMaintenanceDetails(ppmName); + setPPM(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch PPM details'); + } finally { + setLoading(false); + } + }, [ppmName]); + + useEffect(() => { + fetchPPM(); + }, [fetchPPM]); + + const refetch = useCallback(() => { + fetchPPM(); + }, [fetchPPM]); + + return { ppm, loading, error, refetch }; +} + +/** + * Hook to manage PPM operations (create, update, delete) + */ +export function usePPMMutations() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createPPM = useCallback(async (data: CreatePPMData) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.createAssetMaintenance(data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to create PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + const updatePPM = useCallback(async (ppmName: string, data: Partial) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.updateAssetMaintenance(ppmName, data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + const deletePPM = useCallback(async (ppmName: string) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.deleteAssetMaintenance(ppmName); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + return { createPPM, updatePPM, deletePPM, loading, error }; +} + diff --git a/src/hooks/useWorkOrder.ts b/src/hooks/useWorkOrder.ts new file mode 100644 index 0000000..5aaab75 --- /dev/null +++ b/src/hooks/useWorkOrder.ts @@ -0,0 +1,220 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import workOrderService from '../services/workOrderService'; +import type { WorkOrder, WorkOrderFilters, CreateWorkOrderData } from '../services/workOrderService'; + +/** + * Hook to fetch list of work orders with filters and pagination + */ +export function useWorkOrders( + filters?: WorkOrderFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + const [workOrders, setWorkOrders] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refetchTrigger, setRefetchTrigger] = useState(0); + const hasAttemptedRef = useRef(false); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchWorkOrders = async () => { + try { + setLoading(true); + + const response = await workOrderService.getWorkOrders(filters, undefined, limit, offset, orderBy); + + if (!isCancelled) { + setWorkOrders(response.work_orders); + setTotalCount(response.total_count); + setHasMore(response.has_more); + setError(null); + } + } catch (err) { + if (!isCancelled) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch work orders'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy work_order_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setWorkOrders([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchWorkOrders(); + + return () => { + isCancelled = true; + }; + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + setRefetchTrigger(prev => prev + 1); + }, []); + + return { workOrders, totalCount, hasMore, loading, error, refetch }; +} + +/** + * Hook to fetch a single work order by name + */ +export function useWorkOrderDetails(workOrderName: string | null) { + const [workOrder, setWorkOrder] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchWorkOrder = useCallback(async () => { + if (!workOrderName) { + setWorkOrder(null); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const data = await workOrderService.getWorkOrderDetails(workOrderName); + setWorkOrder(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch work order details'); + } finally { + setLoading(false); + } + }, [workOrderName]); + + useEffect(() => { + fetchWorkOrder(); + }, [fetchWorkOrder]); + + const refetch = useCallback(() => { + fetchWorkOrder(); + }, [fetchWorkOrder]); + + return { workOrder, loading, error, refetch }; +} + +/** + * Hook to manage work order operations (create, update, delete) + */ +export function useWorkOrderMutations() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createWorkOrder = async (workOrderData: CreateWorkOrderData) => { + try { + setLoading(true); + setError(null); + + console.log('[useWorkOrderMutations] Creating work order with data:', workOrderData); + const response = await workOrderService.createWorkOrder(workOrderData); + console.log('[useWorkOrderMutations] Create work order response:', response); + + if (response.success) { + return response.work_order; + } else { + const backendError = (response as any).error || 'Failed to create work order'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useWorkOrderMutations] Create work order error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to create work order'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateWorkOrder = async (workOrderName: string, workOrderData: Partial) => { + try { + setLoading(true); + setError(null); + + console.log('[useWorkOrderMutations] Updating work order:', workOrderName, 'with data:', workOrderData); + const response = await workOrderService.updateWorkOrder(workOrderName, workOrderData); + console.log('[useWorkOrderMutations] Update work order response:', response); + + if (response.success) { + return response.work_order; + } else { + const backendError = (response as any).error || 'Failed to update work order'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useWorkOrderMutations] Update work order error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to update work order'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const deleteWorkOrder = async (workOrderName: string) => { + try { + setLoading(true); + setError(null); + + const response = await workOrderService.deleteWorkOrder(workOrderName); + + if (!response.success) { + throw new Error('Failed to delete work order'); + } + + return response; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete work order'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateStatus = async (workOrderName: string, repairStatus?: string, workflowState?: string) => { + try { + setLoading(true); + setError(null); + + const response = await workOrderService.updateWorkOrderStatus(workOrderName, repairStatus, workflowState); + + if (response.success) { + return response.work_order; + } else { + throw new Error('Failed to update work order status'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update status'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { createWorkOrder, updateWorkOrder, deleteWorkOrder, updateStatus, loading, error }; +} + diff --git a/src/index.css b/src/index.css index 0d70891..3cb6b70 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/src/pages/AssetDetail.tsx b/src/pages/AssetDetail.tsx index b09ff44..ff5ed7b 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -4,6 +4,9 @@ import { useAssetDetails, useAssetMutations } from '../hooks/useAsset'; import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa'; import type { CreateAssetData } from '../services/assetService'; +import LinkField from '../components/LinkField'; +import apiService from '../services/apiService'; // ✅ your ApiService + // Helper function to get the base URL for files const getFileBaseUrl = () => { // Always use the full URL to avoid proxy path duplication issues @@ -24,8 +27,12 @@ const AssetDetail: React.FC = () => { isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null) ); const { createAsset, updateAsset, loading: saving } = useAssetMutations(); - + const [isEditing, setIsEditing] = useState(isNewAsset); + + const [userSiteName, setUserSiteName] = useState(''); + const [departmentFilters, setDepartmentFilters] = useState>({}); + const [formData, setFormData] = useState({ asset_name: '', company: '', @@ -44,9 +51,53 @@ const AssetDetail: React.FC = () => { custom_modality: '', custom_attach_image: '', custom_site_contractor: '', - custom_total_amount: 0 + custom_total_amount: 0, + calculate_depreciation: false, + available_for_use_date: isNewAsset ? new Date().toISOString().split('T')[0] : undefined }); + // Load user details on mount + useEffect(() => { + async function loadUserDetails() { + try { + const user = await apiService.getUserDetails(); + setUserSiteName(user.custom_site_name || ''); + } catch (err) { + console.error('Error loading user details', err); + } + } + + loadUserDetails(); + }, []); + + // Update department filters when company or userSiteName changes + useEffect(() => { + const filters: Record = {}; + + // Base filter: company must match + if (formData.company) { + filters['company'] = formData.company; + } + + // Apply department name filters based on site name and company + const isMobileSite = + (userSiteName && userSiteName.startsWith('Mobile')) || + (formData.company && formData.company.startsWith('Mobile')); + + if (isMobileSite) { + // For Mobile sites, exclude Non Bio departments (show Bio departments) + // Frappe filter format: ['not like', 'pattern'] + filters['department_name'] = ['not like', 'Non Bio%']; + } else if (userSiteName || formData.company) { + // For non-Mobile sites, exclude Bio departments (show Non-Bio departments) + filters['department_name'] = ['not like', 'Bio%']; + } + + console.log('Department filters updated:', filters); // Debug log + + setDepartmentFilters(filters); + }, [formData.company, userSiteName]); + // Load asset data for editing or duplicating useEffect(() => { if (asset) { @@ -71,11 +122,52 @@ const AssetDetail: React.FC = () => { custom_modality: asset.custom_modality || '', custom_attach_image: asset.custom_attach_image || '', custom_site_contractor: asset.custom_site_contractor || '', - custom_total_amount: asset.custom_total_amount || 0 + custom_total_amount: asset.custom_total_amount || 0, + gross_purchase_amount:asset.gross_purchase_amount || 0, + available_for_use_date: asset.available_for_use_date || '', + calculate_depreciation: asset.calculate_depreciation || false }); } }, [asset, isDuplicating]); + const [qrCodeUrl, setQrCodeUrl] = useState(null); + + useEffect(() => { + if (!assetName || assetName === "new") return; + + const fetchQRCode = async () => { + try { + // Try fixed predictable URL + const directUrl = `/files/${assetName}-qr.png`; + console.log(directUrl) + + // Quickly test if file exists + const response = await fetch(directUrl, { method: "HEAD" }); + console.log(response) + + if (response.ok) { + setQrCodeUrl(directUrl); + return; + } + + // If not available, fallback to File doctype API + const fileRes = await apiService.apiCall( + `/api/resource/File?filters=[["File","attached_to_name","=","${assetName}"]]` + ); + + if (fileRes?.data?.length > 0) { + setQrCodeUrl(fileRes.data[0].file_url); + } + + } catch (error) { + console.error("Error loading QR code:", error); + } + }; + + fetchQRCode(); + }, [assetName, asset]); + + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ @@ -104,6 +196,10 @@ const AssetDetail: React.FC = () => { try { if (isNewAsset || isDuplicating) { const newAsset = await createAsset(formData); + if (newAsset.name) { + const qrUrl = `/files/${newAsset.name}-qr.png`; + setQrCodeUrl(qrUrl); + } const successMessage = isDuplicating ? 'Asset duplicated successfully!' : 'Asset created successfully!'; @@ -273,7 +369,7 @@ const AssetDetail: React.FC = () => { />
-
+ {/*
@@ -291,9 +387,18 @@ const AssetDetail: React.FC = () => { -
+
*/} -
+ setFormData({ ...formData, custom_asset_type: val })} + disabled={!isEditing} + /> + + + {/*
@@ -311,7 +416,15 @@ const AssetDetail: React.FC = () => { -
+
*/} + setFormData({ ...formData, custom_modality: val })} + disabled={!isEditing} + /> +
-
+ {/*
@@ -406,7 +519,15 @@ const AssetDetail: React.FC = () => { disabled={!isEditing} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" /> -
+
*/} + + setFormData({ ...formData, custom_manufacturer: val })} + disabled={!isEditing} + />
@@ -839,12 +1043,12 @@ const AssetDetail: React.FC = () => { {/* QR Code */}
-
- {asset?.name ? ( +
+ {qrCodeUrl ? ( <> {`QR { // Hide image and show fallback icon if QR code doesn't exist @@ -878,6 +1082,7 @@ const AssetDetail: React.FC = () => { )}
+