Compare commits
No commits in common. "e5ba9ebd0f2a4617ed059913586fa3eb9719b5c3" and "7747e144d810e50edbf806f0432b0340d3da03fe" have entirely different histories.
e5ba9ebd0f
...
7747e144d8
@ -1,220 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/seera-logo.png" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frappe-frontend</title>
|
<title>frappe-frontend</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
72
src/App.tsx
72
src/App.tsx
@ -7,12 +7,6 @@ import UsersList from './pages/UsersList';
|
|||||||
import EventsList from './pages/EventsList';
|
import EventsList from './pages/EventsList';
|
||||||
import AssetList from './pages/AssetList';
|
import AssetList from './pages/AssetList';
|
||||||
import AssetDetail from './pages/AssetDetail';
|
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';
|
import Sidebar from './components/Sidebar';
|
||||||
|
|
||||||
// Layout with Sidebar
|
// Layout with Sidebar
|
||||||
@ -77,72 +71,6 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/work-orders"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<WorkOrderList />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/work-orders/:workOrderName"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<WorkOrderDetail />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/maintenance"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<AssetMaintenanceList />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/maintenance/:logName"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<AssetMaintenanceDetail />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/ppm"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<PPMList />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/ppm/:ppmName"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<LayoutWithSidebar>
|
|
||||||
<PPMDetail />
|
|
||||||
</LayoutWithSidebar>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/old-dashboard"
|
path="/old-dashboard"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -1,201 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkField: React.FC<LinkFieldProps> = ({
|
|
||||||
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<HTMLDivElement>(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 (
|
|
||||||
<div ref={containerRef} className="relative w-full mb-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
placeholder={placeholder || `Select ${label}`}
|
|
||||||
disabled={disabled}
|
|
||||||
onFocus={() => !disabled && setDropdownOpen(true)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const text = e.target.value;
|
|
||||||
setSearchText(text);
|
|
||||||
searchLink(text);
|
|
||||||
onChange(text);
|
|
||||||
}}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
||||||
disabled:bg-gray-100 dark:disabled:bg-gray-700
|
|
||||||
border-gray-300 dark:border-gray-600
|
|
||||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-white`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isDropdownOpen && searchResults.length > 0 && (
|
|
||||||
<ul
|
|
||||||
className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
|
||||||
rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg"
|
|
||||||
>
|
|
||||||
{searchResults.map((item, idx) => (
|
|
||||||
<li
|
|
||||||
key={idx}
|
|
||||||
onClick={() => {
|
|
||||||
onChange(item.value);
|
|
||||||
setDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
className={`px-3 py-2 hover:bg-blue-100 dark:hover:bg-gray-700 cursor-pointer
|
|
||||||
${value === item.value ? 'bg-blue-50 dark:bg-gray-600 font-semibold' : ''}`}
|
|
||||||
>
|
|
||||||
<div>{item.value}</div>
|
|
||||||
{item.description && (
|
|
||||||
<div className="text-gray-500 dark:text-gray-400 text-xs">{item.description}</div>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LinkField;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
// import apiService from '../services/apiService';// ✅ uses your existing ApiService
|
|
||||||
|
|
||||||
// interface LinkFieldProps {
|
|
||||||
// label: string;
|
|
||||||
// doctype: string;
|
|
||||||
// value: string;
|
|
||||||
// onChange: (value: string) => void;
|
|
||||||
// placeholder?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const LinkField: React.FC<LinkFieldProps> = ({ label, doctype, value, onChange, placeholder }) => {
|
|
||||||
// const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]);
|
|
||||||
// const [searchText, setSearchText] = useState('');
|
|
||||||
// const [isDropdownOpen, setDropdownOpen] = useState(false);
|
|
||||||
// const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// // Function to call ERPNext link search API
|
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Load default results 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 (
|
|
||||||
// <div ref={containerRef} className="relative w-full mb-4">
|
|
||||||
// {/* <label className="block text-gray-700 text-sm font-medium mb-1">{label}</label> */}
|
|
||||||
// <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// value={value}
|
|
||||||
// placeholder={placeholder || `Select ${label}`}
|
|
||||||
// className="border border-gray-300 rounded-md p-2 w-full focus:ring focus:ring-blue-200"
|
|
||||||
// onFocus={() => setDropdownOpen(true)}
|
|
||||||
// onChange={(e) => {
|
|
||||||
// const text = e.target.value;
|
|
||||||
// setSearchText(text);
|
|
||||||
// searchLink(text);
|
|
||||||
// onChange(text);
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// {isDropdownOpen && searchResults.length > 0 && (
|
|
||||||
// <ul className="absolute z-50 bg-white border border-gray-300 rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg">
|
|
||||||
// {searchResults.map((item, idx) => (
|
|
||||||
// <li
|
|
||||||
// key={idx}
|
|
||||||
// onClick={() => {
|
|
||||||
// onChange(item.value);
|
|
||||||
// setDropdownOpen(false);
|
|
||||||
// }}
|
|
||||||
// className={`p-2 hover:bg-blue-100 cursor-pointer ${
|
|
||||||
// value === item.value ? 'bg-blue-50 font-semibold' : ''
|
|
||||||
// }`}
|
|
||||||
// >
|
|
||||||
// {item.value}
|
|
||||||
// {item.description && <span className="text-gray-500 text-xs ml-2">{item.description}</span>}
|
|
||||||
// </li>
|
|
||||||
// ))}
|
|
||||||
// </ul>
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default LinkField;
|
|
||||||
@ -2,23 +2,23 @@ import React, { useState } from 'react';
|
|||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
FaTools,
|
||||||
Package,
|
FaBox,
|
||||||
Wrench,
|
FaWrench,
|
||||||
Users,
|
FaCog,
|
||||||
BarChart3,
|
FaUsers,
|
||||||
Building2,
|
FaChartBar,
|
||||||
Truck,
|
FaBuilding,
|
||||||
FileText,
|
FaTruck,
|
||||||
MapPin,
|
FaFileContract,
|
||||||
Menu,
|
FaInfoCircle,
|
||||||
X,
|
FaBars,
|
||||||
Moon,
|
FaTimes,
|
||||||
Sun,
|
FaHome,
|
||||||
LogOut,
|
FaMoon,
|
||||||
ClipboardList,
|
FaSun,
|
||||||
Calendar
|
FaSignOutAlt
|
||||||
} from 'lucide-react';
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
interface SidebarLink {
|
interface SidebarLink {
|
||||||
id: string;
|
id: string;
|
||||||
@ -75,87 +75,80 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
|
|||||||
{
|
{
|
||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
icon: <LayoutDashboard size={20} />,
|
icon: <FaHome />,
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
visible: true
|
visible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'assets',
|
id: 'assets',
|
||||||
title: 'Assets',
|
title: 'Assets',
|
||||||
icon: <Package size={20} />,
|
icon: <FaTools />,
|
||||||
path: '/assets',
|
path: '/assets',
|
||||||
visible: showAsset
|
visible: showAsset
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'work-orders',
|
id: 'work-orders',
|
||||||
title: 'Work Orders',
|
title: 'Work Orders',
|
||||||
icon: <ClipboardList size={20} />,
|
icon: <FaCog />,
|
||||||
path: '/work-orders',
|
path: '/work-orders',
|
||||||
visible: showGeneralWO
|
visible: showGeneralWO
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'maintenance',
|
|
||||||
title: 'Asset Maintenance',
|
|
||||||
icon: <Wrench size={20} />,
|
|
||||||
path: '/maintenance',
|
|
||||||
visible: showPreventiveMaintenance
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'ppm',
|
id: 'ppm',
|
||||||
title: 'PPM',
|
title: 'PPM',
|
||||||
icon: <Calendar size={20} />,
|
icon: <FaWrench />,
|
||||||
path: '/ppm',
|
path: '/ppm',
|
||||||
visible: showPreventiveMaintenance
|
visible: showPreventiveMaintenance
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// id: 'inventory',
|
id: 'inventory',
|
||||||
// title: 'Inventory',
|
title: 'Inventory',
|
||||||
// icon: <Package size={20} />,
|
icon: <FaBox />,
|
||||||
// path: '/inventory',
|
path: '/inventory',
|
||||||
// visible: showInventory
|
visible: showInventory
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'vendors',
|
id: 'vendors',
|
||||||
// title: 'Vendors',
|
title: 'Vendors',
|
||||||
// icon: <Truck size={20} />,
|
icon: <FaTruck />,
|
||||||
// path: '/vendors',
|
path: '/vendors',
|
||||||
// visible: showSupplierDashboard
|
visible: showSupplierDashboard
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'dashboard-view',
|
id: 'dashboard-view',
|
||||||
// title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
// icon: <BarChart3 size={20} />,
|
icon: <FaChartBar />,
|
||||||
// path: '/dashboard-view',
|
path: '/dashboard-view',
|
||||||
// visible: showProjectDashboard
|
visible: showProjectDashboard
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'sites',
|
id: 'sites',
|
||||||
// title: 'Sites',
|
title: 'Sites',
|
||||||
// icon: <Building2 size={20} />,
|
icon: <FaBuilding />,
|
||||||
// path: '/sites',
|
path: '/sites',
|
||||||
// visible: showSiteDashboards
|
visible: showSiteDashboards
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'active-map',
|
id: 'active-map',
|
||||||
// title: 'Active Map',
|
title: 'Active Map',
|
||||||
// icon: <MapPin size={20} />,
|
icon: <FaInfoCircle />,
|
||||||
// path: '/active-map',
|
path: '/active-map',
|
||||||
// visible: showSiteInfo
|
visible: showSiteInfo
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'users',
|
id: 'users',
|
||||||
// title: 'Users',
|
title: 'Users',
|
||||||
// icon: <Users size={20} />,
|
icon: <FaUsers />,
|
||||||
// path: '/users',
|
path: '/users',
|
||||||
// visible: showAMTeam
|
visible: showAMTeam
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'account',
|
id: 'account',
|
||||||
// title: 'Account',
|
title: 'Account',
|
||||||
// icon: <FileText size={20} />,
|
icon: <FaFileContract />,
|
||||||
// path: '/account',
|
path: '/account',
|
||||||
// visible: showSLA
|
visible: showSLA
|
||||||
// }
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const visibleLinks = links.filter(link => link.visible);
|
const visibleLinks = links.filter(link => link.visible);
|
||||||
@ -182,51 +175,18 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
|
|||||||
{/* Sidebar Header */}
|
{/* Sidebar Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-10 h-10 flex items-center justify-center bg-white dark:bg-gray-700 rounded-lg p-1">
|
<div className="w-8 h-8 bg-gradient-to-br from-indigo-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||||
{/* Seera Arabia Logo */}
|
<span className="text-white font-bold text-sm">AL</span>
|
||||||
<img
|
|
||||||
src="/seera-logo.png"
|
|
||||||
alt="Seera Arabia"
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
onError={(e) => {
|
|
||||||
// Fallback to SVG if image not found
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<svg className="w-6 h-6 hidden" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="#6366F1" fillOpacity="0.9"/>
|
|
||||||
<path d="M2 17L12 22L22 17V12L12 17L2 12V17Z" fill="#8B5CF6" fillOpacity="0.7"/>
|
|
||||||
<path d="M12 12V17" stroke="#A855F7" strokeWidth="2" strokeLinecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-gray-900 dark:text-white text-lg font-semibold">Seera Arabia</h1>
|
<h1 className="text-gray-900 dark:text-white text-xl font-bold">Asset Lite</h1>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isCollapsed && (
|
|
||||||
<div className="w-8 h-8 flex items-center justify-center bg-white dark:bg-gray-700 rounded-lg p-1">
|
|
||||||
<img
|
|
||||||
src="/seera-logo.png"
|
|
||||||
alt="Seera Arabia"
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<svg className="w-5 h-5 hidden" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="#6366F1" fillOpacity="0.9"/>
|
|
||||||
<path d="M2 17L12 22L22 17V12L12 17L2 12V17Z" fill="#8B5CF6" fillOpacity="0.7"/>
|
|
||||||
<path d="M12 12V17" stroke="#A855F7" strokeWidth="2" strokeLinecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
className="text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-colors"
|
className="text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{isCollapsed ? <Menu size={20} /> : <X size={20} />}
|
{isCollapsed ? <FaBars size={20} /> : <FaTimes size={20} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -251,7 +211,7 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
|
|||||||
`}
|
`}
|
||||||
title={isCollapsed ? link.title : ''}
|
title={isCollapsed ? link.title : ''}
|
||||||
>
|
>
|
||||||
<span>{link.icon}</span>
|
<span className="text-xl">{link.icon}</span>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<span className="ml-4 font-medium">{link.title}</span>
|
<span className="ml-4 font-medium">{link.title}</span>
|
||||||
)}
|
)}
|
||||||
@ -267,7 +227,7 @@ const Sidebar: React.FC<SidebarProps> = ({ 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"
|
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') : ''}
|
title={isCollapsed ? (theme === 'light' ? 'Dark Mode' : 'Light Mode') : ''}
|
||||||
>
|
>
|
||||||
{theme === 'light' ? <Moon size={18} /> : <Sun size={18} />}
|
{theme === 'light' ? <FaMoon size={16} /> : <FaSun size={16} />}
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<span className="ml-2 text-sm font-medium">
|
<span className="ml-2 text-sm font-medium">
|
||||||
{theme === 'light' ? 'Dark Mode' : 'Light Mode'}
|
{theme === 'light' ? 'Dark Mode' : 'Light Mode'}
|
||||||
@ -281,7 +241,7 @@ const Sidebar: React.FC<SidebarProps> = ({ 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"
|
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' : ''}
|
title={isCollapsed ? 'Logout' : ''}
|
||||||
>
|
>
|
||||||
<LogOut size={18} />
|
<FaSignOutAlt size={16} />
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<span className="ml-2 text-sm font-medium">Logout</span>
|
<span className="ml-2 text-sm font-medium">Logout</span>
|
||||||
)}
|
)}
|
||||||
@ -300,7 +260,7 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
|
|||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500 text-center">
|
<div className="text-xs text-gray-400 dark:text-gray-500 text-center">
|
||||||
Seera Arabia AMS v1.0
|
Asset Lite v1.0
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -40,35 +40,6 @@ const API_CONFIG: ApiConfig = {
|
|||||||
GET_ASSET_STATS: '/api/method/asset_lite.api.asset_api.get_asset_stats',
|
GET_ASSET_STATS: '/api/method/asset_lite.api.asset_api.get_asset_stats',
|
||||||
SEARCH_ASSETS: '/api/method/asset_lite.api.asset_api.search_assets',
|
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
|
// Authentication
|
||||||
LOGIN: '/api/method/login',
|
LOGIN: '/api/method/login',
|
||||||
LOGOUT: '/api/method/logout',
|
LOGOUT: '/api/method/logout',
|
||||||
|
|||||||
@ -1,288 +0,0 @@
|
|||||||
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<AssetMaintenanceLog[]>([]);
|
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
|
||||||
const [hasMore, setHasMore] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<AssetMaintenanceLog | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<string | null>(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<CreateMaintenanceData>) => {
|
|
||||||
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<AssetMaintenanceLog[]>([]);
|
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(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<AssetMaintenanceLog[]>([]);
|
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
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<AssetMaintenance[]>([]);
|
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
|
||||||
const [hasMore, setHasMore] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<AssetMaintenance | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<string | null>(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<CreatePPMData>) => {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
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<WorkOrder[]>([]);
|
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
|
||||||
const [hasMore, setHasMore] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<WorkOrder | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(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<string | null>(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<CreateWorkOrderData>) => {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
@ -4,9 +4,6 @@ import { useAssetDetails, useAssetMutations } from '../hooks/useAsset';
|
|||||||
import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa';
|
import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa';
|
||||||
import type { CreateAssetData } from '../services/assetService';
|
import type { CreateAssetData } from '../services/assetService';
|
||||||
|
|
||||||
import LinkField from '../components/LinkField';
|
|
||||||
|
|
||||||
|
|
||||||
const AssetDetail: React.FC = () => {
|
const AssetDetail: React.FC = () => {
|
||||||
const { assetName } = useParams<{ assetName: string }>();
|
const { assetName } = useParams<{ assetName: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -16,6 +13,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
const isNewAsset = assetName === 'new';
|
const isNewAsset = assetName === 'new';
|
||||||
const isDuplicating = isNewAsset && !!duplicateFromAsset;
|
const isDuplicating = isNewAsset && !!duplicateFromAsset;
|
||||||
|
|
||||||
|
// If duplicating, fetch the source asset
|
||||||
const { asset, loading, error } = useAssetDetails(
|
const { asset, loading, error } = useAssetDetails(
|
||||||
isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null)
|
isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null)
|
||||||
);
|
);
|
||||||
@ -43,12 +41,13 @@ const AssetDetail: React.FC = () => {
|
|||||||
custom_total_amount: 0
|
custom_total_amount: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load asset data for editing or duplicating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
setFormData({
|
setFormData({
|
||||||
asset_name: isDuplicating ? `${asset.asset_name} (Copy)` : (asset.asset_name || ''),
|
asset_name: isDuplicating ? `${asset.asset_name} (Copy)` : (asset.asset_name || ''),
|
||||||
company: asset.company || '',
|
company: asset.company || '',
|
||||||
custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''),
|
custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''), // Clear serial number for duplicates
|
||||||
location: asset.location || '',
|
location: asset.location || '',
|
||||||
custom_manufacturer: asset.custom_manufacturer || '',
|
custom_manufacturer: asset.custom_manufacturer || '',
|
||||||
department: asset.department || '',
|
department: asset.department || '',
|
||||||
@ -79,6 +78,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
if (!formData.asset_name) {
|
if (!formData.asset_name) {
|
||||||
alert('Please enter an Asset Name');
|
alert('Please enter an Asset Name');
|
||||||
return;
|
return;
|
||||||
@ -89,6 +89,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show console log for debugging
|
||||||
console.log('Submitting asset data:', formData);
|
console.log('Submitting asset data:', formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -106,8 +107,10 @@ const AssetDetail: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Asset save error:', err);
|
console.error('Asset save error:', err);
|
||||||
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
|
||||||
|
// Check if it's an API deployment issue
|
||||||
if (errorMessage.includes('404') || errorMessage.includes('not found') ||
|
if (errorMessage.includes('404') || errorMessage.includes('not found') ||
|
||||||
errorMessage.includes('has no attribute') || errorMessage.includes('417')) {
|
errorMessage.includes('has no attribute') || errorMessage.includes('417')) {
|
||||||
alert(
|
alert(
|
||||||
@ -154,6 +157,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error for duplicate if source asset not found
|
||||||
if (error && isDuplicating) {
|
if (error && isDuplicating) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||||
@ -162,7 +166,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
Source Asset Not Found
|
Source Asset Not Found
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-yellow-700 dark:text-yellow-400">
|
<p className="text-yellow-700 dark:text-yellow-400">
|
||||||
The asset you're trying to duplicate could not be found.
|
The asset you're trying to duplicate could not be found. It may have been deleted or you may not have permission to access it.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex gap-3">
|
<div className="mt-4 flex gap-3">
|
||||||
<button
|
<button
|
||||||
@ -193,7 +197,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<FaArrowLeft />
|
<FaArrowLeft />
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
<span className="text-gray-900 dark:text-white">
|
||||||
{isDuplicating ? 'Duplicate Asset' : (isNewAsset ? 'New Asset Details' : 'Asset Details')}
|
{isDuplicating ? 'Duplicate Asset' : (isNewAsset ? 'New Asset Details' : 'Asset Details')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -218,7 +222,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 dark:text-gray-800 px-6 py-2 rounded-lg"
|
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@ -260,7 +264,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Category <span className="text-red-500">*</span>
|
Category <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
@ -278,17 +282,9 @@ const AssetDetail: React.FC = () => {
|
|||||||
<option value="IT Equipment">IT Equipment</option>
|
<option value="IT Equipment">IT Equipment</option>
|
||||||
<option value="Furniture">Furniture</option>
|
<option value="Furniture">Furniture</option>
|
||||||
</select>
|
</select>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<LinkField
|
<div>
|
||||||
label="Category"
|
|
||||||
doctype="Asset Type"
|
|
||||||
value={formData.custom_asset_type || ''}
|
|
||||||
onChange={(val) => setFormData({ ...formData, custom_asset_type: val })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
{/* <div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Modality <span className="text-red-500">*</span>
|
Modality <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
@ -306,25 +302,18 @@ const AssetDetail: React.FC = () => {
|
|||||||
<option value="Ultrasound">Ultrasound</option>
|
<option value="Ultrasound">Ultrasound</option>
|
||||||
<option value="Other">Other</option>
|
<option value="Other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
</div> */}
|
</div>
|
||||||
<LinkField
|
|
||||||
label="Modality"
|
|
||||||
doctype="Modality"
|
|
||||||
value={formData.custom_modality || ''}
|
|
||||||
onChange={(val) => setFormData({ ...formData, custom_modality: val })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Class
|
Class <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
name="custom_class"
|
name="custom_class"
|
||||||
value={formData.custom_class}
|
value={formData.custom_class}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
>
|
>
|
||||||
<option value="">Select class</option>
|
<option value="">Select class</option>
|
||||||
<option value="Class A">Class A</option>
|
<option value="Class A">Class A</option>
|
||||||
@ -333,15 +322,15 @@ const AssetDetail: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="md:col-span-2">
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Asset ID
|
Asset ID <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={isNewAsset || isDuplicating ? 'Auto-generated' : asset?.name}
|
value={isNewAsset || isDuplicating ? 'Auto-generated' : asset?.name}
|
||||||
disabled
|
disabled
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
/>
|
/>
|
||||||
{isDuplicating && (
|
{isDuplicating && (
|
||||||
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
||||||
@ -352,15 +341,13 @@ const AssetDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* COLUMN 2: Technical Specs */}
|
{/* Technical Specs */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Technical Specs</h2>
|
||||||
Technical Specs
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Serial Code
|
Serial No.
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -369,35 +356,35 @@ const AssetDetail: React.FC = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="e.g. SN-12345"
|
placeholder="e.g. SN-12345"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
System ID
|
System ID
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. SYS-755"
|
placeholder="e.g. SYS-755"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Serial No.2
|
Serial No.2
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. SR-V021-A"
|
placeholder="e.g. SR-V021-A"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Manufacturer
|
Manufacturer
|
||||||
</label>
|
</label>
|
||||||
@ -410,17 +397,10 @@ const AssetDetail: React.FC = () => {
|
|||||||
disabled={!isEditing}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<LinkField
|
|
||||||
label="Manufacturer"
|
|
||||||
doctype="Manufacturer"
|
|
||||||
value={formData.manufacturer || ''}
|
|
||||||
onChange={(val) => setFormData({ ...formData, manufacturer: val })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Model
|
Model
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -430,19 +410,19 @@ const AssetDetail: React.FC = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Model number"
|
placeholder="Model number"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Model Number
|
Model Number
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Model number"
|
placeholder="Model number"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -452,7 +432,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Location</h2>
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Location</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* <div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Company
|
Company
|
||||||
</label>
|
</label>
|
||||||
@ -467,16 +447,9 @@ const AssetDetail: React.FC = () => {
|
|||||||
<option value="ABC Hospital">ABC Hospital</option>
|
<option value="ABC Hospital">ABC Hospital</option>
|
||||||
<option value="XYZ Clinic">XYZ Clinic</option>
|
<option value="XYZ Clinic">XYZ Clinic</option>
|
||||||
</select>
|
</select>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<LinkField
|
<div>
|
||||||
label="Hospital"
|
|
||||||
doctype="Company"
|
|
||||||
value={formData.company || ''}
|
|
||||||
onChange={(val) => setFormData({ ...formData, company: val })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* <div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Department
|
Department
|
||||||
</label>
|
</label>
|
||||||
@ -492,17 +465,10 @@ const AssetDetail: React.FC = () => {
|
|||||||
<option value="Cardiology">Cardiology</option>
|
<option value="Cardiology">Cardiology</option>
|
||||||
<option value="IT">IT</option>
|
<option value="IT">IT</option>
|
||||||
</select>
|
</select>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<LinkField
|
|
||||||
label="Department"
|
|
||||||
doctype="Department"
|
|
||||||
value={formData.department || ''}
|
|
||||||
onChange={(val) => setFormData({ ...formData, department: val })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Building
|
Building
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -512,56 +478,340 @@ const AssetDetail: React.FC = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Building name"
|
placeholder="Building name"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Area/Unit
|
Area/Unit
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Area or unit"
|
placeholder="Area or unit"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Room Number
|
Room Number
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Room 001-002"
|
placeholder="e.g. Room 001-002"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Assigned To
|
Assigned To
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Person or department"
|
placeholder="Person or department"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* COLUMN 4: More Details */}
|
{/* Coverage */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Coverage</h2>
|
||||||
More Details
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Site Contractor
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="custom_site_contractor"
|
||||||
|
value={formData.custom_site_contractor}
|
||||||
|
onChange={handleChange}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Contract Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Subcontractor
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Service Agreement
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Select</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Service Coverage
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Select</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Start Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
End Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Total Amount
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="custom_total_amount"
|
||||||
|
value={formData.custom_total_amount}
|
||||||
|
onChange={handleChange}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Comments
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Acquisition Details */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Acquisition Details</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Purchase Order Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="PO number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Supplier/Vendor
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Select</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Gross Purchase Amount
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Price</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Purchase Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Installation Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Available For Use Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Financial Details */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Financial Details</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
The depreciation method is an accounting method used to allocate the cost of a tangible asset over its useful life.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Depreciation Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Straight Line</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Depreciation Rate (%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Current Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Annual Rate
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Current Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End-of-Life Details */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">End-of-Life Details</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Expected End-of-Life Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Disposal Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Recycling</option>
|
||||||
|
<option value="Donation">Donation</option>
|
||||||
|
<option value="Sale">Sale</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - More Details */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">More Details</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Condition
|
Condition
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -569,7 +819,7 @@ const AssetDetail: React.FC = () => {
|
|||||||
value={formData.custom_device_status}
|
value={formData.custom_device_status}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
>
|
>
|
||||||
<option value="">Select status</option>
|
<option value="">Select status</option>
|
||||||
<option value="Operational">Operational</option>
|
<option value="Operational">Operational</option>
|
||||||
@ -579,288 +829,26 @@ const AssetDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* QR Code */}
|
{/* QR Code */}
|
||||||
<div className="flex justify-center my-4">
|
<div className="flex justify-center my-6">
|
||||||
<div className="border-2 border-gray-300 dark:border-gray-600 p-3 rounded-lg bg-white dark:bg-gray-700">
|
<div className="border-2 border-gray-300 dark:border-gray-600 p-4 rounded-lg">
|
||||||
<FaQrcode size={120} className="text-gray-400 dark:text-gray-500" />
|
<FaQrcode size={120} className="text-gray-400 dark:text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Brief description of the asset"
|
placeholder="Brief description of the asset"
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Sections - 3 Columns */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
|
||||||
{/* Coverage */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
Coverage
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Site Contractor
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="custom_site_contractor"
|
|
||||||
value={formData.custom_site_contractor}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Contract Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Service Agreement
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Select</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Start Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
End Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Total Amount
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="custom_total_amount"
|
|
||||||
value={formData.custom_total_amount}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Comments
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={2}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Acquisition Details */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
Acquisition Details
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Purchase Order Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="PO number"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Supplier/Vendor
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Select</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Gross Purchase Amount
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="Price"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Purchase Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Installation Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Available For Use Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Financial Details */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
Financial Details
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Depreciation Method
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Straight Line</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Depreciation Rate (%)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Annual Rate
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Current Value
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* End-of-Life Details - Full Width */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mt-6">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
End-of-Life Details
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Expected End-of-Life Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Disposal Method
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Recycling</option>
|
|
||||||
<option value="Donation">Donation</option>
|
|
||||||
<option value="Sale">Sale</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,423 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { useMaintenanceLogDetails, useMaintenanceMutations } from '../hooks/useAssetMaintenance';
|
|
||||||
import { FaArrowLeft, FaSave, FaEdit, FaCheckCircle, FaClock } from 'react-icons/fa';
|
|
||||||
import type { CreateMaintenanceData } from '../services/assetMaintenanceService';
|
|
||||||
|
|
||||||
const AssetMaintenanceDetail: React.FC = () => {
|
|
||||||
const { logName } = useParams<{ logName: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const duplicateFromLog = searchParams.get('duplicate');
|
|
||||||
|
|
||||||
const isNewLog = logName === 'new';
|
|
||||||
const isDuplicating = isNewLog && !!duplicateFromLog;
|
|
||||||
|
|
||||||
const { log, loading, error } = useMaintenanceLogDetails(
|
|
||||||
isDuplicating ? duplicateFromLog : (isNewLog ? null : logName || null)
|
|
||||||
);
|
|
||||||
const { createLog, updateLog, updateStatus, loading: saving } = useMaintenanceMutations();
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(isNewLog);
|
|
||||||
const [formData, setFormData] = useState<CreateMaintenanceData>({
|
|
||||||
asset_name: '',
|
|
||||||
task: '',
|
|
||||||
task_name: '',
|
|
||||||
maintenance_type: 'Preventive',
|
|
||||||
periodicity: '',
|
|
||||||
maintenance_status: 'Planned',
|
|
||||||
due_date: '',
|
|
||||||
assign_to_name: '',
|
|
||||||
description: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (log) {
|
|
||||||
setFormData({
|
|
||||||
asset_name: log.asset_name || '',
|
|
||||||
task: log.task || '',
|
|
||||||
task_name: log.task_name || '',
|
|
||||||
maintenance_type: log.maintenance_type || 'Preventive',
|
|
||||||
periodicity: log.periodicity || '',
|
|
||||||
maintenance_status: isDuplicating ? 'Planned' : (log.maintenance_status || 'Planned'),
|
|
||||||
due_date: log.due_date || '',
|
|
||||||
assign_to_name: log.assign_to_name || '',
|
|
||||||
description: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [log, isDuplicating]);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.asset_name) {
|
|
||||||
alert('Please enter Asset Name');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.maintenance_type) {
|
|
||||||
alert('Please select Maintenance Type');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Submitting maintenance log data:', formData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isNewLog || isDuplicating) {
|
|
||||||
const newLog = await createLog(formData);
|
|
||||||
const successMessage = isDuplicating
|
|
||||||
? 'Maintenance log duplicated successfully!'
|
|
||||||
: 'Maintenance log created successfully!';
|
|
||||||
alert(successMessage);
|
|
||||||
navigate(`/maintenance/${newLog.name}`);
|
|
||||||
} else if (logName) {
|
|
||||||
await updateLog(logName, formData);
|
|
||||||
alert('Maintenance log updated successfully!');
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Maintenance log save error:', err);
|
|
||||||
alert('Failed to save: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusUpdate = async (newStatus: string) => {
|
|
||||||
if (!logName || isNewLog) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateStatus(logName, newStatus);
|
|
||||||
alert('Status updated successfully!');
|
|
||||||
window.location.reload();
|
|
||||||
} catch (err) {
|
|
||||||
alert('Failed to update status: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading maintenance log...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && !isNewLog && !isDuplicating) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
||||||
<p className="text-red-600 dark:text-red-400">Error: {error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/maintenance')}
|
|
||||||
className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
Back to maintenance logs
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/maintenance')}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaArrowLeft />
|
|
||||||
<span className="text-gray-900 dark:text-white">
|
|
||||||
{isDuplicating ? 'Duplicate Maintenance Log' : (isNewLog ? 'New Maintenance Log' : 'Maintenance Log Details')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isNewLog && !isEditing && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsEditing(true)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
{log?.maintenance_status !== 'Completed' && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleStatusUpdate('Completed')}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
<FaCheckCircle />
|
|
||||||
Mark Complete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isEditing && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (isNewLog) {
|
|
||||||
navigate('/maintenance');
|
|
||||||
} else {
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={saving}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<FaSave />
|
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Left Column */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
{/* Maintenance Information */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">Maintenance Information</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Log ID
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={isNewLog || isDuplicating ? 'Auto-generated' : log?.name}
|
|
||||||
disabled
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
{isDuplicating && (
|
|
||||||
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
|
||||||
💡 Duplicating from: {duplicateFromLog}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Asset Name <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="asset_name"
|
|
||||||
value={formData.asset_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Asset name or ID"
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Maintenance Type <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="maintenance_type"
|
|
||||||
value={formData.maintenance_type}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="Preventive">Preventive</option>
|
|
||||||
<option value="Corrective">Corrective</option>
|
|
||||||
<option value="Calibration">Calibration</option>
|
|
||||||
<option value="Inspection">Inspection</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Task Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="task_name"
|
|
||||||
value={formData.task_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Maintenance task name"
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Periodicity
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="periodicity"
|
|
||||||
value={formData.periodicity}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Select periodicity</option>
|
|
||||||
<option value="Daily">Daily</option>
|
|
||||||
<option value="Weekly">Weekly</option>
|
|
||||||
<option value="Monthly">Monthly</option>
|
|
||||||
<option value="Quarterly">Quarterly</option>
|
|
||||||
<option value="Half-yearly">Half-yearly</option>
|
|
||||||
<option value="Yearly">Yearly</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="maintenance_status"
|
|
||||||
value={formData.maintenance_status}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="Planned">Planned</option>
|
|
||||||
<option value="Completed">Completed</option>
|
|
||||||
<option value="Overdue">Overdue</option>
|
|
||||||
<option value="Cancelled">Cancelled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Due Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="due_date"
|
|
||||||
value={formData.due_date}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Assigned To
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="assign_to_name"
|
|
||||||
value={formData.assign_to_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Technician name"
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Description / Notes
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
rows={4}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Maintenance notes and details..."
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Status Summary */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">Status Summary</h2>
|
|
||||||
|
|
||||||
{!isNewLog && log && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Current Status</p>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{log.maintenance_status || 'Planned'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Due Date</p>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{log.due_date ? new Date(log.due_date).toLocaleDateString() : 'Not set'}
|
|
||||||
</p>
|
|
||||||
{log.due_date && new Date(log.due_date) < new Date() && log.maintenance_status !== 'Completed' && (
|
|
||||||
<p className="text-xs text-red-600 dark:text-red-400 font-semibold mt-1">
|
|
||||||
⚠️ Overdue
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Type</p>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{log.maintenance_type || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Assigned To</p>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{log.assign_to_name || 'Unassigned'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Created</p>
|
|
||||||
<p className="text-xs text-gray-900 dark:text-white">
|
|
||||||
{log.creation ? new Date(log.creation).toLocaleString() : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isNewLog && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<FaClock className="text-4xl text-gray-400 dark:text-gray-500 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Status information will appear after creation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AssetMaintenanceDetail;
|
|
||||||
|
|
||||||
@ -1,499 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useAssetMaintenanceLogs, useMaintenanceMutations } from '../hooks/useAssetMaintenance';
|
|
||||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaCheckCircle, FaClock, FaExclamationTriangle, FaCalendarCheck } from 'react-icons/fa';
|
|
||||||
|
|
||||||
const AssetMaintenanceList: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
|
|
||||||
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const limit = 20;
|
|
||||||
|
|
||||||
const filters = statusFilter ? { maintenance_status: statusFilter } : {};
|
|
||||||
|
|
||||||
const { logs, totalCount, hasMore, loading, error, refetch } = useAssetMaintenanceLogs(
|
|
||||||
filters,
|
|
||||||
limit,
|
|
||||||
page * limit,
|
|
||||||
'due_date asc'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { deleteLog, loading: mutationLoading } = useMaintenanceMutations();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (actionMenuOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [actionMenuOpen]);
|
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
|
||||||
navigate('/maintenance/new');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleView = (logName: string) => {
|
|
||||||
navigate(`/maintenance/${logName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (logName: string) => {
|
|
||||||
navigate(`/maintenance/${logName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (logName: string) => {
|
|
||||||
try {
|
|
||||||
await deleteLog(logName);
|
|
||||||
setDeleteConfirmOpen(null);
|
|
||||||
refetch();
|
|
||||||
alert('Maintenance log deleted successfully!');
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicate = (logName: string) => {
|
|
||||||
navigate(`/maintenance/new?duplicate=${logName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = (log: any) => {
|
|
||||||
const dataStr = JSON.stringify(log, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `maintenance_${log.name}.json`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrint = (logName: string) => {
|
|
||||||
window.open(`/maintenance/${logName}?print=true`, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportAll = () => {
|
|
||||||
const headers = ['Log ID', 'Asset', 'Type', 'Status', 'Due Date', 'Assigned To'];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...logs.map(log => [
|
|
||||||
log.name,
|
|
||||||
log.asset_name || '',
|
|
||||||
log.maintenance_type || '',
|
|
||||||
log.maintenance_status || '',
|
|
||||||
log.due_date || '',
|
|
||||||
log.assign_to_name || ''
|
|
||||||
].join(','))
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const dataBlob = new Blob([csvContent], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `maintenance_logs_${new Date().toISOString().split('T')[0]}.csv`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status?.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
return <FaCheckCircle className="text-green-500" />;
|
|
||||||
case 'planned':
|
|
||||||
return <FaCalendarCheck className="text-blue-500" />;
|
|
||||||
case 'overdue':
|
|
||||||
return <FaExclamationTriangle className="text-red-500" />;
|
|
||||||
default:
|
|
||||||
return <FaClock className="text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status?.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
|
|
||||||
case 'planned':
|
|
||||||
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300';
|
|
||||||
case 'overdue':
|
|
||||||
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
|
||||||
case 'cancelled':
|
|
||||||
return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
|
|
||||||
default:
|
|
||||||
return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOverdue = (dueDate: string, status: string) => {
|
|
||||||
if (!dueDate || status?.toLowerCase() === 'completed') return false;
|
|
||||||
return new Date(dueDate) < new Date();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading && page === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading maintenance logs...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
|
|
||||||
<h2 className="text-xl font-bold text-yellow-800 dark:text-yellow-300 mb-4">⚠️ Maintenance API Not Available</h2>
|
|
||||||
<div className="text-yellow-700 dark:text-yellow-400 space-y-3">
|
|
||||||
<p><strong>The Asset Maintenance API endpoint is not deployed yet.</strong></p>
|
|
||||||
<div className="mt-4 flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/maintenance/new')}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Creating New (Demo)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={refetch}
|
|
||||||
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-yellow-300 dark:border-yellow-700">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<strong>Technical Error:</strong> {error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredLogs = logs.filter(log =>
|
|
||||||
log.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
log.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
log.task_name?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">Asset Maintenance</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Total: {totalCount} maintenance log{totalCount !== 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={handleExportAll}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg flex items-center gap-2 shadow transition-all"
|
|
||||||
disabled={logs.length === 0}
|
|
||||||
>
|
|
||||||
<FaFileExport />
|
|
||||||
<span className="font-medium">Export All</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
<span className="font-medium">New Maintenance Log</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Bar */}
|
|
||||||
<div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
|
|
||||||
<FaSearch className="text-gray-400 dark:text-gray-500" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by ID, asset, task..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
setStatusFilter(e.target.value);
|
|
||||||
setPage(0);
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="Planned">Planned</option>
|
|
||||||
<option value="Completed">Completed</option>
|
|
||||||
<option value="Overdue">Overdue</option>
|
|
||||||
<option value="Cancelled">Cancelled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Maintenance Logs Table */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Log ID
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Asset
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Type
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Due Date
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredLogs.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
|
|
||||||
<p>No maintenance logs found</p>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
|
||||||
>
|
|
||||||
Create your first maintenance log
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredLogs.map((log) => {
|
|
||||||
const overdue = isOverdue(log.due_date || '', log.maintenance_status || '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={log.name}
|
|
||||||
className={`hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${
|
|
||||||
overdue ? 'bg-red-50 dark:bg-red-900/10' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => handleView(log.name)}
|
|
||||||
>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="font-medium text-gray-900 dark:text-white">{log.name}</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{log.creation ? new Date(log.creation).toLocaleDateString() : ''}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-900 dark:text-white">{log.asset_name || '-'}</div>
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">{log.custom_asset_type || ''}</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{log.maintenance_type || '-'}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{log.due_date ? new Date(log.due_date).toLocaleDateString() : '-'}
|
|
||||||
</div>
|
|
||||||
{overdue && (
|
|
||||||
<div className="text-xs text-red-600 dark:text-red-400 font-semibold">
|
|
||||||
Overdue
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getStatusIcon(log.maintenance_status || '')}
|
|
||||||
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(log.maintenance_status || '')}`}>
|
|
||||||
{log.maintenance_status || 'Unknown'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
||||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<button
|
|
||||||
onClick={() => handleView(log.name)}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 p-2 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded transition-colors"
|
|
||||||
title="View Details"
|
|
||||||
>
|
|
||||||
<FaEye />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(log.name)}
|
|
||||||
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 p-2 hover:bg-green-50 dark:hover:bg-green-900/30 rounded transition-colors"
|
|
||||||
title="Edit Log"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDuplicate(log.name)}
|
|
||||||
className="text-purple-600 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 p-2 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded transition-colors"
|
|
||||||
title="Duplicate"
|
|
||||||
>
|
|
||||||
<FaCopy />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirmOpen(log.name)}
|
|
||||||
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 p-2 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
|
||||||
title="Delete"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="relative" ref={actionMenuOpen === log.name ? dropdownRef : null}>
|
|
||||||
<button
|
|
||||||
onClick={() => setActionMenuOpen(actionMenuOpen === log.name ? null : log.name)}
|
|
||||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="More Actions"
|
|
||||||
>
|
|
||||||
<FaEllipsisV />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{actionMenuOpen === log.name && (
|
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleExport(log);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-t-lg"
|
|
||||||
>
|
|
||||||
<FaDownload className="text-blue-500" />
|
|
||||||
Export as JSON
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handlePrint(log.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-b-lg"
|
|
||||||
>
|
|
||||||
<FaPrint className="text-purple-500" />
|
|
||||||
Print Log
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{filteredLogs.length > 0 && (
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 flex items-center justify-between border-t border-gray-200 dark:border-gray-600">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
Showing <span className="font-medium">{page * limit + 1}</span> to{' '}
|
|
||||||
<span className="font-medium">
|
|
||||||
{Math.min((page + 1) * limit, totalCount)}
|
|
||||||
</span>{' '}
|
|
||||||
of <span className="font-medium">{totalCount}</span> results
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
disabled={page === 0}
|
|
||||||
onClick={() => setPage(page - 1)}
|
|
||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
disabled={!hasMore}
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
{deleteConfirmOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
|
||||||
<FaTrash className="text-red-600 dark:text-red-400 text-xl" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Delete Maintenance Log
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
Are you sure you want to delete this maintenance log? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 mb-4">
|
|
||||||
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
|
||||||
<strong>Log ID:</strong> {deleteConfirmOpen}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirmOpen(null)}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(deleteConfirmOpen)}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
{mutationLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
||||||
Deleting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaTrash />
|
|
||||||
Delete Log
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AssetMaintenanceList;
|
|
||||||
|
|
||||||
@ -70,38 +70,11 @@ const Login: React.FC = () => {
|
|||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-center mb-6">
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
<div className="w-32 h-32 flex items-center justify-center bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-4">
|
Sign in to your account
|
||||||
{/* Seera Arabia Logo */}
|
|
||||||
<img
|
|
||||||
src="/seera-logo.png"
|
|
||||||
alt="Seera Arabia"
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
onError={(e) => {
|
|
||||||
// Fallback to gradient background with SVG if image not found
|
|
||||||
const container = e.currentTarget.parentElement;
|
|
||||||
if (container) {
|
|
||||||
container.classList.add('bg-gradient-to-br', 'from-indigo-600', 'to-purple-600');
|
|
||||||
}
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<svg className="w-20 h-20 hidden" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="white" fillOpacity="0.9"/>
|
|
||||||
<path d="M2 17L12 22L22 17V12L12 17L2 12V17Z" fill="white" fillOpacity="0.7"/>
|
|
||||||
<path d="M12 12V17" stroke="white" strokeWidth="2" strokeLinecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-center text-3xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Seera Arabia
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-center text-sm font-medium text-indigo-600 dark:text-indigo-400">
|
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
Asset Management System
|
Connect to your Frappe backend
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-center text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
Sign in to continue
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,406 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { usePPMDetails, usePPMMutations } from '../hooks/usePPM';
|
|
||||||
import { FaArrowLeft, FaSave, FaEdit, FaBuilding, FaTools, FaCalendarCheck, FaDollarSign } from 'react-icons/fa';
|
|
||||||
import type { CreatePPMData } from '../services/ppmService';
|
|
||||||
|
|
||||||
const PPMDetail: React.FC = () => {
|
|
||||||
const { ppmName } = useParams<{ ppmName: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const duplicateFromPPM = searchParams.get('duplicate');
|
|
||||||
|
|
||||||
const isNewPPM = ppmName === 'new';
|
|
||||||
const isDuplicating = isNewPPM && !!duplicateFromPPM;
|
|
||||||
|
|
||||||
const { ppm, loading, error, refetch } = usePPMDetails(
|
|
||||||
isDuplicating ? duplicateFromPPM : (isNewPPM ? null : ppmName || null)
|
|
||||||
);
|
|
||||||
const { createPPM, updatePPM, loading: saving } = usePPMMutations();
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(isNewPPM);
|
|
||||||
const [formData, setFormData] = useState<CreatePPMData>({
|
|
||||||
company: '',
|
|
||||||
asset_name: '',
|
|
||||||
custom_asset_type: '',
|
|
||||||
maintenance_team: '',
|
|
||||||
custom_frequency: '',
|
|
||||||
custom_total_amount: 0,
|
|
||||||
custom_no_of_pms: 0,
|
|
||||||
custom_price_per_pm: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ppm) {
|
|
||||||
setFormData({
|
|
||||||
company: ppm.company || '',
|
|
||||||
asset_name: ppm.asset_name || '',
|
|
||||||
custom_asset_type: ppm.custom_asset_type || '',
|
|
||||||
maintenance_team: ppm.maintenance_team || '',
|
|
||||||
custom_frequency: ppm.custom_frequency || '',
|
|
||||||
custom_total_amount: ppm.custom_total_amount || 0,
|
|
||||||
custom_no_of_pms: ppm.custom_no_of_pms || 0,
|
|
||||||
custom_price_per_pm: ppm.custom_price_per_pm || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ppm, isDuplicating]);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: name.includes('amount') || name.includes('pms') || name.includes('price')
|
|
||||||
? parseFloat(value) || 0
|
|
||||||
: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.asset_name) {
|
|
||||||
alert('Please enter Asset Name');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isNewPPM || isDuplicating) {
|
|
||||||
const result = await createPPM(formData);
|
|
||||||
const successMessage = isDuplicating
|
|
||||||
? 'PPM schedule duplicated successfully!'
|
|
||||||
: 'PPM schedule created successfully!';
|
|
||||||
alert(successMessage);
|
|
||||||
if (result.asset_maintenance?.name) {
|
|
||||||
navigate(`/ppm/${result.asset_maintenance.name}`);
|
|
||||||
} else {
|
|
||||||
refetch();
|
|
||||||
navigate('/ppm');
|
|
||||||
}
|
|
||||||
} else if (ppmName) {
|
|
||||||
await updatePPM(ppmName, formData);
|
|
||||||
alert('PPM schedule updated successfully!');
|
|
||||||
setIsEditing(false);
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('PPM save error:', err);
|
|
||||||
alert('Failed to save: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading PPM schedule...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && !isNewPPM && !isDuplicating) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
||||||
<p className="text-red-600 dark:text-red-400">Error: {error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/ppm')}
|
|
||||||
className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
Back to PPM schedules
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/ppm')}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaArrowLeft />
|
|
||||||
<span className="text-gray-900 dark:text-white">
|
|
||||||
{isDuplicating ? 'Duplicate PPM Schedule' : (isNewPPM ? 'New PPM Schedule' : 'PPM Schedule Details')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isNewPPM && !isEditing && (
|
|
||||||
<button
|
|
||||||
onClick={() => setIsEditing(true)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Main Form */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Basic Information</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Company *
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="company"
|
|
||||||
value={formData.company}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.company || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Asset Name *
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="asset_name"
|
|
||||||
value={formData.asset_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.asset_name || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Asset Type
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="custom_asset_type"
|
|
||||||
value={formData.custom_asset_type}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.custom_asset_type || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Maintenance Team
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="maintenance_team"
|
|
||||||
value={formData.maintenance_team}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.maintenance_team || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Frequency
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="custom_frequency"
|
|
||||||
value={formData.custom_frequency}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="e.g., Monthly, Quarterly, Yearly"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.custom_frequency || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Financial Information */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Financial Information</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Number of PMs
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="custom_no_of_pms"
|
|
||||||
value={formData.custom_no_of_pms}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">{ppm?.custom_no_of_pms || '-'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Price per PM
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="custom_price_per_pm"
|
|
||||||
value={formData.custom_price_per_pm}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white">
|
|
||||||
{ppm?.custom_price_per_pm ? `$${ppm.custom_price_per_pm.toLocaleString()}` : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Total Amount
|
|
||||||
</label>
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="custom_total_amount"
|
|
||||||
value={formData.custom_total_amount}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-gray-900 dark:text-white font-semibold">
|
|
||||||
{ppm?.custom_total_amount ? `$${ppm.custom_total_amount.toLocaleString()}` : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar Info */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Schedule Information</h3>
|
|
||||||
|
|
||||||
{!isNewPPM && ppm && (
|
|
||||||
<>
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">PPM ID</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{ppm.name}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Created</p>
|
|
||||||
<p className="text-xs text-gray-900 dark:text-white">
|
|
||||||
{ppm.creation ? new Date(ppm.creation).toLocaleString() : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isNewPPM && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<FaTools className="text-4xl text-gray-400 dark:text-gray-500 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Schedule information will appear after creation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
{isEditing && (
|
|
||||||
<div className="mt-6 flex justify-end gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (isNewPPM) {
|
|
||||||
navigate('/ppm');
|
|
||||||
} else {
|
|
||||||
setIsEditing(false);
|
|
||||||
if (ppm) {
|
|
||||||
setFormData({
|
|
||||||
company: ppm.company || '',
|
|
||||||
asset_name: ppm.asset_name || '',
|
|
||||||
custom_asset_type: ppm.custom_asset_type || '',
|
|
||||||
maintenance_team: ppm.maintenance_team || '',
|
|
||||||
custom_frequency: ppm.custom_frequency || '',
|
|
||||||
custom_total_amount: ppm.custom_total_amount || 0,
|
|
||||||
custom_no_of_pms: ppm.custom_no_of_pms || 0,
|
|
||||||
custom_price_per_pm: ppm.custom_price_per_pm || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="px-6 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={saving}
|
|
||||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<FaSave />
|
|
||||||
{saving ? 'Saving...' : (isNewPPM ? 'Create' : 'Save Changes')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PPMDetail;
|
|
||||||
|
|
||||||
@ -1,441 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { usePPMs, usePPMMutations } from '../hooks/usePPM';
|
|
||||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaFileExport, FaCalendarCheck, FaTools, FaBuilding } from 'react-icons/fa';
|
|
||||||
|
|
||||||
const PPMList: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [companyFilter, setCompanyFilter] = useState<string>('');
|
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
|
|
||||||
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const limit = 20;
|
|
||||||
|
|
||||||
const filters = companyFilter ? { company: companyFilter } : {};
|
|
||||||
|
|
||||||
const { ppms, totalCount, hasMore, loading, error, refetch } = usePPMs(
|
|
||||||
filters,
|
|
||||||
limit,
|
|
||||||
page * limit,
|
|
||||||
'creation desc'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { deletePPM, loading: mutationLoading } = usePPMMutations();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (actionMenuOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [actionMenuOpen]);
|
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
|
||||||
navigate('/ppm/new');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleView = (ppmName: string) => {
|
|
||||||
navigate(`/ppm/${ppmName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (ppmName: string) => {
|
|
||||||
navigate(`/ppm/${ppmName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (ppmName: string) => {
|
|
||||||
try {
|
|
||||||
await deletePPM(ppmName);
|
|
||||||
setDeleteConfirmOpen(null);
|
|
||||||
refetch();
|
|
||||||
alert('PPM schedule deleted successfully!');
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicate = (ppmName: string) => {
|
|
||||||
navigate(`/ppm/new?duplicate=${ppmName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = (ppm: any) => {
|
|
||||||
const dataStr = JSON.stringify(ppm, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `ppm_${ppm.name}.json`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportAll = () => {
|
|
||||||
const headers = ['PPM ID', 'Company', 'Asset', 'Asset Type', 'Frequency', 'No. of PMs', 'Total Amount'];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...ppms.map(ppm => [
|
|
||||||
ppm.name,
|
|
||||||
ppm.company || '',
|
|
||||||
ppm.asset_name || '',
|
|
||||||
ppm.custom_asset_type || '',
|
|
||||||
ppm.custom_frequency || '',
|
|
||||||
ppm.custom_no_of_pms || '',
|
|
||||||
ppm.custom_total_amount || ''
|
|
||||||
].join(','))
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const dataBlob = new Blob([csvContent], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `ppm_schedules_${new Date().toISOString().split('T')[0]}.csv`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading && page === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading PPM schedules...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
|
|
||||||
<h2 className="text-xl font-bold text-yellow-800 dark:text-yellow-300 mb-4">⚠️ PPM API Not Available</h2>
|
|
||||||
<div className="text-yellow-700 dark:text-yellow-400 space-y-3">
|
|
||||||
<p><strong>The PPM API endpoint is not deployed yet.</strong></p>
|
|
||||||
<div className="mt-4 flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/ppm/new')}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Creating New (Demo)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={refetch}
|
|
||||||
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-yellow-300 dark:border-yellow-700">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<strong>Technical Error:</strong> {error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredPPMs = ppms.filter(ppm =>
|
|
||||||
ppm.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
ppm.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
ppm.company?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
ppm.custom_asset_type?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">PPM Schedules</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Total: {totalCount} PPM schedule{totalCount !== 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={handleExportAll}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg flex items-center gap-2 shadow transition-all"
|
|
||||||
disabled={ppms.length === 0}
|
|
||||||
>
|
|
||||||
<FaFileExport />
|
|
||||||
<span className="font-medium">Export All</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
<span className="font-medium">New PPM Schedule</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Bar */}
|
|
||||||
<div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
|
|
||||||
<FaSearch className="text-gray-400 dark:text-gray-500" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by ID, asset, company..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Filter by Company"
|
|
||||||
value={companyFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCompanyFilter(e.target.value);
|
|
||||||
setPage(0);
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PPM Schedules Table */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
PPM ID
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Company
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Asset
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Asset Type
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Frequency
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
No. of PMs
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Total Amount
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredPPMs.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={8} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
|
|
||||||
<p>No PPM schedules found</p>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
|
||||||
>
|
|
||||||
Create your first PPM schedule
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredPPMs.map((ppm) => (
|
|
||||||
<tr
|
|
||||||
key={ppm.name}
|
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer"
|
|
||||||
onClick={() => handleView(ppm.name)}
|
|
||||||
>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{ppm.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaBuilding className="text-gray-400" />
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{ppm.company || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{ppm.asset_name || '-'}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{ppm.custom_asset_type || '-'}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaCalendarCheck className="text-blue-500" />
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{ppm.custom_frequency || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{ppm.custom_no_of_pms || '-'}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{ppm.custom_total_amount ? `$${ppm.custom_total_amount.toLocaleString()}` : '-'}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
||||||
<div className="relative" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<button
|
|
||||||
onClick={() => setActionMenuOpen(actionMenuOpen === ppm.name ? null : ppm.name)}
|
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<FaEllipsisV />
|
|
||||||
</button>
|
|
||||||
{actionMenuOpen === ppm.name && (
|
|
||||||
<div
|
|
||||||
ref={dropdownRef}
|
|
||||||
className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-10 border border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleView(ppm.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaEye />
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleEdit(ppm.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleDuplicate(ppm.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaCopy />
|
|
||||||
Duplicate
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleExport(ppm);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaFileExport />
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700"></div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteConfirmOpen(ppm.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{(hasMore || page > 0) && (
|
|
||||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
Showing {page * limit + 1} to {Math.min((page + 1) * limit, totalCount)} of {totalCount} results
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(Math.max(0, page - 1))}
|
|
||||||
disabled={page === 0}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
disabled={!hasMore}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
{deleteConfirmOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
|
||||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4">Confirm Delete</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
Are you sure you want to delete this PPM schedule? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3 justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirmOpen(null)}
|
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(deleteConfirmOpen)}
|
|
||||||
disabled={mutationLoading}
|
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{mutationLoading ? 'Deleting...' : 'Delete'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PPMList;
|
|
||||||
|
|
||||||
@ -1,571 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { useWorkOrderDetails, useWorkOrderMutations } from '../hooks/useWorkOrder';
|
|
||||||
import { FaArrowLeft, FaSave, FaEdit, FaCheckCircle, FaClock } from 'react-icons/fa';
|
|
||||||
import type { CreateWorkOrderData } from '../services/workOrderService';
|
|
||||||
|
|
||||||
const WorkOrderDetail: React.FC = () => {
|
|
||||||
const { workOrderName } = useParams<{ workOrderName: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const duplicateFromWorkOrder = searchParams.get('duplicate');
|
|
||||||
|
|
||||||
const isNewWorkOrder = workOrderName === 'new';
|
|
||||||
const isDuplicating = isNewWorkOrder && !!duplicateFromWorkOrder;
|
|
||||||
|
|
||||||
const { workOrder, loading, error } = useWorkOrderDetails(
|
|
||||||
isDuplicating ? duplicateFromWorkOrder : (isNewWorkOrder ? null : workOrderName || null)
|
|
||||||
);
|
|
||||||
const { createWorkOrder, updateWorkOrder, updateStatus, loading: saving } = useWorkOrderMutations();
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(isNewWorkOrder);
|
|
||||||
const [formData, setFormData] = useState<CreateWorkOrderData>({
|
|
||||||
company: '',
|
|
||||||
work_order_type: '',
|
|
||||||
asset: '',
|
|
||||||
asset_name: '',
|
|
||||||
description: '',
|
|
||||||
repair_status: 'Open',
|
|
||||||
workflow_state: '',
|
|
||||||
department: '',
|
|
||||||
custom_priority_: 'Normal',
|
|
||||||
asset_type: '',
|
|
||||||
manufacturer: '',
|
|
||||||
serial_number: '',
|
|
||||||
model: '',
|
|
||||||
custom_site_contractor: '',
|
|
||||||
custom_subcontractor: '',
|
|
||||||
failure_date: '',
|
|
||||||
custom_deadline_date: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (workOrder) {
|
|
||||||
setFormData({
|
|
||||||
company: workOrder.company || '',
|
|
||||||
work_order_type: workOrder.work_order_type || '',
|
|
||||||
asset: workOrder.asset || '',
|
|
||||||
asset_name: isDuplicating ? `${workOrder.asset_name} (Copy)` : (workOrder.asset_name || ''),
|
|
||||||
description: workOrder.description || '',
|
|
||||||
repair_status: isDuplicating ? 'Open' : (workOrder.repair_status || 'Open'),
|
|
||||||
workflow_state: workOrder.workflow_state || '',
|
|
||||||
department: workOrder.department || '',
|
|
||||||
custom_priority_: workOrder.custom_priority_ || 'Normal',
|
|
||||||
asset_type: workOrder.asset_type || '',
|
|
||||||
manufacturer: workOrder.manufacturer || '',
|
|
||||||
serial_number: workOrder.serial_number || '',
|
|
||||||
model: workOrder.model || '',
|
|
||||||
custom_site_contractor: workOrder.custom_site_contractor || '',
|
|
||||||
custom_subcontractor: workOrder.custom_subcontractor || '',
|
|
||||||
failure_date: workOrder.failure_date || '',
|
|
||||||
custom_deadline_date: workOrder.custom_deadline_date || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [workOrder, isDuplicating]);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.work_order_type) {
|
|
||||||
alert('Please select a Work Order Type');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Submitting work order data:', formData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isNewWorkOrder || isDuplicating) {
|
|
||||||
const newWorkOrder = await createWorkOrder(formData);
|
|
||||||
const successMessage = isDuplicating
|
|
||||||
? 'Work order duplicated successfully!'
|
|
||||||
: 'Work order created successfully!';
|
|
||||||
alert(successMessage);
|
|
||||||
navigate(`/work-orders/${newWorkOrder.name}`);
|
|
||||||
} else if (workOrderName) {
|
|
||||||
await updateWorkOrder(workOrderName, formData);
|
|
||||||
alert('Work order updated successfully!');
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Work order save error:', err);
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
||||||
|
|
||||||
if (errorMessage.includes('404') || errorMessage.includes('not found') ||
|
|
||||||
errorMessage.includes('has no attribute') || errorMessage.includes('417')) {
|
|
||||||
alert(
|
|
||||||
'⚠️ Work Order API Not Deployed\n\n' +
|
|
||||||
'The Work Order API endpoint is not deployed on your Frappe server yet.\n\n' +
|
|
||||||
'Deploy work_order_api.py to: frappe-bench/apps/asset_lite/asset_lite/api/\n\n' +
|
|
||||||
'Error: ' + errorMessage
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
alert('Failed to save work order:\n\n' + errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusUpdate = async (newStatus: string) => {
|
|
||||||
if (!workOrderName || isNewWorkOrder) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateStatus(workOrderName, newStatus);
|
|
||||||
alert('Status updated successfully!');
|
|
||||||
window.location.reload();
|
|
||||||
} catch (err) {
|
|
||||||
alert('Failed to update status: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading work order details...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && !isNewWorkOrder && !isDuplicating) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
||||||
<p className="text-red-600 dark:text-red-400">Error: {error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/work-orders')}
|
|
||||||
className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
Back to work orders list
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/work-orders')}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaArrowLeft />
|
|
||||||
<span className="text-gray-900 dark:text-white">
|
|
||||||
{isDuplicating ? 'Duplicate Work Order' : (isNewWorkOrder ? 'New Work Order' : 'Work Order Details')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isNewWorkOrder && !isEditing && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsEditing(true)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
{/* Quick Status Update Buttons */}
|
|
||||||
{workOrder?.repair_status !== 'Completed' && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleStatusUpdate('Completed')}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
<FaCheckCircle />
|
|
||||||
Mark Complete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{workOrder?.repair_status !== 'In Progress' && workOrder?.repair_status !== 'Completed' && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleStatusUpdate('In Progress')}
|
|
||||||
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
<FaClock />
|
|
||||||
Start Work
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isEditing && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (isNewWorkOrder) {
|
|
||||||
navigate('/work-orders');
|
|
||||||
} else {
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={saving}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<FaSave />
|
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Left Column - Main Info */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
{/* Work Order Information */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">Work Order Information</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Work Order ID
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={isNewWorkOrder || isDuplicating ? 'Auto-generated' : workOrder?.name}
|
|
||||||
disabled
|
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
|
|
||||||
/>
|
|
||||||
{isDuplicating && (
|
|
||||||
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
|
||||||
💡 Duplicating from: {duplicateFromWorkOrder}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Work Order Type <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="work_order_type"
|
|
||||||
value={formData.work_order_type}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="">Select type</option>
|
|
||||||
<option value="Preventive Maintenance">Preventive Maintenance</option>
|
|
||||||
<option value="Corrective Maintenance">Corrective Maintenance</option>
|
|
||||||
<option value="Breakdown Maintenance">Breakdown Maintenance</option>
|
|
||||||
<option value="Calibration">Calibration</option>
|
|
||||||
<option value="Inspection">Inspection</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Priority
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="custom_priority_"
|
|
||||||
value={formData.custom_priority_}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="Low">Low</option>
|
|
||||||
<option value="Normal">Normal</option>
|
|
||||||
<option value="Medium">Medium</option>
|
|
||||||
<option value="High">High</option>
|
|
||||||
<option value="Urgent">Urgent</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="repair_status"
|
|
||||||
value={formData.repair_status}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
>
|
|
||||||
<option value="Open">Open</option>
|
|
||||||
<option value="In Progress">In Progress</option>
|
|
||||||
<option value="Pending">Pending</option>
|
|
||||||
<option value="Completed">Completed</option>
|
|
||||||
<option value="Cancelled">Cancelled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Company
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="company"
|
|
||||||
value={formData.company}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
rows={3}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Describe the issue or maintenance task..."
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* COLUMN 2: Asset Information */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
Asset Information
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Asset ID
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="asset"
|
|
||||||
value={formData.asset}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="e.g. ACC-ASS-2025-00001"
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Asset Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="asset_name"
|
|
||||||
value={formData.asset_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="Asset name"
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Asset Type
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="asset_type"
|
|
||||||
value={formData.asset_type}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Manufacturer
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="manufacturer"
|
|
||||||
value={formData.manufacturer}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Serial Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="serial_number"
|
|
||||||
value={formData.serial_number}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Model
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="model"
|
|
||||||
value={formData.model}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location & Assignment */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">Location & Assignment</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Department
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="department"
|
|
||||||
value={formData.department}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Site Contractor
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="custom_site_contractor"
|
|
||||||
value={formData.custom_site_contractor}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Subcontractor
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="custom_subcontractor"
|
|
||||||
value={formData.custom_subcontractor}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Failure Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="failure_date"
|
|
||||||
value={formData.failure_date}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Deadline Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="custom_deadline_date"
|
|
||||||
value={formData.custom_deadline_date}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className="w-full px-3 py-2 text-sm 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Status & Summary */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">Status Summary</h2>
|
|
||||||
|
|
||||||
{!isNewWorkOrder && workOrder && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Current Status</p>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{workOrder.repair_status || 'Open'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Priority</p>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{workOrder.custom_priority_ || 'Normal'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Created</p>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{workOrder.creation ? new Date(workOrder.creation).toLocaleString() : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Last Modified</p>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
|
||||||
{workOrder.modified ? new Date(workOrder.modified).toLocaleString() : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isNewWorkOrder && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<FaClock className="text-4xl text-gray-400 dark:text-gray-500 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Status information will appear after creation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WorkOrderDetail;
|
|
||||||
|
|
||||||
@ -1,513 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useWorkOrders, useWorkOrderMutations } from '../hooks/useWorkOrder';
|
|
||||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaCheckCircle, FaClock, FaExclamationTriangle } from 'react-icons/fa';
|
|
||||||
|
|
||||||
const WorkOrderList: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
|
|
||||||
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const limit = 20;
|
|
||||||
|
|
||||||
const filters = statusFilter ? { repair_status: statusFilter } : {};
|
|
||||||
|
|
||||||
const { workOrders, totalCount, hasMore, loading, error, refetch } = useWorkOrders(
|
|
||||||
filters,
|
|
||||||
limit,
|
|
||||||
page * limit,
|
|
||||||
'creation desc'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { deleteWorkOrder, loading: mutationLoading } = useWorkOrderMutations();
|
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (actionMenuOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [actionMenuOpen]);
|
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
|
||||||
navigate('/work-orders/new');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleView = (workOrderName: string) => {
|
|
||||||
navigate(`/work-orders/${workOrderName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (workOrderName: string) => {
|
|
||||||
navigate(`/work-orders/${workOrderName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (workOrderName: string) => {
|
|
||||||
try {
|
|
||||||
await deleteWorkOrder(workOrderName);
|
|
||||||
setDeleteConfirmOpen(null);
|
|
||||||
refetch();
|
|
||||||
alert('Work order deleted successfully!');
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Failed to delete work order: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicate = (workOrderName: string) => {
|
|
||||||
navigate(`/work-orders/new?duplicate=${workOrderName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = (workOrder: any) => {
|
|
||||||
const dataStr = JSON.stringify(workOrder, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `work_order_${workOrder.name}.json`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrint = (workOrderName: string) => {
|
|
||||||
window.open(`/work-orders/${workOrderName}?print=true`, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportAll = () => {
|
|
||||||
const headers = ['Work Order ID', 'Asset', 'Type', 'Status', 'Department', 'Priority', 'Created'];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...workOrders.map(wo => [
|
|
||||||
wo.name,
|
|
||||||
wo.asset_name || wo.asset || '',
|
|
||||||
wo.work_order_type || '',
|
|
||||||
wo.repair_status || '',
|
|
||||||
wo.department || '',
|
|
||||||
wo.custom_priority_ || '',
|
|
||||||
wo.creation ? new Date(wo.creation).toLocaleDateString() : ''
|
|
||||||
].join(','))
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const dataBlob = new Blob([csvContent], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `work_orders_export_${new Date().toISOString().split('T')[0]}.csv`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status?.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
return <FaCheckCircle className="text-green-500" />;
|
|
||||||
case 'in progress':
|
|
||||||
return <FaClock className="text-blue-500" />;
|
|
||||||
case 'pending':
|
|
||||||
return <FaExclamationTriangle className="text-yellow-500" />;
|
|
||||||
default:
|
|
||||||
return <FaClock className="text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status?.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
|
|
||||||
case 'in progress':
|
|
||||||
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300';
|
|
||||||
case 'pending':
|
|
||||||
case 'open':
|
|
||||||
return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300';
|
|
||||||
case 'cancelled':
|
|
||||||
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority?.toLowerCase()) {
|
|
||||||
case 'high':
|
|
||||||
case 'urgent':
|
|
||||||
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
|
||||||
case 'medium':
|
|
||||||
return 'bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300';
|
|
||||||
case 'low':
|
|
||||||
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading && page === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading work orders...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
|
|
||||||
<h2 className="text-xl font-bold text-yellow-800 dark:text-yellow-300 mb-4">⚠️ Work Order API Not Available</h2>
|
|
||||||
<div className="text-yellow-700 dark:text-yellow-400 space-y-3">
|
|
||||||
<p><strong>The Work Order API endpoint is not deployed yet.</strong></p>
|
|
||||||
<p>To fix this, deploy the work_order_api.py file to your Frappe server.</p>
|
|
||||||
<div className="mt-4 flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/work-orders/new')}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Creating New (Demo)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={refetch}
|
|
||||||
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-yellow-300 dark:border-yellow-700">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<strong>Technical Error:</strong> {error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter work orders by search term
|
|
||||||
const filteredWorkOrders = workOrders.filter(wo =>
|
|
||||||
wo.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
wo.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
wo.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
wo.asset?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">Work Orders</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Total: {totalCount} work order{totalCount !== 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={handleExportAll}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg flex items-center gap-2 shadow transition-all"
|
|
||||||
disabled={workOrders.length === 0}
|
|
||||||
>
|
|
||||||
<FaFileExport />
|
|
||||||
<span className="font-medium">Export All</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
<span className="font-medium">New Work Order</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Bar */}
|
|
||||||
<div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
|
|
||||||
<FaSearch className="text-gray-400 dark:text-gray-500" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by ID, asset name, description..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
setStatusFilter(e.target.value);
|
|
||||||
setPage(0);
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="Open">Open</option>
|
|
||||||
<option value="In Progress">In Progress</option>
|
|
||||||
<option value="Pending">Pending</option>
|
|
||||||
<option value="Completed">Completed</option>
|
|
||||||
<option value="Cancelled">Cancelled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Work Orders Table */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Work Order ID
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Asset
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Type
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Department
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Priority
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredWorkOrders.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
|
|
||||||
<p>No work orders found</p>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
|
||||||
>
|
|
||||||
Create your first work order
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredWorkOrders.map((workOrder) => (
|
|
||||||
<tr
|
|
||||||
key={workOrder.name}
|
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
|
|
||||||
onClick={() => handleView(workOrder.name)}
|
|
||||||
>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="font-medium text-gray-900 dark:text-white">{workOrder.name}</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{workOrder.creation ? new Date(workOrder.creation).toLocaleDateString() : ''}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-900 dark:text-white">{workOrder.asset_name || '-'}</div>
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">{workOrder.asset || ''}</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{workOrder.work_order_type || '-'}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{workOrder.department || '-'}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getStatusIcon(workOrder.repair_status || '')}
|
|
||||||
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(workOrder.repair_status || '')}`}>
|
|
||||||
{workOrder.repair_status || 'Unknown'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(workOrder.custom_priority_ || '')}`}>
|
|
||||||
{workOrder.custom_priority_ || 'Normal'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
||||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<button
|
|
||||||
onClick={() => handleView(workOrder.name)}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 p-2 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded transition-colors"
|
|
||||||
title="View Details"
|
|
||||||
>
|
|
||||||
<FaEye />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(workOrder.name)}
|
|
||||||
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 p-2 hover:bg-green-50 dark:hover:bg-green-900/30 rounded transition-colors"
|
|
||||||
title="Edit Work Order"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDuplicate(workOrder.name)}
|
|
||||||
className="text-purple-600 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 p-2 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded transition-colors"
|
|
||||||
title="Duplicate Work Order"
|
|
||||||
>
|
|
||||||
<FaCopy />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirmOpen(workOrder.name)}
|
|
||||||
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 p-2 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
|
||||||
title="Delete Work Order"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* More Actions Dropdown */}
|
|
||||||
<div className="relative" ref={actionMenuOpen === workOrder.name ? dropdownRef : null}>
|
|
||||||
<button
|
|
||||||
onClick={() => setActionMenuOpen(actionMenuOpen === workOrder.name ? null : workOrder.name)}
|
|
||||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="More Actions"
|
|
||||||
>
|
|
||||||
<FaEllipsisV />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{actionMenuOpen === workOrder.name && (
|
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleExport(workOrder);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-t-lg"
|
|
||||||
>
|
|
||||||
<FaDownload className="text-blue-500" />
|
|
||||||
Export as JSON
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handlePrint(workOrder.name);
|
|
||||||
setActionMenuOpen(null);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-b-lg"
|
|
||||||
>
|
|
||||||
<FaPrint className="text-purple-500" />
|
|
||||||
Print Work Order
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{filteredWorkOrders.length > 0 && (
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 flex items-center justify-between border-t border-gray-200 dark:border-gray-600">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
Showing <span className="font-medium">{page * limit + 1}</span> to{' '}
|
|
||||||
<span className="font-medium">
|
|
||||||
{Math.min((page + 1) * limit, totalCount)}
|
|
||||||
</span>{' '}
|
|
||||||
of <span className="font-medium">{totalCount}</span> results
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
disabled={page === 0}
|
|
||||||
onClick={() => setPage(page - 1)}
|
|
||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
disabled={!hasMore}
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
{deleteConfirmOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
|
||||||
<FaTrash className="text-red-600 dark:text-red-400 text-xl" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Delete Work Order
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
Are you sure you want to delete this work order? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 mb-4">
|
|
||||||
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
|
||||||
<strong>Work Order ID:</strong> {deleteConfirmOpen}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirmOpen(null)}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(deleteConfirmOpen)}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50"
|
|
||||||
disabled={mutationLoading}
|
|
||||||
>
|
|
||||||
{mutationLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
||||||
Deleting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaTrash />
|
|
||||||
Delete Work Order
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WorkOrderList;
|
|
||||||
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
import apiService from './apiService';
|
|
||||||
import API_CONFIG from '../config/api';
|
|
||||||
|
|
||||||
// Asset Maintenance Interfaces
|
|
||||||
export interface AssetMaintenanceLog {
|
|
||||||
name: string;
|
|
||||||
asset_maintenance?: string;
|
|
||||||
naming_series?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
custom_asset_type?: string;
|
|
||||||
item_code?: string;
|
|
||||||
item_name?: string;
|
|
||||||
custom_asset_names?: string;
|
|
||||||
custom_hospital_name?: string;
|
|
||||||
task?: string;
|
|
||||||
task_name?: string;
|
|
||||||
maintenance_type?: string;
|
|
||||||
periodicity?: string;
|
|
||||||
has_certificate?: number;
|
|
||||||
custom_early_completion?: number;
|
|
||||||
maintenance_status?: string;
|
|
||||||
custom_pm_overdue_reason?: string;
|
|
||||||
custom_accepted_by_moh?: string;
|
|
||||||
assign_to_name?: string;
|
|
||||||
due_date?: string;
|
|
||||||
custom_accepted_by_moh_?: string;
|
|
||||||
custom_template?: string;
|
|
||||||
workflow_state?: string;
|
|
||||||
creation?: string;
|
|
||||||
modified?: string;
|
|
||||||
owner?: string;
|
|
||||||
modified_by?: string;
|
|
||||||
docstatus?: number;
|
|
||||||
idx?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssetMaintenanceListResponse {
|
|
||||||
asset_maintenance_logs: AssetMaintenanceLog[];
|
|
||||||
total_count: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
has_more: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MaintenanceFilters {
|
|
||||||
maintenance_status?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
custom_hospital_name?: string;
|
|
||||||
maintenance_type?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateMaintenanceData {
|
|
||||||
asset_name?: string;
|
|
||||||
task?: string;
|
|
||||||
task_name?: string;
|
|
||||||
maintenance_type?: string;
|
|
||||||
periodicity?: string;
|
|
||||||
maintenance_status?: string;
|
|
||||||
due_date?: string;
|
|
||||||
assign_to_name?: string;
|
|
||||||
description?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssetMaintenanceService {
|
|
||||||
/**
|
|
||||||
* Get list of asset maintenance logs with optional filters and pagination
|
|
||||||
*/
|
|
||||||
async getMaintenanceLogs(
|
|
||||||
filters?: MaintenanceFilters,
|
|
||||||
fields?: string[],
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0,
|
|
||||||
orderBy?: string
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
|
||||||
params.append('fields', JSON.stringify(fields));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
if (orderBy) {
|
|
||||||
params.append('order_by', orderBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_LOGS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed information about a specific maintenance log
|
|
||||||
*/
|
|
||||||
async getMaintenanceLogDetails(logName: string): Promise<AssetMaintenanceLog> {
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_LOG_DETAILS}?log_name=${encodeURIComponent(logName)}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceLog>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new maintenance log
|
|
||||||
*/
|
|
||||||
async createMaintenanceLog(logData: CreateMaintenanceData): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.CREATE_ASSET_MAINTENANCE_LOG, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ log_data: logData })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing maintenance log
|
|
||||||
*/
|
|
||||||
async updateMaintenanceLog(
|
|
||||||
logName: string,
|
|
||||||
logData: Partial<CreateMaintenanceData>
|
|
||||||
): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_ASSET_MAINTENANCE_LOG, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
log_name: logName,
|
|
||||||
log_data: logData
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a maintenance log
|
|
||||||
*/
|
|
||||||
async deleteMaintenanceLog(logName: string): Promise<{ success: boolean; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.DELETE_ASSET_MAINTENANCE_LOG, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ log_name: logName })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update maintenance log status
|
|
||||||
*/
|
|
||||||
async updateMaintenanceStatus(
|
|
||||||
logName: string,
|
|
||||||
maintenanceStatus?: string,
|
|
||||||
workflowState?: string
|
|
||||||
): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_MAINTENANCE_STATUS, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
log_name: logName,
|
|
||||||
maintenance_status: maintenanceStatus,
|
|
||||||
workflow_state: workflowState
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get maintenance logs for a specific asset
|
|
||||||
*/
|
|
||||||
async getMaintenanceLogsByAsset(
|
|
||||||
assetName: string,
|
|
||||||
filters?: MaintenanceFilters,
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('asset_name', assetName);
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCE_LOGS_BY_ASSET}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get overdue maintenance logs
|
|
||||||
*/
|
|
||||||
async getOverdueMaintenanceLogs(
|
|
||||||
filters?: MaintenanceFilters,
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_OVERDUE_MAINTENANCE_LOGS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and export singleton instance
|
|
||||||
const assetMaintenanceService = new AssetMaintenanceService();
|
|
||||||
export default assetMaintenanceService;
|
|
||||||
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
import apiService from './apiService';
|
|
||||||
import API_CONFIG from '../config/api';
|
|
||||||
|
|
||||||
// PPM (Asset Maintenance) Interfaces
|
|
||||||
export interface AssetMaintenance {
|
|
||||||
name: string;
|
|
||||||
company?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
custom_asset_type?: string;
|
|
||||||
asset_category?: string;
|
|
||||||
custom_type_of_maintenance?: string;
|
|
||||||
custom_asset_name?: string;
|
|
||||||
item_code?: string;
|
|
||||||
item_name?: string;
|
|
||||||
maintenance_team?: string;
|
|
||||||
custom_pm_schedule?: string;
|
|
||||||
maintenance_manager?: string;
|
|
||||||
maintenance_manager_name?: string;
|
|
||||||
custom_warranty?: string;
|
|
||||||
custom_warranty_status?: string;
|
|
||||||
custom_service_contract?: number;
|
|
||||||
custom_service_contract_status?: string;
|
|
||||||
custom_frequency?: string;
|
|
||||||
custom_total_amount?: number;
|
|
||||||
custom_no_of_pms?: number;
|
|
||||||
custom_price_per_pm?: number;
|
|
||||||
creation?: string;
|
|
||||||
modified?: string;
|
|
||||||
owner?: string;
|
|
||||||
modified_by?: string;
|
|
||||||
docstatus?: number;
|
|
||||||
idx?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssetMaintenanceListResponse {
|
|
||||||
asset_maintenances: AssetMaintenance[];
|
|
||||||
total_count: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
has_more: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MaintenanceTask {
|
|
||||||
name: string;
|
|
||||||
parent?: string;
|
|
||||||
task?: string;
|
|
||||||
task_name?: string;
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
periodicity?: string;
|
|
||||||
maintenance_type?: string;
|
|
||||||
maintenance_status?: string;
|
|
||||||
assign_to?: string;
|
|
||||||
assign_to_name?: string;
|
|
||||||
next_due_date?: string;
|
|
||||||
last_completion_date?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceCoverage {
|
|
||||||
name: string;
|
|
||||||
parent?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PPMFilters {
|
|
||||||
company?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
custom_asset_type?: string;
|
|
||||||
maintenance_team?: string;
|
|
||||||
custom_service_contract?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreatePPMData {
|
|
||||||
company?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
custom_asset_type?: string;
|
|
||||||
maintenance_team?: string;
|
|
||||||
custom_frequency?: string;
|
|
||||||
custom_total_amount?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PPMService {
|
|
||||||
/**
|
|
||||||
* Get list of asset maintenances (PPM schedules) with optional filters and pagination
|
|
||||||
*/
|
|
||||||
async getAssetMaintenances(
|
|
||||||
filters?: PPMFilters,
|
|
||||||
fields?: string[],
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0,
|
|
||||||
orderBy?: string
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
|
||||||
params.append('fields', JSON.stringify(fields));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
if (orderBy) {
|
|
||||||
params.append('order_by', orderBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCES}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed information about a specific asset maintenance
|
|
||||||
*/
|
|
||||||
async getAssetMaintenanceDetails(maintenanceName: string): Promise<AssetMaintenance> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('maintenance_name', maintenanceName);
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_DETAILS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenance>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new asset maintenance (PPM schedule)
|
|
||||||
*/
|
|
||||||
async createAssetMaintenance(data: CreatePPMData): Promise<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }> {
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.CREATE_ASSET_MAINTENANCE}`;
|
|
||||||
return apiService.apiCall<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }>(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ maintenance_data: JSON.stringify(data) })
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing asset maintenance
|
|
||||||
*/
|
|
||||||
async updateAssetMaintenance(
|
|
||||||
maintenanceName: string,
|
|
||||||
data: Partial<CreatePPMData>
|
|
||||||
): Promise<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }> {
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.UPDATE_ASSET_MAINTENANCE}`;
|
|
||||||
return apiService.apiCall<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }>(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
maintenance_name: maintenanceName,
|
|
||||||
maintenance_data: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an asset maintenance
|
|
||||||
*/
|
|
||||||
async deleteAssetMaintenance(maintenanceName: string): Promise<{ success: boolean; message?: string }> {
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.DELETE_ASSET_MAINTENANCE}`;
|
|
||||||
return apiService.apiCall<{ success: boolean; message?: string }>(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ maintenance_name: maintenanceName })
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all maintenance tasks for a specific asset maintenance
|
|
||||||
*/
|
|
||||||
async getMaintenanceTasks(maintenanceName: string): Promise<{ maintenance_tasks: MaintenanceTask[]; total_count: number }> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('maintenance_name', maintenanceName);
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCE_TASKS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<{ maintenance_tasks: MaintenanceTask[]; total_count: number }>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get service coverage for a specific asset maintenance
|
|
||||||
*/
|
|
||||||
async getServiceCoverage(maintenanceName: string): Promise<{ service_coverage: ServiceCoverage[]; total_count: number }> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('maintenance_name', maintenanceName);
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_SERVICE_COVERAGE}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<{ service_coverage: ServiceCoverage[]; total_count: number }>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all maintenance schedules for a specific asset
|
|
||||||
*/
|
|
||||||
async getMaintenancesByAsset(
|
|
||||||
assetName: string,
|
|
||||||
filters?: PPMFilters,
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('asset_name', assetName);
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCES_BY_ASSET}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all asset maintenances with active service contracts
|
|
||||||
*/
|
|
||||||
async getActiveServiceContracts(
|
|
||||||
filters?: PPMFilters,
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0
|
|
||||||
): Promise<AssetMaintenanceListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ACTIVE_SERVICE_CONTRACTS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<AssetMaintenanceListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ppmService = new PPMService();
|
|
||||||
export default ppmService;
|
|
||||||
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
import apiService from './apiService';
|
|
||||||
import API_CONFIG from '../config/api';
|
|
||||||
|
|
||||||
// Work Order Interfaces
|
|
||||||
export interface WorkOrder {
|
|
||||||
name: string;
|
|
||||||
company?: string;
|
|
||||||
naming_series?: string;
|
|
||||||
work_order_type?: string;
|
|
||||||
asset_type?: string;
|
|
||||||
manufacturer?: string;
|
|
||||||
serial_number?: string;
|
|
||||||
custom_priority_?: string;
|
|
||||||
asset?: string;
|
|
||||||
custom_maintenance_manager?: string;
|
|
||||||
department?: string;
|
|
||||||
repair_status?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
supplier?: string;
|
|
||||||
custom_pending_reason?: string;
|
|
||||||
model?: string;
|
|
||||||
custom_site_contractor?: string;
|
|
||||||
custom_subcontractor?: string;
|
|
||||||
custom_service_agreement?: string;
|
|
||||||
custom_service_coverage?: string;
|
|
||||||
custom_start_date?: string;
|
|
||||||
custom_end_date?: string;
|
|
||||||
custom_total_amount?: number;
|
|
||||||
warranty?: string;
|
|
||||||
service_contract?: string;
|
|
||||||
covering_spare_parts?: string;
|
|
||||||
spare_parts_labour?: string;
|
|
||||||
covering_labour?: string;
|
|
||||||
ppm_only?: number;
|
|
||||||
failure_date?: string;
|
|
||||||
total_hours_spent?: number;
|
|
||||||
job_completed?: string;
|
|
||||||
custom_difference?: number;
|
|
||||||
custom_vendors_hrs?: number;
|
|
||||||
custom_deadline_date?: string;
|
|
||||||
custom_diffrence?: number;
|
|
||||||
feedback_rating?: number;
|
|
||||||
first_responded_on?: string;
|
|
||||||
penalty?: number;
|
|
||||||
custom_assigned_supervisor?: string;
|
|
||||||
stock_consumption?: number;
|
|
||||||
need_procurement?: number;
|
|
||||||
repair_cost?: number;
|
|
||||||
total_repair_cost?: number;
|
|
||||||
capitalize_repair_cost?: number;
|
|
||||||
increase_in_asset_life?: number;
|
|
||||||
description?: string;
|
|
||||||
actions_performed?: string;
|
|
||||||
bio_med_dept?: string;
|
|
||||||
workflow_state?: string;
|
|
||||||
creation?: string;
|
|
||||||
modified?: string;
|
|
||||||
owner?: string;
|
|
||||||
modified_by?: string;
|
|
||||||
docstatus?: number;
|
|
||||||
idx?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkOrderListResponse {
|
|
||||||
work_orders: WorkOrder[];
|
|
||||||
total_count: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
has_more: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkOrderFilters {
|
|
||||||
company?: string;
|
|
||||||
department?: string;
|
|
||||||
work_order_type?: string;
|
|
||||||
repair_status?: string;
|
|
||||||
workflow_state?: string;
|
|
||||||
asset?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateWorkOrderData {
|
|
||||||
company?: string;
|
|
||||||
work_order_type?: string;
|
|
||||||
asset?: string;
|
|
||||||
asset_name?: string;
|
|
||||||
description?: string;
|
|
||||||
repair_status?: string;
|
|
||||||
workflow_state?: string;
|
|
||||||
department?: string;
|
|
||||||
custom_priority_?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkOrderService {
|
|
||||||
/**
|
|
||||||
* Get list of work orders with optional filters and pagination
|
|
||||||
*/
|
|
||||||
async getWorkOrders(
|
|
||||||
filters?: WorkOrderFilters,
|
|
||||||
fields?: string[],
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0,
|
|
||||||
orderBy?: string
|
|
||||||
): Promise<WorkOrderListResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
params.append('filters', JSON.stringify(filters));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
|
||||||
params.append('fields', JSON.stringify(fields));
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append('limit', limit.toString());
|
|
||||||
params.append('offset', offset.toString());
|
|
||||||
|
|
||||||
if (orderBy) {
|
|
||||||
params.append('order_by', orderBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_WORK_ORDERS}?${params.toString()}`;
|
|
||||||
return apiService.apiCall<WorkOrderListResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed information about a specific work order
|
|
||||||
*/
|
|
||||||
async getWorkOrderDetails(workOrderName: string): Promise<WorkOrder> {
|
|
||||||
const endpoint = `${API_CONFIG.ENDPOINTS.GET_WORK_ORDER_DETAILS}?work_order_name=${encodeURIComponent(workOrderName)}`;
|
|
||||||
return apiService.apiCall<WorkOrder>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new work order
|
|
||||||
*/
|
|
||||||
async createWorkOrder(workOrderData: CreateWorkOrderData): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.CREATE_WORK_ORDER, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ work_order_data: workOrderData })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing work order
|
|
||||||
*/
|
|
||||||
async updateWorkOrder(
|
|
||||||
workOrderName: string,
|
|
||||||
workOrderData: Partial<CreateWorkOrderData>
|
|
||||||
): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_WORK_ORDER, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
work_order_name: workOrderName,
|
|
||||||
work_order_data: workOrderData
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a work order
|
|
||||||
*/
|
|
||||||
async deleteWorkOrder(workOrderName: string): Promise<{ success: boolean; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.DELETE_WORK_ORDER, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ work_order_name: workOrderName })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update work order status
|
|
||||||
*/
|
|
||||||
async updateWorkOrderStatus(
|
|
||||||
workOrderName: string,
|
|
||||||
repairStatus?: string,
|
|
||||||
workflowState?: string
|
|
||||||
): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
|
|
||||||
return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_WORK_ORDER_STATUS, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
work_order_name: workOrderName,
|
|
||||||
repair_status: repairStatus,
|
|
||||||
workflow_state: workflowState
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and export singleton instance
|
|
||||||
const workOrderService = new WorkOrderService();
|
|
||||||
export default workOrderService;
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user