Original Idea
Reforging... (Takes ~30s)
Market Analysis
Top Competitors
- SoftSmile Vision: AI-powered orthodontic treatment planning for aligner production, but interface feels dated and requires installation.
- DentaLab for QuickBooks: Niche solution integrating case tracking with financial management for labs already using QuickBooks.
- EasyRx VisualDLP: Helps manage tasks and build clearer communication with clinical partners through a dedicated portal.
Market Gaps / Complaints
- Need for more user-friendly and affordable solutions for smaller labs.
- Desire for fully cloud-based solutions.
- Difficulty hiring and managing staff, along with keeping them happy and on time.
Unique Selling Points
Affordable, All-in-One Cloud Solution
Designed specifically for small to medium-sized dental labs, our cloud-based platform eliminates expensive upfront costs and hardware maintenance. Pay only for what you need with flexible subscription options.
Intelligent Scheduling & Staff Management
Streamline your lab's workflow with AI-powered scheduling that optimizes technician assignments based on skill, workload, and deadlines. Includes built-in time tracking, performance reports, and communication tools to boost team morale and productivity.
Simplified Workflow & Case Tracking
Manage cases from start to finish with our intuitive drag-and-drop interface. Track progress, assign tasks, communicate with dentists, and generate detailed reports, all in one place.
Feature Breakdown
New Feature Definition
User Authentication & Authorization
Securely manage user accounts, including registration, login...
Generated Prompt
```markdown
# Cursor/Windsurf Coding Prompt: User Authentication & Authorization for Dental Lab Manager
**Goal:** Implement a secure and robust user authentication and authorization system with role-based access control and two-factor authentication.
**Tech Stack:** (Assume standard web stack - adjust if different)
* **Backend:** Python (Flask or Django), Node.js (Express), or equivalent. Choose *one* and adapt accordingly. This prompt assumes Python/Flask.
* **Database:** PostgreSQL, MySQL, or equivalent. (SQLAlchemy used in the examples)
* **Frontend:** React, Vue.js, or similar. This prompt assumes React.
* **Authentication Library:** Authlib (for Python) or Passport.js (for Node.js). This prompt uses Authlib (Flask).
**Step 1: Database Model Design (Backend)**
```python
# models.py (Example using SQLAlchemy - adapt for your ORM)
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
password = Column(String(255), nullable=False) # Store password hash, not plaintext!
role = Column(String(20), default='Lab Technician') # Admin, Lab Technician, Dentist
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
two_factor_secret = Column(String(255), nullable=True) # For storing 2FA secret
two_factor_enabled = Column(Boolean, default=False)
def __repr__(self):
return f"<User(username='{self.username}', email='{self.email}', role='{self.role}')>"
# Example Database Setup (Adapt to your config)
DATABASE_URL = "postgresql://user:password@host:port/database" # Replace with your DB details
engine = create_engine(DATABASE_URL)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# TODO: Add password hashing functions (bcrypt, scrypt, or argon2)
import bcrypt
def hash_password(password: str) -> str:
"""Hashes the password using bcrypt."""
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
return hashed_password.decode('utf-8')
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verifies the plain password against the hashed password."""
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
```
**Step 2: API Routes (Backend - Flask Example)**
```python
# app.py (Example using Flask)
from flask import Flask, request, jsonify, redirect, url_for
from sqlalchemy.orm import Session
from authlib.integrations.flask_client import OAuth
import os
import pyotp # for 2FA
from .models import User, get_db, hash_password, verify_password # Import from models.py (adjust based on file structure)
from functools import wraps
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24) # Strong secret key!
# OAuth Setup (if using social logins) - REMOVE IF NOT NEEDED
# oauth = OAuth(app)
# google = oauth.register(
# name='google',
# client_id='YOUR_GOOGLE_CLIENT_ID',
# client_secret='YOUR_GOOGLE_CLIENT_SECRET',
# access_token_url='https://oauth2.googleapis.com/token',
# authorize_url='https://accounts.google.com/o/oauth2/auth',
# api_base_url='https://www.googleapis.com/oauth2/v1/',
# client_kwargs={'scope': 'email profile openid'},
# )
# User Authentication Routes
@app.route('/register', methods=['POST'])
def register():
db: Session = next(get_db())
data = request.get_json()
username = data.get('username')
email = data.get('email')
password = data.get('password')
if not username or not email or not password:
return jsonify({'message': 'Missing required fields'}), 400
if db.query(User).filter(User.username == username).first():
return jsonify({'message': 'Username already exists'}), 400
if db.query(User).filter(User.email == email).first():
return jsonify({'message': 'Email already exists'}), 400
hashed_password = hash_password(password) # Hash the password
new_user = User(username=username, email=email, password=hashed_password) # Store the HASHED password!
db.add(new_user)
db.commit()
db.refresh(new_user)
return jsonify({'message': 'User registered successfully'}), 201
@app.route('/login', methods=['POST'])
def login():
db: Session = next(get_db())
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'message': 'Missing required fields'}), 400
user = db.query(User).filter(User.username == username).first()
if not user or not verify_password(password, user.password):
return jsonify({'message': 'Invalid credentials'}), 401
# TODO: Implement JWT or session-based authentication here
# For simplicity, we'll return a dummy token for now. Replace with a proper token implementation!
token = 'dummy_token_for_user_' + str(user.id)
# Check if 2FA is enabled and return a 2FA setup status
if user.two_factor_enabled:
return jsonify({'message': 'Login successful, 2FA required', 'token': token, 'two_factor_required': True}), 200
return jsonify({'message': 'Login successful', 'token': token, 'two_factor_required': False}), 200
# Password Reset (Basic Implementation - enhance with email verification)
@app.route('/reset_password', methods=['POST'])
def reset_password():
db: Session = next(get_db())
data = request.get_json()
email = data.get('email')
new_password = data.get('new_password')
if not email or not new_password:
return jsonify({'message': 'Missing required fields'}), 400
user = db.query(User).filter(User.email == email).first()
if not user:
return jsonify({'message': 'User not found'}), 404
hashed_password = hash_password(new_password)
user.password = hashed_password
db.commit()
return jsonify({'message': 'Password reset successfully'}), 200
# 2FA Implementation
@app.route('/enable_2fa', methods=['POST'])
def enable_2fa():
db: Session = next(get_db())
data = request.get_json()
user_id = data.get('user_id') # In real app, get this from auth token
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'message': 'User not found'}), 404
# Generate a new secret for 2FA
totp_secret = pyotp.random_base32()
user.two_factor_secret = totp_secret
user.two_factor_enabled = True
db.commit()
# Generate a QR code for the user to scan (optional)
totp = pyotp.TOTP(totp_secret)
qr_code_url = totp.provisioning_uri(user.username, issuer_name="DentalLabManager")
return jsonify({'message': '2FA enabled', 'qr_code_url': qr_code_url, 'two_factor_secret': totp_secret}), 200
@app.route('/verify_2fa', methods=['POST'])
def verify_2fa():
db: Session = next(get_db())
data = request.get_json()
user_id = data.get('user_id') # In real app, get this from auth token
token = data.get('token')
user = db.query(User).filter(User.id == user_id).first()
if not user or not user.two_factor_enabled:
return jsonify({'message': '2FA is not enabled for this user'}), 400
totp = pyotp.TOTP(user.two_factor_secret)
if totp.verify(token):
return jsonify({'message': '2FA verification successful'}), 200
else:
return jsonify({'message': 'Invalid 2FA token'}), 401
# Example Protected Route (Requires Authentication)
def token_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization') # Example: Bearer <token>
if not token:
return jsonify({'message': 'Token is missing'}), 401
# In a real application, you would verify the token here
# and extract user information from it. For now, assume token is valid if it exists
# Example: (Replace with real token validation)
if not token.startswith('Bearer dummy_token_for_user_'):
return jsonify({'message': 'Invalid token'}), 401
user_id = token.replace('Bearer dummy_token_for_user_', '') # Extract a fake user id
# Pass the user ID to the protected function
return f(user_id, *args, **kwargs) # Pass the user_id
return decorated_function
# Role-Based Access Control (Example)
def admin_required(f):
@wraps(f)
def decorated_function(user_id, *args, **kwargs):
db: Session = next(get_db())
user = db.query(User).filter(User.id == int(user_id)).first()
if not user:
return jsonify({'message': 'User not found'}), 404
if user.role != 'Admin':
return jsonify({'message': 'Unauthorized'}), 403 # HTTP 403 Forbidden
return f(user_id, *args, **kwargs)
return decorated_function
@app.route('/admin_route')
@token_required
@admin_required
def admin_route(user_id):
# user_id is passed from token_required and admin_required
return jsonify({'message': f'Admin route accessed by user {user_id}'})
# Generic Lab Tech route example
@app.route('/lab_tech_route')
@token_required
def lab_tech_route(user_id):
db: Session = next(get_db())
user = db.query(User).filter(User.id == int(user_id)).first() # retrieve user from DB
if user and (user.role == 'Lab Technician' or user.role == 'Admin'):
return jsonify({'message': f'Lab tech route accessed by user {user.username}'})
return jsonify({'message': 'Unauthorized'}), 403
# Example: OAuth callback (REMOVE IF NOT USING SOCIAL LOGINS)
# @app.route('/login/google')
# def login_google():
# redirect_uri = url_for('authorize', _external=True)
# return google.authorize_redirect(redirect_uri)
# @app.route('/authorize')
# def authorize():
# token = google.authorize_access_token()
# resp = google.get('userinfo')
# user_info = resp.json()
# # TODO: Create/Login user based on Google user info
# return jsonify(user_info)
if __name__ == '__main__':
app.run(debug=True)
```
**Step 3: Frontend Components (React Example)**
```jsx
// RegistrationForm.jsx
import React, { useState } from 'react';
const RegistrationForm = () => {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/register', { // Backend endpoint
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, email, password }),
});
const data = await response.json();
setMessage(data.message);
if (response.ok) {
// Optionally, redirect to login page
// window.location.href = '/login';
}
} catch (error) {
setMessage('An error occurred during registration.');
console.error(error);
}
};
return (
<div>
<h2>Register</h2>
{message && <p>{message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
</div>
<div>
<label>Email:</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div>
<label>Password:</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<button type="submit">Register</button>
</form>
</div>
);
};
export default RegistrationForm;
```
```jsx
// LoginForm.jsx
import React, { useState } from 'react';
const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const [twoFactorRequired, setTwoFactorRequired] = useState(false); // State for 2FA requirement
const [token, setToken] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/login', { // Backend endpoint
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
setMessage(data.message);
if (response.ok) {
setToken(data.token); // Store the token (e.g., in local storage)
// localStorage.setItem('token', data.token); // Example using localStorage
if (data.two_factor_required) {
setTwoFactorRequired(true);
} else {
// Redirect to dashboard or appropriate route
window.location.href = '/dashboard'; // Replace with your route
}
}
} catch (error) {
setMessage('An error occurred during login.');
console.error(error);
}
};
if (twoFactorRequired) {
return <TwoFactorVerificationForm token={token} userId={token.replace('dummy_token_for_user_', '')} />; // Pass userId from token - VERY BAD in production.
}
return (
<div>
<h2>Login</h2>
{message && <p>{message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
</div>
<div>
<label>Password:</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<button type="submit">Login</button>
</form>
</div>
);
};
export default LoginForm;
```
```jsx
// TwoFactorVerificationForm.jsx
import React, { useState } from 'react';
const TwoFactorVerificationForm = ({ token, userId }) => {
const [verificationCode, setVerificationCode] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/verify_2fa', { // Backend endpoint
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId, token: verificationCode }),
});
const data = await response.json();
setMessage(data.message);
if (response.ok) {
// Redirect to dashboard upon successful verification
window.location.href = '/dashboard'; // Replace with your route
}
} catch (error) {
setMessage('An error occurred during 2FA verification.');
console.error(error);
}
};
return (
<div>
<h2>Two-Factor Verification</h2>
{message && <p>{message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>Verification Code:</label>
<input
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
</div>
<button type="submit">Verify</button>
</form>
</div>
);
};
export default TwoFactorVerificationForm;
```
```jsx
// Enable2FAForm.jsx
import React, { useState, useEffect } from 'react';
const Enable2FAForm = () => {
const [qrCodeUrl, setQrCodeUrl] = useState('');
const [message, setMessage] = useState('');
const [secretKey, setSecretKey] = useState('');
// Replace with the actual logged-in user ID or get it from a context
const userId = 1; // Example: Hardcoded user ID - BAD PRACTICE
useEffect(() => {
const enable2FA = async () => {
try {
const response = await fetch('/enable_2fa', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId }),
});
const data = await response.json();
if (response.ok) {
setQrCodeUrl(data.qr_code_url);
setSecretKey(data.two_factor_secret); // Only display the secret key to admin or for debugging purposes. Never expose it to the end-user!
setMessage(data.message);
} else {
setMessage(data.message);
}
} catch (error) {
setMessage('An error occurred while enabling 2FA.');
console.error(error);
}
};
enable2FA();
}, [userId]);
return (
<div>
<h2>Enable Two-Factor Authentication</h2>
{message && <p>{message}</p>}
{qrCodeUrl && (
<div>
<p>Scan this QR code with your authenticator app:</p>
<img src={qrCodeUrl} alt="QR Code" />
<p>Secret Key (for backup): {secretKey} </p> {/* Only for admin purposes */}
</div>
)}
</div>
);
};
export default Enable2FAForm;
```
```jsx
// Dashboard.jsx (Example - Requires a valid token from Login)
import React, { useState, useEffect } from 'react';
const Dashboard = () => {
const [message, setMessage] = useState('Loading...');
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const fetchAdminData = async () => {
const token = localStorage.getItem('token'); // Or from wherever you store the token
if (!token) {
setMessage("Not logged in. Redirecting...");
setTimeout(() => { window.location.href = '/login'; }, 1500); // redirect to login
return;
}
try {
const response = await fetch('/admin_route', { // Backend endpoint
headers: {
'Authorization': `Bearer ${token}`, // Include token in header
},
});
if (response.ok) {
const data = await response.json();
setMessage(data.message);
setIsAdmin(true);
} else {
setMessage("You don't have admin privileges. You're seeing this because the backend rejected your token.");
}
} catch (error) {
setMessage('An error occurred fetching admin data.');
console.error(error);
}
};
fetchAdminData();
}, []);
return (
<div>
<h2>Dashboard</h2>
<p>{message}</p>
{isAdmin && <p>Admin Privileges confirmed</p>}
</div>
);
};
export default Dashboard;
```
**Step 4: Frontend Routing and State Management**
* Use a React Router (or equivalent for your framework) to manage routes like `/register`, `/login`, `/dashboard`, `/enable-2fa`.
* Use Context API, Redux, or Zustand to manage the user's authentication state (e.g., `isLoggedIn`, `userRole`, `authToken`). This is crucial to prevent passing the token down through props excessively.
* Store the JWT (or session identifier) securely (e.g., in an HTTP-only cookie or local storage with appropriate security considerations). **Never store sensitive data in local storage without proper encryption.**
**Step 5: Security Considerations**
* **Password Hashing:** *Never* store passwords in plain text. Use a strong hashing algorithm like bcrypt, scrypt, or Argon2. Salt each password individually. The provided `hash_password` function is a basic example using bcrypt.
* **Input Validation:** Sanitize all user inputs on both the client and server to prevent injection attacks (SQL injection, XSS).
* **HTTPS:** Ensure your entire application runs over HTTPS to protect data in transit.
* **CSRF Protection:** Implement CSRF (Cross-Site Request Forgery) protection, especially for form submissions.
* **Rate Limiting:** Implement rate limiting on login and registration endpoints to prevent brute-force attacks.
* **JWT (JSON Web Tokens):** If using JWTs for authentication, use a strong secret key and properly validate the token on each request. Consider using refresh tokens for long-lived sessions. **Don't store sensitive information in the JWT itself.**
* **Two-Factor Authentication:** Implement 2FA using TOTP (Time-based One-Time Password) algorithms. Libraries like `pyotp` (Python) or `otplib` (Node.js) can help. Provide users with a backup code in case they lose access to their 2FA device.
* **Role-Based Access Control:** Implement RBAC to restrict access to sensitive data and functionality based on user roles.
* **Authorization Header Handling:** Securely store and handle the authentication token (e.g., JWT). Use "Bearer" authentication scheme in the `Authorization` header.
* **CORS:** Configure CORS (Cross-Origin Resource Sharing) to allow requests only from your frontend domain.
**Step 6: Testing**
* Write unit tests for your backend logic (e.g., password hashing, authentication, authorization).
* Write integration tests to verify that your API endpoints work correctly.
* Write end-to-end tests to simulate user interactions and verify that the entire authentication flow works as expected.
**Cursor/Windsurf Usage Notes:**
* Use `@insert` to inject code snippets into existing files.
* Use `@replace` to replace existing code with new code.
* Use `@create` to create new files.
* Be specific with your instructions. For example, instead of "Implement login," say "Implement a POST route at `/login` that authenticates users based on their username and password, returning a JWT on successful authentication. Hash the password using bcrypt before comparison."
* Provide context to Cursor/Windsurf. Tell it what libraries you're using, what your directory structure is, and what the existing code does.
* Break down complex tasks into smaller, manageable steps.
* Review the generated code carefully to ensure it meets your requirements and is secure.
**Example Prompts (for use *within* Cursor/Windsurf *after* providing the above context):**
1. `@insert into models.py: Add a function called 'create_user' that takes username, email, and password as arguments, hashes the password using bcrypt, and creates a new User object in the database. Return the new user object.`
2. `@insert into app.py: In the /login route, replace the dummy token generation with JWT generation using the PyJWT library. Set the JWT expiration time to 1 hour. Use the app.config['SECRET_KEY'] for signing the JWT.`
3. `@insert into app.py: In the 'token_required' decorator, verify the JWT using the PyJWT library and extract the user ID from the token. If the token is invalid or expired, return a 401 Unauthorized error.`
4. `@replace in LoginForm.jsx: After a successful login, store the token securely in an HTTP-only cookie using the 'js-cookie' library.`
5. `@insert into app.py: Create a new API endpoint '/logout' that invalidates the JWT (e.g., by adding it to a blacklist) and removes the authentication cookie from the client.`
6. `@insert into app.py: In the /reset_password route, before updating the password, generate a password reset token and send it to the user's email address. The token should expire after 24 hours.`
7. `@insert into TwoFactorVerificationForm.jsx: After the successful two-factor authentication, redirect user to /dashboard route.`
8. `@insert into RegistrationForm.jsx: After successful registration, automatically redirect the user to the login page.`
This detailed prompt should provide a solid foundation for implementing user authentication and authorization in your Dental Lab Manager application. Remember to adapt the code examples and instructions to your specific tech stack and requirements. Good luck!
```
Order Management System (OMS)
Create, track, and manage dental orders. This includes input...
Generated Prompt
Case Tracking & Workflow Automation
Real-time tracking of each case's progress through the lab. ...
Generated Prompt
Inventory Management
Track inventory levels of materials (e.g., zirconia, acrylic...
Generated Prompt
Digital Impression Integration & Management
Allow dentists to upload digital impressions (STL files) dir...
Generated Prompt
CAD/CAM Integration
Direct integration with CAD/CAM software used in the lab. A...
Generated Prompt
Reporting & Analytics
Generate reports on key performance indicators (KPIs) such a...
Generated Prompt
Communication Portal
Facilitate secure communication between the lab and dentists...
Generated Prompt
Invoice & Payment Processing
Generate invoices based on order details and materials used....
Generated Prompt
Device & Machine Integration
Implement integrations with lab equipment (e.g. 3D printers,...
Generated Prompt
```markdown
## Cursor/Windsurf Coding Prompt: Dental Lab Manager - Device & Machine Integration
This prompt focuses on implementing the "Device & Machine Integration" feature for a Dental Lab Manager application. We'll cover database models, API endpoints, and frontend components to manage lab equipment, track usage, and monitor status.
**Step 1: Database Schema (Models)**
```sql
-- Devices Table
CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL, -- e.g., "3D Printer", "Milling Machine", "Scanner"
manufacturer VARCHAR(255),
model VARCHAR(255),
serial_number VARCHAR(255) UNIQUE,
ip_address VARCHAR(15), -- For network-connected devices
connection_type VARCHAR(255), -- e.g., "Network", "USB", "Serial"
status VARCHAR(255) DEFAULT 'Idle', -- e.g., "Idle", "Running", "Error", "Maintenance"
last_heartbeat DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- DeviceLogs Table
CREATE TABLE device_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
event_type VARCHAR(255) NOT NULL, -- e.g., "Job Started", "Job Completed", "Error", "Maintenance Required"
message TEXT,
FOREIGN KEY (device_id) REFERENCES devices(id)
);
-- DeviceUsage Table
CREATE TABLE device_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id INTEGER NOT NULL,
job_id INTEGER, -- Link to the Job table (if applicable)
start_time DATETIME NOT NULL,
end_time DATETIME,
duration_seconds INTEGER, -- Calculated duration
material_used VARCHAR(255), -- e.g., "Resin", "Zirconia"
quantity_used DECIMAL(10, 2), -- Amount of material used
notes TEXT,
FOREIGN KEY (device_id) REFERENCES devices(id)
-- Consider adding a foreign key to the `jobs` table if you have one
);
```
**Prompt Instructions:**
* **Create these tables in your database.** You can use SQLite, PostgreSQL, MySQL, or any other preferred database.
* **Consider adding indexes** to `device_id` in `device_logs` and `device_usage` for faster queries.
* **`devices.connection_type` and `devices.ip_address`**: Design these fields to accommodate various connection methods. Prioritize network connections first.
* **`devices.status`**: Use an enum or a lookup table for device status to ensure consistency.
**Step 2: API Endpoints (Backend)**
Assume a REST API using Python (Flask or FastAPI) or Node.js (Express).
```python
# Example (Flask):
from flask import Flask, request, jsonify
import sqlite3 # Or your preferred DB library
app = Flask(__name__)
DATABASE = 'dental_lab.db' # Replace with your DB file
def get_db_connection():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row # Return rows as dictionaries
return conn
@app.route('/devices', methods=['GET'])
def get_devices():
conn = get_db_connection()
devices = conn.execute('SELECT * FROM devices').fetchall()
conn.close()
return jsonify([dict(row) for row in devices])
@app.route('/devices/<int:device_id>', methods=['GET'])
def get_device(device_id):
conn = get_db_connection()
device = conn.execute('SELECT * FROM devices WHERE id = ?', (device_id,)).fetchone()
conn.close()
if device is None:
return jsonify({'error': 'Device not found'}), 404
return jsonify(dict(device))
@app.route('/devices/<int:device_id>', methods=['PUT'])
def update_device(device_id):
conn = get_db_connection()
data = request.get_json()
# Validate data here!
try:
conn.execute("""
UPDATE devices SET
name = ?, type = ?, manufacturer = ?, model = ?, serial_number = ?,
ip_address = ?, connection_type = ?, status = ?
WHERE id = ?
""", (data['name'], data['type'], data['manufacturer'], data['model'], data['serial_number'],
data['ip_address'], data['connection_type'], data['status'], device_id))
conn.commit()
conn.close()
return jsonify({'message': 'Device updated successfully'})
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 400 # Bad Request
@app.route('/devices/<int:device_id>/logs', methods=['GET'])
def get_device_logs(device_id):
conn = get_db_connection()
logs = conn.execute('SELECT * FROM device_logs WHERE device_id = ? ORDER BY timestamp DESC', (device_id,)).fetchall()
conn.close()
return jsonify([dict(row) for row in logs])
@app.route('/devices/<int:device_id>/usage', methods=['GET'])
def get_device_usage(device_id):
conn = get_db_connection()
usage = conn.execute('SELECT * FROM device_usage WHERE device_id = ? ORDER BY start_time DESC', (device_id,)).fetchall()
conn.close()
return jsonify([dict(row) for row in usage])
# POST endpoint to create a new log entry
@app.route('/devices/<int:device_id>/logs', methods=['POST'])
def create_device_log(device_id):
conn = get_db_connection()
data = request.get_json()
try:
conn.execute("""
INSERT INTO device_logs (device_id, event_type, message)
VALUES (?, ?, ?)
""", (device_id, data['event_type'], data['message']))
conn.commit()
conn.close()
return jsonify({'message': 'Log created successfully'}), 201 # Created
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 400
@app.route('/devices/<int:device_id>/usage', methods=['POST'])
def create_device_usage(device_id):
conn = get_db_connection()
data = request.get_json()
try:
conn.execute("""
INSERT INTO device_usage (device_id, job_id, start_time, end_time, duration_seconds, material_used, quantity_used, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (device_id, data.get('job_id'), data['start_time'], data['end_time'], data.get('duration_seconds'), data.get('material_used'), data.get('quantity_used'), data.get('notes')))
conn.commit()
conn.close()
return jsonify({'message': 'Usage record created successfully'}), 201 # Created
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 400
if __name__ == '__main__':
app.run(debug=True)
```
**API Endpoint Summary:**
* `GET /devices`: Retrieve a list of all devices.
* `GET /devices/{device_id}`: Retrieve details of a specific device.
* `PUT /devices/{device_id}`: Update device information (name, type, status, etc.). Requires request body with updated fields. **Important: Implement robust validation.**
* `GET /devices/{device_id}/logs`: Retrieve device logs for a specific device, ordered by timestamp (most recent first).
* `POST /devices/{device_id}/logs`: Create a new device log entry. Requires request body with `event_type` and `message`.
* `GET /devices/{device_id}/usage`: Retrieve device usage records for a specific device, ordered by `start_time` (most recent first).
* `POST /devices/{device_id}/usage`: Create a new device usage record. Requires request body with `start_time`, `end_time` (can be null initially), `material_used`, `quantity_used`, `notes`. `job_id` and `duration_seconds` are optional.
**Prompt Instructions:**
* **Implement these API endpoints** using your preferred backend framework. Adapt the example to your specific needs.
* **Implement proper error handling and validation** for all endpoints, especially the `PUT` and `POST` methods. Return appropriate HTTP status codes.
* **Authentication and Authorization:** Incorporate authentication and authorization to protect your API endpoints. This is critical!
* **Consider pagination** for the `/devices`, `/devices/{device_id}/logs`, and `/devices/{device_id}/usage` endpoints to handle large datasets.
* **Data Validation:** Implement data validation on the backend to prevent invalid data from being stored in the database (e.g. incorrect date formats, non-numeric quantities).
* **Calculate `duration_seconds`:** When creating or updating a `device_usage` record, automatically calculate `duration_seconds` based on `start_time` and `end_time`.
**Step 3: Frontend Components (UI Logic)**
Assume React for the frontend.
```jsx
// DeviceList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // Or your preferred HTTP client
import DeviceDetails from './DeviceDetails';
const DeviceList = () => {
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState(null);
useEffect(() => {
fetchDevices();
}, []);
const fetchDevices = async () => {
try {
const response = await axios.get('/devices'); // Replace with your API endpoint
setDevices(response.data);
} catch (error) {
console.error('Error fetching devices:', error);
}
};
const handleDeviceClick = (deviceId) => {
setSelectedDevice(deviceId);
};
return (
<div>
<h2>Devices</h2>
<ul>
{devices.map(device => (
<li key={device.id} onClick={() => handleDeviceClick(device.id)}>
{device.name} ({device.type}) - Status: {device.status}
</li>
))}
</ul>
{selectedDevice && <DeviceDetails deviceId={selectedDevice} />}
</div>
);
};
export default DeviceList;
// DeviceDetails.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const DeviceDetails = ({ deviceId }) => {
const [device, setDevice] = useState(null);
const [logs, setLogs] = useState([]);
const [usage, setUsage] = useState([]);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({});
useEffect(() => {
fetchDeviceDetails();
fetchDeviceLogs();
fetchDeviceUsage();
}, [deviceId]);
const fetchDeviceDetails = async () => {
try {
const response = await axios.get(`/devices/${deviceId}`);
setDevice(response.data);
setFormData(response.data); // Initialize form data
} catch (error) {
console.error('Error fetching device details:', error);
}
};
const fetchDeviceLogs = async () => {
try {
const response = await axios.get(`/devices/${deviceId}/logs`);
setLogs(response.data);
} catch (error) {
console.error('Error fetching device logs:', error);
}
};
const fetchDeviceUsage = async () => {
try {
const response = await axios.get(`/devices/${deviceId}/usage`);
setUsage(response.data);
} catch (error) {
console.error('Error fetching device usage:', error);
}
};
const handleInputChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleUpdateDevice = async () => {
try {
await axios.put(`/devices/${deviceId}`, formData);
setEditMode(false);
fetchDeviceDetails(); // Refresh details
} catch (error) {
console.error('Error updating device:', error);
}
};
const renderLogItem = (log) => (
<div key={log.id}>
<strong>{log.timestamp}:</strong> {log.event_type} - {log.message}
</div>
);
const renderUsageItem = (usageItem) => (
<div key={usageItem.id}>
<strong>Start:</strong> {usageItem.start_time}, <strong>End:</strong> {usageItem.end_time || 'N/A'},
<strong>Material:</strong> {usageItem.material_used || 'N/A'}, <strong>Quantity:</strong> {usageItem.quantity_used || 'N/A'}
<strong>Notes:</strong> {usageItem.notes || 'N/A'}
</div>
);
if (!device) {
return <div>Loading device details...</div>;
}
return (
<div>
<h3>Device Details: {device.name}</h3>
{editMode ? (
<div>
<label>Name:</label>
<input type="text" name="name" value={formData.name} onChange={handleInputChange} /><br />
<label>Type:</label>
<input type="text" name="type" value={formData.type} onChange={handleInputChange} /><br />
<label>Manufacturer:</label>
<input type="text" name="manufacturer" value={formData.manufacturer} onChange={handleInputChange} /><br />
<label>Model:</label>
<input type="text" name="model" value={formData.model} onChange={handleInputChange} /><br />
<label>Serial Number:</label>
<input type="text" name="serial_number" value={formData.serial_number} onChange={handleInputChange} /><br />
<label>IP Address:</label>
<input type="text" name="ip_address" value={formData.ip_address} onChange={handleInputChange} /><br />
<label>Connection Type:</label>
<input type="text" name="connection_type" value={formData.connection_type} onChange={handleInputChange} /><br />
<label>Status:</label>
<input type="text" name="status" value={formData.status} onChange={handleInputChange} /><br />
<button onClick={handleUpdateDevice}>Update</button>
<button onClick={() => setEditMode(false)}>Cancel</button>
</div>
) : (
<div>
<p>Name: {device.name}</p>
<p>Type: {device.type}</p>
<p>Manufacturer: {device.manufacturer}</p>
<p>Model: {device.model}</p>
<p>Serial Number: {device.serial_number}</p>
<p>IP Address: {device.ip_address}</p>
<p>Connection Type: {device.connection_type}</p>
<p>Status: {device.status}</p>
<button onClick={() => setEditMode(true)}>Edit</button>
</div>
)}
<h4>Logs:</h4>
<div>{logs.map(renderLogItem)}</div>
<h4>Usage:</h4>
<div>{usage.map(renderUsageItem)}</div>
</div>
);
};
export default DeviceDetails;
```
**Frontend Component Summary:**
* **`DeviceList.jsx`**:
* Fetches and displays a list of devices.
* Handles selecting a device to view details.
* **`DeviceDetails.jsx`**:
* Fetches and displays details for a selected device.
* Fetches and displays device logs and usage records.
* Implements an edit mode for updating device information.
**Prompt Instructions:**
* **Implement these React components.** Adapt the code to your project structure and styling.
* **Replace placeholder API endpoints** with your actual API URLs.
* **Implement a form for creating new device log entries and usage records.** Add POST requests to the appropriate endpoints.
* **Improve the UI:** Add styling, error handling, loading indicators, and better data formatting.
* **Real-time Updates:** Consider using WebSockets or Server-Sent Events (SSE) for real-time updates on device status and logs. This is crucial for a responsive and informative UI. This feature is *highly* recommended.
* **Date/Time Formatting:** Use a library like `moment.js` or `date-fns` to properly format and display dates and times.
* **Device Status Indicators:** Use visual cues (e.g., color-coded icons) to indicate device status in the `DeviceList`.
* **Error Handling:** Display user-friendly error messages when API requests fail.
* **Form Validation:** Implement client-side form validation to prevent invalid data from being sent to the server.
**Step 4: Device Communication (Device Integration - Core Challenge)**
This is the most challenging part. How will your application communicate with the lab equipment?
**Potential Approaches:**
1. **Direct Network Communication (Recommended):**
* If the devices have network interfaces, you can use protocols like:
* **HTTP/REST:** If the device exposes a REST API.
* **TCP/IP Sockets:** For custom protocols. You'll need to understand the device's communication protocol.
* **MQTT:** A lightweight messaging protocol suitable for IoT devices. Your devices would publish status updates to an MQTT broker.
2. **Middleware/Agent:**
* Develop a small "agent" application that runs on a computer connected to the devices (e.g., Raspberry Pi). This agent would:
* Communicate with the devices using USB, serial, or other local interfaces.
* Send status updates and data to your Dental Lab Manager application via HTTP or MQTT.
3. **File-Based Integration:**
* The devices might generate log files or status files.
* Your application can periodically scan these files to extract information. This is the least desirable approach as it's less real-time.
**Implementation Notes:**
* **Device-Specific Code:** You'll likely need to write device-specific code to handle the unique communication protocols of each type of lab equipment.
* **Asynchronous Operations:** Device communication should be handled asynchronously to avoid blocking the main application thread.
* **Error Handling:** Robust error handling is essential to deal with communication failures and device errors.
* **Security:** Implement security measures to protect against unauthorized access to devices.
**Prompt Instructions:**
* **Research the communication protocols** of the specific lab equipment you need to integrate with.
* **Choose the most appropriate communication approach** based on the capabilities of the devices.
* **Implement the device communication logic** in your backend. This might involve writing custom network clients, parsing device-specific data formats, and handling errors.
* **Update the `devices.status` and create `device_logs` entries** based on the information received from the devices.
* **Start by implementing a "heartbeat" mechanism:** Have each device (or the agent) periodically send a "I'm alive" message to the server. Update the `devices.last_heartbeat` field. If a device hasn't sent a heartbeat in a while, mark its status as "Offline" or "Error".
* **Simulate Device Data:** If you don't have access to real lab equipment, simulate device data to test your integration logic.
**Example (Simplified - Using Python and Sockets for a Hypothetical Device Protocol):**
```python
# (This code would run on the backend server, potentially in a separate thread)
import socket
import threading
import datetime
def handle_device_connection(conn, addr):
print(f"Connected by {addr}")
try:
while True:
data = conn.recv(1024) # Receive up to 1024 bytes
if not data:
break # Client disconnected
message = data.decode('utf-8').strip()
print(f"Received: {message}")
# Example: Assume the device sends messages like "STATUS:Running" or "LOG:Job Finished"
if message.startswith("STATUS:"):
status = message[7:] # Extract the status
# Update the device status in the database
update_device_status(addr[0], status) # IP address as identifier
elif message.startswith("LOG:"):
log_message = message[4:]
# Create a new device log entry in the database
create_device_log_entry(addr[0], "Info", log_message) # IP address as identifier
conn.sendall(b"OK\n") # Send an acknowledgment
except Exception as e:
print(f"Error: {e}")
finally:
print(f"Connection closed with {addr}")
conn.close()
def update_device_status(ip_address, status):
# (Database update logic - use the get_db_connection() function from above)
conn = get_db_connection()
try:
conn.execute("UPDATE devices SET status = ?, last_heartbeat = ? WHERE ip_address = ?", (status, datetime.datetime.now(), ip_address))
conn.commit()
except Exception as e:
print(f"Database error: {e}")
finally:
conn.close()
def create_device_log_entry(ip_address, event_type, message):
# (Database insert logic - use the get_db_connection() function from above)
conn = get_db_connection()
try:
# Get the device ID based on IP address
cursor = conn.execute("SELECT id FROM devices WHERE ip_address = ?", (ip_address,))
device_id_row = cursor.fetchone()
if device_id_row:
device_id = device_id_row['id']
conn.execute("INSERT INTO device_logs (device_id, event_type, message) VALUES (?, ?, ?)", (device_id, event_type, message))
conn.commit()
else:
print(f"Device with IP address {ip_address} not found.")
except Exception as e:
print(f"Database error: {e}")
finally:
conn.close()
def start_socket_server():
HOST = '0.0.0.0' # Listen on all interfaces
PORT = 65432 # Port to listen on (adjust as needed)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f"Listening on {HOST}:{PORT}")
while True:
conn, addr = s.accept()
thread = threading.Thread(target=handle_device_connection, args=(conn, addr))
thread.start()
# Start the socket server in a separate thread when your Flask app starts
if __name__ == '__main__':
threading.Thread(target=start_socket_server, daemon=True).start()
app.run(debug=True)
```
**Important Considerations:**
* **Scalability:** If you expect to manage a large number of devices, consider using a message queue (e.g., RabbitMQ, Kafka) to handle device communication more efficiently.
* **Device Discovery:** Implement a mechanism for automatically discovering new devices on the network. Bonjour/mDNS or UPnP could be options.
* **Configuration:** Allow users to configure device connection settings (IP address, port, serial port settings, etc.) through the UI.
This prompt provides a detailed roadmap for implementing the "Device & Machine Integration" feature. Remember to adapt the code examples and instructions to your specific technology stack and requirements. Good luck!
```
AI-Powered Design Suggestion & Error Detection
Leverage AI to analyze digital impressions and order specifi...
Generated Prompt
Predictive Maintenance for Lab Equipment
Integrate sensor data from lab equipment (3D printers, milli...
Generated Prompt
Augmented Reality (AR) Visualization for Dentists
Enable dentists to visualize the designed prosthetic in the ...
Generated Prompt
Master Coding Prompt
Customize Your Prompt
Final Output
# Dental Lab Manager MVP - Master Coding Prompt
## Overview
This prompt outlines the development of an MVP for a Dental Lab Manager application, targeting small to medium-sized dental labs. The application will be cloud-based and focus on affordability, intelligent scheduling, and simplified case tracking.
## Tech Stack
**Recommendation: Next.js (Frontend) with a Python/Flask API (Backend) and PostgreSQL (Database)**
* **Frontend:** Next.js with TypeScript, Tailwind CSS, and possibly Material UI or Ant Design for UI components.
* **Backend:** Python 3.9+ with Flask, Flask-RESTful, SQLAlchemy (ORM), and gunicorn for deployment.
* **Database:** PostgreSQL (cloud-hosted, e.g., AWS RDS, Google Cloud SQL, or Heroku Postgres).
* **Deployment:** Vercel (Frontend) and Heroku or AWS Elastic Beanstalk (Backend).
**Reasoning:**
* Next.js offers excellent performance, SEO benefits, and a great developer experience for building interactive UIs. The use of TypeScript adds type safety and improves code maintainability.
* Flask is a lightweight and flexible framework suitable for building a REST API. Python is easy to learn and has a rich ecosystem of libraries. SQLAlchemy simplifies database interactions.
* PostgreSQL is a robust and scalable open-source database. Its advanced features and reliability make it a great choice for this application.
## Core Features (Based on USPs)
1. **User Authentication & Authorization:**
* Secure user login/registration (email/password).
* Role-based access control (Admin, Lab Manager, Technician).
2. **Case Management:**
* Create, read, update, and delete dental cases.
* Case details: Patient name, Doctor name, Doctor email, Case type (crown, bridge, etc.), Due date, Materials, Special instructions, Status (New, In Progress, Completed, Shipped).
* File upload: Allow uploading of case files (e.g., STL, images).
* Case Status Tracking: Drag-and-drop interface for moving cases through different stages (e.g., Received, Model Work, Wax-up, Finishing, Shipping). Provide notifications when cases progress through stages.
3. **Intelligent Scheduling & Staff Management:**
* Technician profiles: Skillsets, Availability, Current workload.
* AI-assisted scheduling: Suggest optimal technician assignments based on case requirements, technician skills, and availability. (Initial MVP can use a simple algorithm that prioritizes underutilized technicians.)
* Time tracking: Clock in/out functionality for technicians. Simple report generation based on time tracked.
* Technician performance reports: Track case completion times and error rates. (Basic metrics for MVP).
* Communication tools: Simple in-app messaging system for communication between lab members.
4. **Reporting:**
* Case summary reports: Overview of all cases, cases by status, cases due this week/month.
* Technician workload reports: Overview of assigned cases and hours worked.
## Database Schema (PostgreSQL)
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'technician', -- admin, lab_manager, technician
name VARCHAR(255),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() at time zone 'utc')
);
CREATE TABLE technicians (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
skills TEXT[], -- Array of skills (e.g., ['Crowns', 'Bridges', 'Implants'])
availability TEXT, -- JSON representing weekly availability
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() at time zone 'utc')
);
CREATE TABLE cases (
id SERIAL PRIMARY KEY,
patient_name VARCHAR(255) NOT NULL,
doctor_name VARCHAR(255) NOT NULL,
doctor_email VARCHAR(255) NOT NULL,
case_type VARCHAR(255) NOT NULL,
due_date DATE NOT NULL,
materials TEXT,
special_instructions TEXT,
status VARCHAR(50) NOT NULL DEFAULT 'new', -- new, in_progress, completed, shipped
assigned_technician_id INTEGER REFERENCES technicians(id),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() at time zone 'utc')
);
CREATE TABLE case_files (
id SERIAL PRIMARY KEY,
case_id INTEGER REFERENCES cases(id),
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(255) NOT NULL, -- Store file path in cloud storage (e.g., AWS S3)
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() at time zone 'utc')
);
CREATE TABLE time_entries (
id SERIAL PRIMARY KEY,
technician_id INTEGER REFERENCES technicians(id),
start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
end_time TIMESTAMP WITHOUT TIME ZONE,
case_id INTEGER REFERENCES cases(id)
);
```
## Key UI/UX Elements
* **Dashboard:** Overview of key metrics (number of new cases, cases due soon, technician workload).
* **Case Management View:** Table with filter and sorting capabilities. Detailed case view with all information and file attachments.
* **Scheduling View:** Calendar or Gantt chart style view showing technician assignments and workload.
* **Technician Profile:** Editable profile with skills and availability.
* **Intuitive Drag-and-Drop Interface:** Used for case status updates and technician scheduling.
* **Responsive Design:** Ensure the application works well on desktop and mobile devices.
## Backend API Endpoints (Flask)
* `/auth/register` (POST): Register a new user.
* `/auth/login` (POST): Log in an existing user.
* `/cases` (GET, POST): Get all cases, create a new case.
* `/cases/<case_id>` (GET, PUT, DELETE): Get, update, or delete a specific case.
* `/technicians` (GET): Get all technicians.
* `/technicians/<technician_id>` (GET): Get a specific technician.
* `/schedule` (GET): Get the schedule for a given date range.
* `/time-entries` (POST, GET): Clock in/out, Get time entries for a technician
## Frontend Components (Next.js)
* `Login.tsx`
* `Register.tsx`
* `Dashboard.tsx`
* `CaseList.tsx`
* `CaseDetail.tsx`
* `SchedulingCalendar.tsx`
* `TechnicianProfile.tsx`
## Development Process
1. **Set up the Backend (Flask API):**
* Create the Flask application and database connection.
* Implement the database schema using SQLAlchemy.
* Develop the API endpoints for user authentication, case management, technician management, and scheduling.
* Implement authentication and authorization middleware.
2. **Set up the Frontend (Next.js):**
* Create the Next.js application and configure routing.
* Implement the UI components based on the design mockups.
* Connect the frontend to the backend API using `fetch` or `axios`.
* Implement user authentication and authorization logic.
3. **Implement Core Features:**
* Start with Case Management, then move on to Scheduling and Staff Management.
* Focus on delivering the core functionality of each feature before adding advanced options.
4. **Testing:**
* Write unit tests for the backend API using `pytest`.
* Write integration tests for the frontend components using `Jest` and `React Testing Library`.
5. **Deployment:**
* Deploy the frontend to Vercel and the backend to Heroku or AWS Elastic Beanstalk.
## Future Enhancements
* Advanced scheduling algorithms (e.g., using optimization techniques).
* Integration with dental practice management systems.
* Inventory management.
* Automated invoice generation.
* More detailed reporting and analytics.