Compare commits
355 Commits
v2025-07-3
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e79e8d3a | ||
|
|
c9125e5b75 | ||
|
|
dadbfb56a4 | ||
|
|
f96a408852 | ||
|
|
8afe2eedee | ||
|
|
4faea51004 | ||
|
|
70ea752992 | ||
|
|
492889b9e1 | ||
|
|
ea54dc5471 | ||
|
|
8ff431ca10 | ||
|
|
7d5fe84b3a | ||
|
|
f287eb63f6 | ||
|
|
565d88def8 | ||
|
|
92feb05a0d | ||
|
|
013413a01c | ||
|
|
9e7f8ebd1f | ||
|
|
07d657002e | ||
|
|
24520d1f01 | ||
|
|
6b8202992d | ||
|
|
bc0755b9bf | ||
|
|
cb61374582 | ||
|
|
cb70222c04 | ||
|
|
dfcd22fadf | ||
|
|
7277c6ab34 | ||
|
|
fbf0a8c9e4 | ||
|
|
ee219e1d96 | ||
|
|
90b8223385 | ||
|
|
0bc662dbde | ||
|
|
15df8d12fe | ||
|
|
a9cdfb567a | ||
|
|
f0a6f3b6b3 | ||
|
|
8802952e5a | ||
|
|
ab4a05bc7f | ||
|
|
648854190e | ||
|
|
13a4367c92 | ||
|
|
2258e74960 | ||
|
|
fd63885507 | ||
|
|
4b88679b37 | ||
|
|
dd9fda10f7 | ||
|
|
3dbaa9bd33 | ||
|
|
3402183f3c | ||
|
|
1bc9aa5295 | ||
|
|
c946dad334 | ||
|
|
a32aa89a56 | ||
|
|
4d1952d998 | ||
|
|
5fe308eac6 | ||
|
|
ad46651ed8 | ||
|
|
0eb519dea4 | ||
|
|
9b0d33710f | ||
|
|
28a0fced87 | ||
|
|
60a7649c36 | ||
|
|
cca21ac3d3 | ||
|
|
78c0abf92d | ||
|
|
d40d600a49 | ||
|
|
0e1ab0c619 | ||
|
|
0366f62dfb | ||
|
|
33594b2508 | ||
|
|
db9626aa7b | ||
|
|
ba022dea3c | ||
|
|
f0e32b4ad0 | ||
|
|
bbbc8b1d63 | ||
|
|
af2f642d45 | ||
|
|
9f00b97677 | ||
|
|
196245ffa0 | ||
|
|
7cf1bf40c7 | ||
|
|
860f6019ad | ||
|
|
4fefdcaf3d | ||
|
|
9a22545ec2 | ||
|
|
49a021b9dd | ||
|
|
0cd6048bf2 | ||
|
|
1bec976efc | ||
|
|
a03a224cda | ||
|
|
0bdd3ba8b1 | ||
|
|
96b3c60568 | ||
|
|
811fabfced | ||
|
|
a3d9278d6f | ||
|
|
4e67381cf0 | ||
|
|
f1593de431 | ||
|
|
889722451c | ||
|
|
bb9c1ee7d3 | ||
|
|
fc2e4d27d2 | ||
|
|
6bde2fb2b8 | ||
|
|
7a510329ba | ||
|
|
a5cc4e7cc7 | ||
|
|
c77ac5c264 | ||
|
|
f14d19f59a | ||
|
|
fccd9308e2 | ||
|
|
2265ad28f4 | ||
|
|
b1993ba83a | ||
|
|
d00d4c7af2 | ||
|
|
4d175d9aa1 | ||
|
|
b0fa3d0844 | ||
|
|
617711fb1a | ||
|
|
f46ec17c03 | ||
|
|
de163f7f9b | ||
|
|
3250669dc9 | ||
|
|
d00d004dd8 | ||
|
|
b38d470b02 | ||
|
|
0f2a894edb | ||
|
|
ad71ceae21 | ||
|
|
6e3e809435 | ||
|
|
4719c346f5 | ||
|
|
2e070ea7fd | ||
|
|
5f573c49be | ||
|
|
9524c9e3f3 | ||
|
|
b3a6a19f95 | ||
|
|
332b39aa23 | ||
|
|
14ee3ab4a0 | ||
|
|
75ce2aa207 | ||
|
|
48b68eff83 | ||
|
|
3124540f4f | ||
|
|
42728ab445 | ||
|
|
af5141099d | ||
|
|
1ce4207294 | ||
|
|
9d3d6bc363 | ||
|
|
74dc7fe404 | ||
|
|
2f3efbcf66 | ||
|
|
b3ff2a7a9d | ||
|
|
2b4e28eea6 | ||
|
|
f7b3d95526 | ||
|
|
553f43c7b3 | ||
|
|
8b1185b507 | ||
|
|
eecab547df | ||
|
|
b069f7db61 | ||
|
|
424d767549 | ||
|
|
4f3d16326c | ||
|
|
ac68228e26 | ||
|
|
673d64e720 | ||
|
|
ec7746f79f | ||
|
|
6ceae4f1d5 | ||
|
|
c50c29f743 | ||
|
|
34f61777c3 | ||
|
|
9c81cdb1e0 | ||
|
|
bdd880bc24 | ||
|
|
aa01eaeaa5 | ||
|
|
fc1aa567bc | ||
|
|
cc3d694ce3 | ||
|
|
f11760d867 | ||
|
|
fc4170acb8 | ||
|
|
98bda7db5d | ||
|
|
3bfbdc11af | ||
|
|
a59a26fa29 | ||
|
|
a5de1b3855 | ||
|
|
0624103778 | ||
|
|
cd6e4e8b64 | ||
|
|
fb9d574ff4 | ||
|
|
d27b3ec90c | ||
|
|
7d5d631b05 | ||
|
|
f13218e0b1 | ||
|
|
ac12dc627d | ||
|
|
c550812deb | ||
|
|
9efb05bc1e | ||
|
|
db244e2953 | ||
|
|
a3098a15f2 | ||
|
|
12b90e3e1e | ||
|
|
1763de12bd | ||
|
|
92dc877942 | ||
|
|
841f063695 | ||
|
|
e6301bfb64 | ||
|
|
7e1c6f1bf8 | ||
|
|
5adff05283 | ||
|
|
6f3108134b | ||
|
|
d5650b2e3b | ||
|
|
e3b062d9ea | ||
|
|
6bd797e5bb | ||
|
|
ac3b43361c | ||
|
|
b9d3c5a10d | ||
|
|
d9b379ec42 | ||
|
|
1f3ec25f32 | ||
|
|
2458082968 | ||
|
|
5f59f9f0c0 | ||
|
|
28ffd17bed | ||
|
|
e2446de20f | ||
|
|
c33b9e6ace | ||
|
|
73cb8d5614 | ||
|
|
69c981de3c | ||
|
|
bdc5c42cde | ||
|
|
5588c94ec3 | ||
|
|
42310c1f55 | ||
|
|
08431defbb | ||
|
|
52c15e4863 | ||
|
|
bab0ebc858 | ||
|
|
b64a9bc78b | ||
|
|
e9342b6fec | ||
|
|
b2617f605f | ||
|
|
c28aaecc66 | ||
|
|
e3d4998d80 | ||
|
|
be9c87790d | ||
|
|
e243f27b70 | ||
|
|
704241335e | ||
|
|
8406bd02c2 | ||
|
|
8a13e6e71b | ||
|
|
34b27b15ba | ||
|
|
14c081a615 | ||
|
|
81d200899d | ||
|
|
056f705f25 | ||
|
|
e084d42eb3 | ||
|
|
b4700f46fd | ||
|
|
369eb040af | ||
|
|
d21fae5052 | ||
|
|
1bc4364084 | ||
|
|
eebca7752b | ||
|
|
a42ad8d9ad | ||
|
|
63546605f2 | ||
|
|
357166b159 | ||
|
|
8866c68cee | ||
|
|
df46f378d7 | ||
|
|
b00b503d4c | ||
|
|
1575b61051 | ||
|
|
7d5667451c | ||
|
|
dbddd70ef9 | ||
|
|
027669cfd6 | ||
|
|
693f5c0af9 | ||
|
|
d596edc107 | ||
|
|
eeb540f269 | ||
|
|
a9b43a7f7d | ||
|
|
ba3cc7424d | ||
|
|
ed2148be8d | ||
|
|
36e3eaf958 | ||
|
|
77600a45d5 | ||
|
|
4988bdff1e | ||
|
|
8accafc599 | ||
|
|
bd9db0df22 | ||
|
|
7794d53802 | ||
|
|
014bc50690 | ||
|
|
ad93d958f9 | ||
|
|
c040215bb6 | ||
|
|
0132c88ce7 | ||
|
|
a74280f087 | ||
|
|
ccefabbc4a | ||
|
|
e29c02ebe5 | ||
|
|
2f1b036c3f | ||
|
|
f480dd5491 | ||
|
|
d4d4fd4b5f | ||
|
|
0feba6a09b | ||
|
|
4ba8141675 | ||
|
|
d71fca510f | ||
|
|
4303a02c1a | ||
|
|
afa224cbdc | ||
|
|
8de9af66e2 | ||
|
|
d13c66aa01 | ||
|
|
faf1d112c9 | ||
|
|
138af3a364 | ||
|
|
e0470d0dd1 | ||
|
|
ab75e3089e | ||
|
|
bfce6f32b6 | ||
|
|
737c0bd65f | ||
|
|
93a78a9d97 | ||
|
|
415334ca4a | ||
|
|
341119988c | ||
|
|
2567b65ff2 | ||
|
|
69458ecc97 | ||
|
|
7a391c338a | ||
|
|
9e8ba5a2e9 | ||
|
|
4f56f06208 | ||
|
|
c42ebadf2d | ||
|
|
62d1cafef9 | ||
|
|
6ec7a5feb1 | ||
|
|
a02f1cb588 | ||
|
|
aa3f312047 | ||
|
|
0fbc7cad39 | ||
|
|
293f57133b | ||
|
|
9ceb62dff0 | ||
|
|
7cb0fdea76 | ||
|
|
8820e16974 | ||
|
|
58e0c55480 | ||
|
|
7c7cec6ac8 | ||
|
|
8316377344 | ||
|
|
830fdd3206 | ||
|
|
9eadb04a93 | ||
|
|
cf0ecc4d27 | ||
|
|
6d1b3fa97e | ||
|
|
e4b9ebe7a4 | ||
|
|
416188a572 | ||
|
|
a8e496e78a | ||
|
|
20553d4c99 | ||
|
|
618ac4e6db | ||
|
|
3dd674b172 | ||
|
|
919ff298ba | ||
|
|
0599756b63 | ||
|
|
5b8641680d | ||
|
|
2b15258bd9 | ||
|
|
3ad83a523d | ||
|
|
cdd80eb4be | ||
|
|
f5aa8c4366 | ||
|
|
97c97b561f | ||
|
|
975ed0426c | ||
|
|
c3475e707b | ||
|
|
8321ba9373 | ||
|
|
c81202edf9 | ||
|
|
24a82911b6 | ||
|
|
3fcdc49502 | ||
|
|
1beadb739b | ||
|
|
846c0e8898 | ||
|
|
1ea087e683 | ||
|
|
096042e7c1 | ||
|
|
cb5e481c4d | ||
|
|
e91ac0c719 | ||
|
|
45f6cf82d5 | ||
|
|
0cccaafb38 | ||
|
|
b90bbcb8c2 | ||
|
|
ae9fcf4887 | ||
|
|
1ebc7079eb | ||
|
|
ea8726e0d6 | ||
|
|
83125bcf96 | ||
|
|
8823159a30 | ||
|
|
ad458de818 | ||
|
|
d2a11d65a7 | ||
|
|
1d8ddd8d8b | ||
|
|
9de720c920 | ||
|
|
0c4c9df396 | ||
|
|
8fc2e96985 | ||
|
|
852f9eddc9 | ||
|
|
d51e57fcee | ||
|
|
1be3885c70 | ||
|
|
52e6e3c09d | ||
|
|
310696a4c6 | ||
|
|
505f591839 | ||
|
|
b07dcd8683 | ||
|
|
fe5c54ff3e | ||
|
|
09fa34007b | ||
|
|
57171963be | ||
|
|
881dfedb14 | ||
|
|
1cc6e512ab | ||
|
|
0b748e45f8 | ||
|
|
e6688bf74c | ||
|
|
cbd5351981 | ||
|
|
31007d6979 | ||
|
|
7fb2d5f67f | ||
|
|
9b9bd730dd | ||
|
|
2e438c4b9e | ||
|
|
6ed2922ba4 | ||
|
|
b0ff7dd456 | ||
|
|
43dfa414aa | ||
|
|
1d73f3c427 | ||
|
|
72fba9e976 | ||
|
|
84719e4c6b | ||
|
|
9131d25dd5 | ||
|
|
c65b1b25a4 | ||
|
|
e8a9cff2ec | ||
|
|
d39f92554a | ||
|
|
84cc070e68 | ||
|
|
cf553295bb | ||
|
|
23c4122afb | ||
|
|
7414e3d6de | ||
|
|
5f2ef09672 | ||
|
|
f2588fc50f | ||
|
|
0ea230e99b | ||
|
|
a83be0f7c7 | ||
|
|
ed5755b005 | ||
|
|
0fcd6ccaba | ||
|
|
1ee951fc9f | ||
|
|
325465f9a5 | ||
|
|
7dee605d95 | ||
|
|
645e8c194a |
@@ -1,47 +0,0 @@
|
||||
name: Build server
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*-*-*_*"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: '${{ secrets.TOKEN_PULL }}'
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.13.1'
|
||||
- name: "Build app"
|
||||
run: "python3 build.py"
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
# Upload built files
|
||||
name: 'TeleSec-dist'
|
||||
path: ./dist
|
||||
- uses: mdallasanta/ssh-scp-deploy@v1.2.0
|
||||
with:
|
||||
local: './dist/' # Local file path - REQUIRED false - DEFAULT ./
|
||||
remote: '${{ secrets.FOLDER }}' # Remote file path - REQUIRED false - DEFAULT ~/
|
||||
host: ${{secrets.HOST}} # Remote server address - REQUIRED true
|
||||
port: 22 # Remote server port - REQUIRED false - DEFAULT 22
|
||||
user: ${{secrets.USER}} # Remote server user - REQUIRED true
|
||||
key: ${{secrets.KEY}} # Remote server private key - REQUIRED at least one of "password" or "key"
|
||||
- name: "zip-it-up"
|
||||
run: "zip -r TeleSec-dist.zip ./dist"
|
||||
- name: Upload as release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |-
|
||||
TeleSec-dist.zip
|
||||
api_key: '${{secrets.TOKEN_PUSH_PLUS}}'
|
||||
224
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
# TeleSec - Secure Distributed Communication Application
|
||||
|
||||
TeleSec is a Spanish Progressive Web Application (PWA) built with vanilla JavaScript, HTML, and CSS that provides secure group communication using a local-first PouchDB datastore with optional CouchDB replication for syncing. The application allows users to join encrypted communication groups using group codes and secret keys.
|
||||
|
||||
**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**
|
||||
|
||||
## Working Effectively
|
||||
|
||||
### Build and Deploy Process
|
||||
- **Build the application (FASTEST BUILD EVER - 0.036 seconds):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec`
|
||||
- `python3 build.py`
|
||||
- **NEVER CANCEL**: Build completes in under 0.1 seconds. No timeout needed.
|
||||
- The build script copies files from `assets/` to `dist/` and processes template variables in `src/` files.
|
||||
|
||||
### Serving the Application
|
||||
- **Python HTTP Server (Recommended for development):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec/dist`
|
||||
- `python3 -m http.server 8000`
|
||||
- Access at: `http://localhost:8000`
|
||||
|
||||
- **Node.js HTTP Server (Alternative with CORS support):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec/dist`
|
||||
- `npx http-server . --port 8001 --cors`
|
||||
- **NEVER CANCEL**: First run takes ~14 seconds to download http-server package. Set timeout to 30+ seconds.
|
||||
- Access at: `http://localhost:8001`
|
||||
|
||||
### Development Environment Requirements
|
||||
- **Python 3.x** (for build script) - Version 3.12.3+ confirmed working
|
||||
- **Node.js and npm** (optional, for alternative serving) - Version 20.19.4+ confirmed working
|
||||
- **Web browser** (for testing the PWA functionality)
|
||||
|
||||
## Validation and Testing
|
||||
|
||||
### Mandatory Validation Steps
|
||||
1. **Build Validation:**
|
||||
- Run `python3 build.py` and verify it completes in under 1 second
|
||||
- Verify `dist/` directory is created with all assets and processed files
|
||||
- Check that template variables (%%PREFETCH%%, %%VERSIONCO%%, %%ASSETSJSON%%) are replaced
|
||||
|
||||
2. **Application Functionality Test:**
|
||||
- Start web server: `python3 -m http.server 8000` in `dist/` directory
|
||||
- Navigate to `http://localhost:8000` in browser
|
||||
- **CRITICAL LOGIN TEST:** Enter any group code (e.g., "TEST") and secret key (e.g., "SECRET123")
|
||||
- Click "Iniciar sesión" button
|
||||
- **VERIFY NETWORK CONNECTIVITY:** Confirm the header shows connected nodes (e.g., "TeleSec - TEST - (8 nodos)")
|
||||
- **SUCCESS INDICATORS:**
|
||||
- Application loads without errors
|
||||
- Login form accepts credentials
|
||||
- Distributed network connects (node count > 0)
|
||||
- No JavaScript console errors except expected WebSocket connection failures
|
||||
|
||||
3. **PWA Features Test:**
|
||||
- Verify Service Worker registration in browser console
|
||||
- Check manifest.json loads correctly
|
||||
- Confirm offline caching functionality
|
||||
|
||||
### Error Handling Validation
|
||||
- **Build Script Errors:** Python syntax errors will cause build to fail with clear error messages
|
||||
- **Network Connectivity:** Some WebSocket connections to gun-manhattan.herokuapp.com may fail - this is expected
|
||||
- **Browser Compatibility:** Application works in modern browsers supporting Service Workers
|
||||
|
||||
## Repository Structure
|
||||
|
||||
### Key Files and Directories
|
||||
```
|
||||
/home/runner/work/TeleSec/TeleSec/
|
||||
├── build.py # Main build script - processes template variables
|
||||
├── index.html # Build error fallback (should never be served)
|
||||
├── README.md # Basic repository information (minimal)
|
||||
├── LICENSE # Project license
|
||||
├── CNAME # GitHub Pages configuration
|
||||
├── .gitignore # Excludes dist/, radata/, node_modules/
|
||||
├── src/ # Source files with template variables
|
||||
│ ├── index.html # Main application HTML with %%PREFETCH%% variables
|
||||
│ ├── app_logic.js # Core application logic and authentication
|
||||
│ ├── app_modules.js # Application modules and utilities
|
||||
│ ├── config.js # Configuration and CouchDB setup
|
||||
│ ├── db.js # PouchDB wrapper and replication
|
||||
│ ├── pwa.js # Progressive Web App functionality
|
||||
│ ├── sw.js # Service Worker with cache configuration
|
||||
│ └── page/ # Individual page modules
|
||||
│ ├── login.js # Login functionality
|
||||
│ ├── index.js # Main dashboard
|
||||
│ ├── materiales.js # Materials management
|
||||
│ ├── personas.js # People management
|
||||
│ ├── supercafe.js # SuperCafé module
|
||||
│ ├── comedor.js # Dining hall module
|
||||
│ ├── importar.js # Data import functionality
|
||||
│ ├── exportar.js # Data export functionality
|
||||
│ ├── resumen_diario.js # Daily summary
|
||||
│ └── notificaciones.js # Notifications
|
||||
└── assets/ # Static assets copied to dist/
|
||||
├── manifest.json # PWA manifest
|
||||
├── *.png, *.jpg # Icons and images
|
||||
├── static/ # JavaScript libraries and CSS
|
||||
│ │ ├── pouchdb (via CDN) # PouchDB is used for local storage and replication
|
||||
│ ├── webrtc.js # WebRTC functionality
|
||||
│ ├── euskaditech-css/ # CSS framework
|
||||
│ └── ico/ # Application icons
|
||||
└── page/ # Page-specific assets (empty placeholder)
|
||||
```
|
||||
|
||||
### Build Process Details
|
||||
The `build.py` script performs these operations:
|
||||
1. **Clean:** Removes existing `dist/` directory (if it exists)
|
||||
2. **Copy Assets:** Copies all files from `assets/` to `dist/`
|
||||
3. **Process Templates:** Processes files from `src/` and replaces:
|
||||
- `%%PREFETCH%%` - Generates link prefetch tags for all assets
|
||||
- `%%VERSIONCO%%` - Inserts version code "2025-08-04_1"
|
||||
- `%%ASSETSJSON%%` - Inserts JSON array of all asset files
|
||||
4. **Output:** Creates complete deployable application in `dist/`
|
||||
|
||||
## Application Architecture
|
||||
|
||||
### Technology Stack
|
||||
- **Frontend:** Vanilla JavaScript, HTML5, CSS3
|
||||
- **Data Layer:** PouchDB (local-first) with optional CouchDB replication
|
||||
- **Networking:** WebRTC for peer-to-peer connections (where applicable)
|
||||
- **Authentication:** Group codes + secret keys (converted to uppercase)
|
||||
- **Storage:** Browser LocalStorage + PouchDB local datastore
|
||||
- **PWA Features:** Service Worker, Web App Manifest
|
||||
|
||||
### Remote Sync (Optional)
|
||||
The application can optionally replicate to a remote CouchDB server for cloud backup and multi-device syncing. Configure the CouchDB server, database name, and credentials in the login/setup form in the application.
|
||||
|
||||
### Application Modules
|
||||
- **Login/Authentication:** Group-based access with secret keys
|
||||
- **Materials Management:** Track and manage materials/supplies
|
||||
- **People Management:** Manage group members
|
||||
- **SuperCafé:** Café/beverage ordering system
|
||||
- **Dining Hall:** Restaurant/meal management
|
||||
- **Import/Export:** Data backup and restoration
|
||||
- **Daily Summary:** Reports and analytics
|
||||
- **Notifications:** Alert system
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Making Changes to the Application
|
||||
1. **ALWAYS** edit files in `src/` directory, never `dist/`
|
||||
2. Run `python3 build.py` to rebuild after changes
|
||||
3. Refresh browser or restart web server to see changes
|
||||
4. Test authentication flow and network connectivity after any changes
|
||||
|
||||
### Adding New Features
|
||||
1. Create new JavaScript files in `src/page/` for new modules
|
||||
2. Add script references in `src/index.html`
|
||||
3. Update assets if new static files are needed
|
||||
4. Rebuild and test complete user workflows
|
||||
|
||||
### Debugging Common Issues
|
||||
- **Login Issues:** Check browser console for replication/auth errors and DB initialization logs
|
||||
- **Network Connectivity:** Verify remote CouchDB server is reachable and replication is active
|
||||
- **Build Issues:** Check Python syntax in build.py
|
||||
- **Performance:** Monitor browser DevTools Network tab for asset loading
|
||||
|
||||
## Validation Scenarios
|
||||
|
||||
### Complete User Workflow Test
|
||||
After making any changes, ALWAYS test this complete scenario:
|
||||
|
||||
1. **Build and Serve:**
|
||||
```bash
|
||||
cd /home/runner/work/TeleSec/TeleSec
|
||||
python3 build.py
|
||||
cd dist
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
2. **Login Test:**
|
||||
- Navigate to `http://localhost:8000`
|
||||
- Enter group code: "TEST"
|
||||
- Enter secret key: "SECRET123"
|
||||
- Click "Iniciar sesión"
|
||||
- Verify header shows: "TeleSec - TEST" and that the login is accepted
|
||||
|
||||
3. **Network Connectivity Test:**
|
||||
- Confirm green connection indicator appears (bottom right)
|
||||
- Check browser console shows PouchDB replication logs when a remote is configured
|
||||
- Verify heartbeat/last-seen docs are being updated in the local DB
|
||||
|
||||
4. **PWA Functionality Test:**
|
||||
- Check Service Worker registers successfully
|
||||
- Verify offline caching works (Network tab → Offline)
|
||||
- Test manifest.json loads correctly
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
### Essential Operations
|
||||
```bash
|
||||
# Build application (< 0.1 seconds)
|
||||
python3 build.py
|
||||
|
||||
# Serve with Python (most compatible)
|
||||
cd dist && python3 -m http.server 8000
|
||||
|
||||
# Serve with Node.js (advanced features)
|
||||
cd dist && npx http-server . --port 8001 --cors
|
||||
|
||||
# Clean rebuild
|
||||
rm -rf dist && python3 build.py
|
||||
|
||||
# Check build output
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
### File Structure Verification
|
||||
```bash
|
||||
# Verify all source files exist
|
||||
find src/ -name "*.js" -o -name "*.html" | sort
|
||||
|
||||
# Check template variable processing
|
||||
grep -r "%%.*%%" dist/ || echo "All template variables processed correctly"
|
||||
|
||||
# Verify assets copied correctly
|
||||
diff -r assets/ dist/ --exclude="*.js" --exclude="*.html" || echo "Some differences expected due to processing"
|
||||
```
|
||||
|
||||
**CRITICAL REMINDERS:**
|
||||
- **NEVER CANCEL**: Builds complete in under 0.1 seconds - no timeout needed
|
||||
- **ALWAYS test login and network connectivity** after changes
|
||||
- **Edit source files in `src/` directory only**, never `dist/`
|
||||
- **Test with real user credentials** to verify distributed networking
|
||||
- **Monitor browser console** for connection status and errors
|
||||
45
.github/workflows/static.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy to Github Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Build
|
||||
run: TELESEC_HOSTER=GitHub-Pages python3 build.py
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './dist/'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
6
.gitignore
vendored
@@ -1 +1,5 @@
|
||||
dist/*
|
||||
dist/*
|
||||
radata/*
|
||||
node_modules/*
|
||||
.DS_Store
|
||||
._*
|
||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "assets/static/euskaditech-css"]
|
||||
path = assets/static/euskaditech-css
|
||||
url = https://git.tech.eus/EuskadiTech/css.git
|
||||
BIN
assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 103 KiB |
@@ -1,252 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>TeleSec</title>
|
||||
<link rel="icon" type="image/png" href="static/TeleSec.jpg" />
|
||||
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
|
||||
<link href="static/toastr.min.css" rel="stylesheet" />
|
||||
|
||||
<link rel="prefetch" href="icon512_maskable.png" />
|
||||
<link rel="prefetch" href="icon512_rounded.png" />
|
||||
<link rel="prefetch" href="manifest.json" />
|
||||
<link rel="prefetch" href="static/axe.js" />
|
||||
<link rel="prefetch" href="static/doublescroll.js" />
|
||||
<link rel="prefetch" href="static/gun.js" />
|
||||
<link rel="prefetch" href="static/jquery.js" />
|
||||
<link rel="prefetch" href="static/load.js" />
|
||||
<link rel="prefetch" href="static/open.js" />
|
||||
<link rel="prefetch" href="static/path.js" />
|
||||
<link rel="prefetch" href="static/radisk.js" />
|
||||
<link rel="prefetch" href="static/radix.js" />
|
||||
<link rel="prefetch" href="static/rindexed.js" />
|
||||
<link rel="prefetch" href="static/sea.js" />
|
||||
<link rel="prefetch" href="static/showdown.min.js" />
|
||||
<link rel="prefetch" href="static/simplemde.min.css" />
|
||||
<link rel="prefetch" href="static/simplemde.min.js" />
|
||||
<link rel="prefetch" href="static/store.js" />
|
||||
<link rel="prefetch" href="static/synchronous.js" />
|
||||
<link rel="prefetch" href="static/TeleSec.jpg" />
|
||||
<link rel="prefetch" href="static/toastr.min.css" />
|
||||
<link rel="prefetch" href="static/toastr.min.js" />
|
||||
<link rel="prefetch" href="static/webrtc.js" />
|
||||
<link rel="prefetch" href="static/yson.js" />
|
||||
<link rel="prefetch" href="static/ico/add.png" />
|
||||
<link rel="prefetch" href="static/ico/azucar-moreno.png" />
|
||||
<link rel="prefetch" href="static/ico/azucar-blanco.jpg" />
|
||||
<link rel="prefetch" href="static/ico/stevia.jpg" />
|
||||
<link rel="prefetch" href="static/ico/stevia-gotas.webp" />
|
||||
<link rel="prefetch" href="static/ico/sacarina.jpg" />
|
||||
<link rel="prefetch" href="static/ico/arrow_down_blue.png" />
|
||||
<link rel="prefetch" href="static/ico/arrow_left_green.png" />
|
||||
<link rel="prefetch" href="static/ico/arrow_up_red.png" />
|
||||
<link rel="prefetch" href="static/ico/camera2.png" />
|
||||
<link rel="prefetch" href="static/ico/cereales.png" />
|
||||
<link rel="prefetch" href="static/ico/checkbox.png" />
|
||||
<link rel="prefetch" href="static/ico/checkbox_unchecked.png" />
|
||||
<link rel="prefetch" href="static/ico/connect_ok.svg" />
|
||||
<link rel="prefetch" href="static/ico/connect_ko.svg" />
|
||||
<link rel="prefetch" href="static/ico/coffee_bean.png" />
|
||||
<link rel="prefetch" href="static/ico/colacao.jpg" />
|
||||
<link rel="prefetch" href="static/ico/cookies.png" />
|
||||
<link rel="prefetch" href="static/ico/cow.png" />
|
||||
<link rel="prefetch" href="static/ico/delete.png" />
|
||||
<link rel="prefetch" href="static/ico/fire.png" />
|
||||
<link rel="prefetch" href="static/ico/keyboard_key_g.png" />
|
||||
<link rel="prefetch" href="static/ico/keyboard_key_p.png" />
|
||||
<link rel="prefetch" href="static/ico/lollipop.png" />
|
||||
<link rel="prefetch" href="static/ico/milk.png" />
|
||||
<link rel="prefetch" href="static/ico/preferences.png" />
|
||||
<link rel="prefetch" href="static/ico/sizes.png" />
|
||||
<link rel="prefetch" href="static/ico/statusok.png" />
|
||||
<link rel="prefetch" href="static/ico/snowflake.png" />
|
||||
<link rel="prefetch" href="static/ico/tea_bag.png" />
|
||||
<link rel="prefetch" href="static/ico/thermometer2.png" />
|
||||
<link rel="prefetch" href="static/ico/user.png" />
|
||||
<link rel="prefetch" href="static/ico/user_generic.png" />
|
||||
<link rel="prefetch" href="static/ico/water_tap.png" />
|
||||
<link rel="prefetch" href="static/ico/wheat.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Az. Blanco.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Az. Moreno.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Edulcorante.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Sacarina.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Sin.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Stevia (Gotas).png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Azucar-Stevia (Pastillas).png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Background.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Cafeina-Con.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Cafeina-Sin.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Leche-Agua.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Leche-Sin lactosa.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Leche-Vegetal.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Leche-de Vaca.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Selección-CafeSolo.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Selección-CaféLeche.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Selección-ColaCao.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Selección-Infusion.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Selección-Leche.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Tamaño-Grande.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Tamaño-Pequeño.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Temperatura-Caliente.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Temperatura-Frio.png" />
|
||||
<link rel="prefetch" href="static/ico/layered1/Temperatura-Templado.png" />
|
||||
<link rel="prefetch" href="page__supercafe.js" />
|
||||
<link rel="prefetch" href="app_logic.js" />
|
||||
<link rel="prefetch" href="page__index.js" />
|
||||
<link rel="prefetch" href="page__notificaciones.js" />
|
||||
<link rel="prefetch" href="page__materiales.js" />
|
||||
<link rel="prefetch" href="index.html" />
|
||||
<link rel="prefetch" href="page__exportar.js" />
|
||||
<link rel="prefetch" href="gun_init.js" />
|
||||
<link rel="prefetch" href="page__personas.js" />
|
||||
<link rel="prefetch" href="page__importar.js" />
|
||||
<link rel="prefetch" href="config.js" />
|
||||
<link rel="prefetch" href="app_modules.js" />
|
||||
<link rel="prefetch" href="page__comedor.js" />
|
||||
<link rel="prefetch" href="page__resumen_diario.js" />
|
||||
<link rel="prefetch" href="pwa.js" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<details class="supermesh-indicator">
|
||||
<summary>
|
||||
<b>SuperMesh</b><br />
|
||||
<br /><small id="peerPID" style="font-family: monospace"
|
||||
>PID ??????????</small
|
||||
>
|
||||
</summary>
|
||||
<ul id="peerList"></ul>
|
||||
<i>Todos los datos están encriptados.</i>
|
||||
</details>
|
||||
<main>
|
||||
<header class="no_print" id="header_hide_query">
|
||||
<details id="LinkAccount_details" open>
|
||||
<summary>
|
||||
<b
|
||||
>TeleSec - <span id="groupId">???</span> - (<span id="peerCount"
|
||||
>?</span
|
||||
>
|
||||
nodos)</b
|
||||
>
|
||||
</summary>
|
||||
<fieldset id="auth_fieldSet">
|
||||
<legend>Credenciales</legend>
|
||||
<br />
|
||||
<label
|
||||
>Codigo de grupo:<br />
|
||||
<input type="text" id="LinkAccount_group"
|
||||
/></label>
|
||||
<br />
|
||||
<br />
|
||||
<label
|
||||
>Clave secreta:<br />
|
||||
<input type="text" id="LinkAccount_secret"
|
||||
/></label>
|
||||
<br /><br />
|
||||
<button
|
||||
type="button"
|
||||
onclick='LinkAccount(document.getElementById("LinkAccount_group").value, document.getElementById("LinkAccount_secret").value, true)'
|
||||
>
|
||||
Iniciar sesión
|
||||
</button>
|
||||
</fieldset>
|
||||
</details>
|
||||
<!-- <button onclick="displayPost('index')">Ir a la pagina de inicio</button> -->
|
||||
<div id="appendApps">
|
||||
<!--<a class="button nav-supercafe nav-disabled" disabled>SuperCafé</a>
|
||||
<a class="button nav-comedor nav-disabled" disabled>Menú Comedor</a>
|
||||
<a class="button nav-recetas nav-disabled" disabled>Recetas</a>-->
|
||||
</div>
|
||||
<hr />
|
||||
</header>
|
||||
<div id="container"></div>
|
||||
<!-- <br><br><br>
|
||||
<footer>
|
||||
<hr>
|
||||
<details>
|
||||
<summary><b>Apps SuperMesh</b></summary>
|
||||
<button type="button">
|
||||
<img src="static/TeleSec.jpg" alt="" width="100px">
|
||||
<br>TeleSec
|
||||
</button>
|
||||
</details>
|
||||
</footer> -->
|
||||
<img
|
||||
id="connectStatus"
|
||||
style="bottom: 15px; right: 15px; position: fixed; width: 50px"
|
||||
/>
|
||||
</main>
|
||||
<img
|
||||
id="actionStatus"
|
||||
src="static/ico/statusok.png"
|
||||
style="
|
||||
z-index: 2048;
|
||||
margin: 0px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: none;
|
||||
"
|
||||
/>
|
||||
<div id="snackbar">
|
||||
Hay una nueva versión de TeleSec.<br /><a id="reload"
|
||||
>Pulsa aqui para actualizar.</a
|
||||
>
|
||||
</div>
|
||||
<script src="static/showdown.min.js"></script>
|
||||
<script src="static/jquery.js"></script>
|
||||
<script src="static/gun.js"></script>
|
||||
<script src="static/webrtc.js"></script>
|
||||
<script src="static/sea.js"></script>
|
||||
<script src="static/yson.js"></script>
|
||||
<script src="static/radix.js"></script>
|
||||
<!-- <script src="static/radisk.js"></script> -->
|
||||
<!-- <script src="static/store.js"></script> -->
|
||||
<script src="static/rindexed.js"></script>
|
||||
<script src="static/path.js"></script>
|
||||
<script src="static/open.js"></script>
|
||||
<script src="static/load.js"></script>
|
||||
<!--<script src="static/synchronous.js"></script>-->
|
||||
<!--<script src="static/axe.js"></script>-->
|
||||
<script src="static/toastr.min.js"></script>
|
||||
<script src="static/doublescroll.js"></script>
|
||||
<!--<script src="static/simplemde.min.js"></script>-->
|
||||
<script async>
|
||||
async function getQuota(cb = () => {}) {
|
||||
if (navigator.storage && navigator.storage.estimate) {
|
||||
const quota = await navigator.storage.estimate();
|
||||
// quota.usage -> Number of bytes used.
|
||||
// quota.quota -> Maximum number of bytes available.
|
||||
const percentageUsed = (quota.usage / quota.quota) * 100;
|
||||
console.log(
|
||||
`You've used ${percentageUsed}% of the available storage.`
|
||||
);
|
||||
const remaining = quota.quota - quota.usage;
|
||||
cb(percentageUsed, remaining);
|
||||
console.log(`You can write up to ${remaining} more bytes.`);
|
||||
}
|
||||
}
|
||||
getQuota();
|
||||
</script>
|
||||
<script src="pwa.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="gun_init.js"></script>
|
||||
<script src="app_logic.js"></script>
|
||||
<script src="app_modules.js"></script>
|
||||
<script src="page__index.js"></script>
|
||||
<script src="page__importar.js"></script>
|
||||
<script src="page__exportar.js"></script>
|
||||
<script src="page__materiales.js"></script>
|
||||
<script src="page__resumen_diario.js"></script>
|
||||
<script src="page__personas.js"></script>
|
||||
<script src="page__supercafe.js"></script>
|
||||
<script src="page__notificaciones.js"></script>
|
||||
<script src="page__comedor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
assets/load.gif
Normal file
|
After Width: | Height: | Size: 47 KiB |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"theme_color":"#23365e",
|
||||
"background_color":"#ffffff",
|
||||
"icons":[
|
||||
{"purpose":"maskable","sizes":"512x512","src":"icon512_maskable.png","type":"image/png"},
|
||||
{"purpose":"any","sizes":"512x512","src":"icon512_rounded.png","type":"image/png"}
|
||||
],
|
||||
"orientation":"portrait",
|
||||
"display":"standalone",
|
||||
"dir":"auto",
|
||||
"lang":"es-ES",
|
||||
"start_url":"index.html",
|
||||
"scope":"/",
|
||||
"description":"La app de TeleSec",
|
||||
"id":"telesec.tech.eus",
|
||||
"name": "TeleSec",
|
||||
"short_name": "TeleSec"
|
||||
}
|
||||
45
assets/static/appico/Alert_Warning.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path style="fill:#010101; fill-opacity:0.4549"
|
||||
d="M30 62H36L38 60L42 62H56L64 52L54 48H44L43 49L40 48H38L30 62z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4"
|
||||
d="M14 41V52L30 60L38 52V41L22 35L14 41z"
|
||||
/>
|
||||
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="56.22" y1="56.15" x2="49.67" y2="65.65">
|
||||
<stop offset="0" stop-color="#e07900"/>
|
||||
<stop offset="1" stop-color="#fff289"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient0)"
|
||||
d="M14 41L30 48L38 41L22 35L14 41z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4"
|
||||
d="M14 6V34L30 41L38 35V6L22 2L14 6z"
|
||||
/>
|
||||
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="24.32" y1="-24.13" x2="51.3" y2="-13.84">
|
||||
<stop offset="0" stop-color="#fff49e"/>
|
||||
<stop offset="1" stop-color="#ffbd30"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient1)"
|
||||
d="M14 6V34L30 41V11L14 6z
|
||||
M14 41V52L30 60V48L14 41z"
|
||||
/>
|
||||
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="30.5" y1="-22.81" x2="56.01" y2="-9.13">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#fff289"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient2)"
|
||||
d="M14 6L30 11L38 6L22 2L14 6z"
|
||||
/>
|
||||
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="33.3" y1="-0.68" x2="46.77" y2="0.74">
|
||||
<stop offset="0" stop-color="#ed9406"/>
|
||||
<stop offset="1" stop-color="#fcb23d"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient3)"
|
||||
d="M30 11V41L38 35V6L30 11z
|
||||
M30 48V60L38 52V41L30 48z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
75
assets/static/appico/App_CodyCam.svg
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path style="fill:#010101; fill-opacity:0.4078"
|
||||
d="M38 40C25.84 40 16 45.37 16 52C16 52 25.84 64 38 64C50.14 64 60 58.61 60 52C60 45.37 50.14 40 38 40z"
|
||||
/>
|
||||
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="52" y1="-64" x2="68" y2="-64">
|
||||
<stop offset="0" stop-color="#010000"/>
|
||||
<stop offset="1" stop-color="#010000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<path style="fill:none; stroke:url(#gradient0); stroke-width:4"
|
||||
d="M44 44C44 44 49.51 38 54 38C58 38 56 46 64 42"
|
||||
/>
|
||||
<path style="fill:none; stroke:#010000; stroke-width:4"
|
||||
d="M28 34C28 34 23 48 14 49V52L34 62L48 48L45 34H28z"
|
||||
/>
|
||||
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="41.67" y1="-9.73" x2="71.92" y2="-3.5">
|
||||
<stop offset="0" stop-color="#5c5c5c"/>
|
||||
<stop offset="1" stop-color="#b8b8b8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient1)"
|
||||
d="M34 36V62L48 48L45 34L34 36z"
|
||||
/>
|
||||
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="14.26" y1="97.46" x2="-8.94" y2="81.76">
|
||||
<stop offset="0" stop-color="#c9c9c9"/>
|
||||
<stop offset="1" stop-color="#ffffff"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient2)"
|
||||
d="M14 52L34 62V58L14 49V52z"
|
||||
/>
|
||||
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="51.94" y1="63.65" x2="26.51" y2="73.34">
|
||||
<stop offset="0" stop-color="#797979"/>
|
||||
<stop offset="1" stop-color="#e4e4e4"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient3)"
|
||||
d="M34 58C41 51 40 39 40 39L28 34C28 34 23 48 14 49L34 58z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#010000; stroke-width:4"
|
||||
d="M30 2C18.95 2 10 10.95 10 22C10 33.03 18.95 42 30 42C41.03 42 50 33.03 50 22C50 10.95 41.03 2 30 2z"
|
||||
/>
|
||||
<radialGradient id="gradient4" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.373,0.2924,-0.3363,0.4291,25.2905,16.1419)">
|
||||
<stop offset="0.1647" stop-color="#ffffff"/>
|
||||
<stop offset="0.7686" stop-color="#959191"/>
|
||||
<stop offset="1" stop-color="#b9b9b9"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#gradient4)"
|
||||
d="M30 2C18.95 2 10 10.95 10 22C10 33.03 18.95 42 30 42C41.03 42 50 33.03 50 22C50 10.95 41.03 2 30 2z"
|
||||
/>
|
||||
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="-6" y1="-4" x2="38" y2="-4">
|
||||
<stop offset="0.2509" stop-color="#010101"/>
|
||||
<stop offset="1" stop-color="#c1acac"/>
|
||||
</linearGradient>
|
||||
<path style="fill:none; stroke:url(#gradient5); stroke-width:4"
|
||||
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
|
||||
transform="matrix(0.9846,0.4347,-0.4379,0.9773,24.6832,-30.2941)"
|
||||
/>
|
||||
<radialGradient id="gradient6" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.1345,0.0871,-0.0673,0.1039,20.7047,50.7358)">
|
||||
<stop offset="0" stop-color="#434a68"/>
|
||||
<stop offset="1" stop-color="#0f1238"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#gradient6)"
|
||||
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
|
||||
transform="matrix(0.9846,0.4347,-0.4379,0.9773,26.8385,-30.2941)"
|
||||
/>
|
||||
<path style="fill:#ffffff"
|
||||
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
|
||||
transform="matrix(-0.0781,0.1744,-0.1757,-0.0775,31.659,20.9543)"
|
||||
/>
|
||||
<path style="fill:#ffffff"
|
||||
d="M19 53C19 53 22.24 53.24 23.24 52.24C24.24 51.24 24 49 24 49C24 49 24.87 52.1 23.87 53.1C22.87 54.1 19 53 19 53z"
|
||||
transform="matrix(0.9846,0.4347,-0.4379,0.9773,26.8385,-30.2941)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
83
assets/static/appico/App_Dropbox.svg
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path style="fill:#000000; fill-opacity:0.396"
|
||||
d="M32 62H40L44 64L61 43L55.8 40.4L60 38L52 37L32 62z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4; stroke-linejoin:round"
|
||||
d="M10 39V49L32 60L50 42V36"
|
||||
/>
|
||||
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="46.07" y1="78.24" x2="23.93" y2="73.15">
|
||||
<stop offset="0" stop-color="#82aac8"/>
|
||||
<stop offset="1" stop-color="#40407c"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient0)"
|
||||
d="M32 38L50 20V42L32 60V38z"
|
||||
/>
|
||||
<path style="fill:#3d3d5d"
|
||||
d="M32 38H42L32 48V38z"
|
||||
/>
|
||||
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="44.47" y1="70.75" x2="29.89" y2="79.36">
|
||||
<stop offset="0" stop-color="#5a6e82"/>
|
||||
<stop offset="1" stop-color="#9be2ff"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient1)"
|
||||
d="M11.89 36.01L14 46L32 55V60L10 49V35.99L11.89 36.01z"
|
||||
/>
|
||||
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="54.08" y1="49.54" x2="50.66" y2="59.95">
|
||||
<stop offset="0" stop-color="#5a6e82"/>
|
||||
<stop offset="1" stop-color="#3a7d99"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient2)"
|
||||
d="M32 38V55L14 46L11.89 36.01L32 38z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4; stroke-linejoin:round"
|
||||
d="M24 46L2 35L10 27H2L24 14H30L37 6L54 12L50 23L62 26L44 44L32 38L24 46z"
|
||||
/>
|
||||
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="44.25" y1="2.48" x2="55.01" y2="17.28">
|
||||
<stop offset="0" stop-color="#40407c"/>
|
||||
<stop offset="0.9962" stop-color="#417297"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient3)"
|
||||
d="M10 27L30 14V33L23.89 37.88L10 30.99V27z"
|
||||
/>
|
||||
<linearGradient id="gradient4" gradientUnits="userSpaceOnUse" x1="42.68" y1="-29.81" x2="61.51" y2="-25.25">
|
||||
<stop offset="0" stop-color="#6dc8ed"/>
|
||||
<stop offset="1" stop-color="#a4c6e1"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient4)"
|
||||
d="M30 14L34 30L50 37V22L30 14z"
|
||||
/>
|
||||
<path style="fill:#3d3d5d"
|
||||
d="M30 14L34 30L50 37L41.85 38.33L30 33V14z"
|
||||
/>
|
||||
<path style="fill:#376181"
|
||||
d="M30 33L23.89 37.88L32 38L36.1 35.74L30 33z"
|
||||
/>
|
||||
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="32.24" y1="-14.37" x2="49.83" y2="-16.21">
|
||||
<stop offset="0" stop-color="#9be2ff"/>
|
||||
<stop offset="1" stop-color="#5789bd"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient5)"
|
||||
d="M37 6L54 12L50 23L30 14L37 6z"
|
||||
/>
|
||||
<linearGradient id="gradient6" gradientUnits="userSpaceOnUse" x1="1.22" y1="-19.6" x2="44.89" y2="-14.22">
|
||||
<stop offset="0" stop-color="#9be2ff"/>
|
||||
<stop offset="1" stop-color="#5d81a5"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient6)"
|
||||
d="M2 35L24 46L32 38L10 27L2 35z"
|
||||
/>
|
||||
<path style="fill:#9be2ff"
|
||||
d="M2 27L24 14H30L10 27H2z"
|
||||
/>
|
||||
<linearGradient id="gradient7" gradientUnits="userSpaceOnUse" x1="43.44" y1="10.75" x2="62.48" y2="20.22">
|
||||
<stop offset="0" stop-color="#9be2ff"/>
|
||||
<stop offset="1" stop-color="#5a6e82"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient7)"
|
||||
d="M32 38L50 23L62 26L44 44L32 38z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
7
assets/static/appico/Chat.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 428.428 428.428" xml:space="preserve">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
99
assets/static/appico/Classroom.svg
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512.001 512.001" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M467.309,16.768H221.454c-6.128,0-11.095,4.967-11.095,11.095v86.451l12.305-7.64c3.131-1.945,6.475-3.257,9.884-3.978
|
||||
V38.958h223.665v160.016H232.549v-25.89l-22.19,13.778v23.208c0,6.128,4.967,11.095,11.095,11.095h245.855
|
||||
c6.127,0,11.095-4.967,11.095-11.095V27.863C478.404,21.735,473.436,16.768,467.309,16.768z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M306.001,78.356c-2.919-3.702-8.285-4.335-11.986-1.418l-38.217,30.133c3.649,2.385,6.85,5.58,9.301,9.527
|
||||
c0.695,1.117,1.298,2.266,1.834,3.431l37.651-29.687C308.286,87.424,308.92,82.057,306.001,78.356z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="121.535" cy="31.935" r="31.935"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M252.01,124.728c-4.489-7.229-13.987-9.451-21.218-4.963l-31.206,19.375c-0.13-25.879-0.061-12.145-0.144-28.811
|
||||
c-0.101-20.005-16.458-36.281-36.464-36.281h-15.159c-12.951,33.588-8.779,21.12-19.772,49.63l4.623-20.131
|
||||
c0.32-1.508,0.088-3.08-0.655-4.43l-6.264-11.393l5.559-10.109c0.829-1.508-0.264-3.356-1.985-3.356h-15.271
|
||||
c-1.72,0-2.815,1.848-1.985,3.356l5.57,10.13l-6.276,11.414c-0.728,1.325-0.966,2.865-0.672,4.347l4.005,20.172
|
||||
c-2.159-5.599-17.084-44.306-19.137-49.63H80.093c-20.005,0-36.363,16.275-36.464,36.281l-0.569,113.2
|
||||
c-0.042,8.51,6.821,15.443,15.331,15.486c0.027,0,0.052,0,0.079,0c8.473,0,15.364-6.848,15.406-15.331l0.569-113.2
|
||||
c0-0.018,0-0.036,0-0.053c0.024-1.68,1.399-3.026,3.079-3.013c1.68,0.012,3.034,1.378,3.034,3.058l0.007,160.381
|
||||
c14.106-0.6,27.176,4.488,36.981,13.423v-62.568h7.983v71.773c5.623,8.268,8.914,18.243,8.914,28.974
|
||||
c0,9.777-2.732,18.928-7.469,26.731c4.866,0.023,9.592,0.669,14.099,1.861c6.076-5.271,13.385-9.151,21.437-11.136
|
||||
c0-279.342-0.335-106.627-0.335-229.418c0-1.779,1.439-3.221,3.218-3.224c1.779-0.004,3.224,1.432,3.232,3.211
|
||||
c0.054,10.807,0.224,44.59,0.283,56.351c0.028,5.579,3.07,10.708,7.953,13.407c4.874,2.694,10.835,2.554,15.583-0.394
|
||||
l54.604-33.903C254.276,141.458,256.499,131.957,252.01,124.728z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="429.221" cy="322.831" r="33.803"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M511.459,405.811c-0.107-21.176-17.421-38.404-38.598-38.404c-9.137,0-76.583,0-84.781,0
|
||||
c3.637,7.068,5.704,15.069,5.704,23.55c0,9.005-2.405,18.413-7.5,26.782c18.904,0.764,35.468,10.91,45.149,25.897h40.579v-37.43
|
||||
c0-1.842,1.46-3.352,3.301-3.415s3.402,1.345,3.526,3.182c0,0,0,0.001,0,0.002l0.19,37.661h32.621L511.459,405.811z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M290.469,390.956c0-8.629,2.138-16.763,5.894-23.92c-22.009,0-47.852,0-75.267,0c3.472,6.939,5.437,14.756,5.437,23.029
|
||||
c0,9.721-2.73,18.926-7.469,26.731c15.558,0.074,29.912,6.538,40.283,17.267c10.054-9.822,23.759-15.914,38.836-15.995
|
||||
C292.948,409.616,290.469,400.126,290.469,390.956z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M264.819,288.655c-18.668,0-33.804,15.132-33.804,33.803c0,18.628,15.107,33.803,33.804,33.803
|
||||
c18.518,0,33.803-14.965,33.803-33.803C298.622,303.808,283.517,288.655,264.819,288.655z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M123.217,390.065c0-8.252,1.956-16.053,5.411-22.98c-1.457-0.072,4.672-0.049-89.485-0.049
|
||||
c-21.068,0-38.491,17.138-38.598,38.404l-0.192,38.196c14.907,0,17.906,0,32.621,0l0.191-38.031
|
||||
c0.01-1.884,1.541-3.402,3.423-3.397c1.882,0.006,3.404,1.532,3.404,3.414v38.014h45.727c9.855-15.754,26.8-25.646,45.243-26.406
|
||||
C125.956,409.168,123.217,399.865,123.217,390.065z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M82.786,288.655c-18.668,0-33.803,15.134-33.803,33.803c0,18.584,15.046,33.803,33.803,33.803
|
||||
c18.536,0,33.804-15.015,33.804-33.803C116.59,303.788,101.455,288.655,82.786,288.655z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M422.533,473.807c-0.105-21.178-17.42-38.406-38.597-38.406c-2.246,0-82.969,0-85.507,0
|
||||
c-21.176,0-39.601,17.227-39.708,38.404l-0.275-0.891c-0.105-21.092-17.341-38.404-38.597-38.404c-24.544,0-59.795,0-85.507,0
|
||||
c-21.176,0-39.601,17.227-39.708,38.404L94.442,512h32.621l0.191-38.922c0.008-1.622,1.327-2.93,2.948-2.926
|
||||
c1.621,0.004,2.932,1.32,2.932,2.941v38.908c19.121,0,68.483,0,86.392,0v-38.908c0-1.736,1.405-3.144,3.141-3.149
|
||||
c1.735-0.004,3.149,1.397,3.158,3.133l0.191,38.923c6.669,0,58.238,0,65.134,0l0.191-38.031c0,0,0-0.001,0-0.002
|
||||
c0.009-1.621,1.328-2.928,2.949-2.924c1.621,0.004,2.931,1.32,2.931,2.941v38.016c19.121,0,68.483,0,86.392,0v-38.016
|
||||
c0-1.736,1.405-3.144,3.141-3.149c1.735-0.004,3.149,1.397,3.158,3.133l0.191,38.031h32.621L422.533,473.807z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="175.934" cy="389.933" r="34.198"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="342.07" cy="390.821" r="34.198"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
97
assets/static/appico/Coffee.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="287.96585" height="275.66766" id="svg2" version="1.1" inkscape:version="0.48.0 r9654" sodipodi:docname="Coffee cup.svg">
|
||||
<defs id="defs4">
|
||||
<linearGradient id="linearGradient3817">
|
||||
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="0" id="stop3819"/>
|
||||
<stop style="stop-color:#ececec;stop-opacity:1;" offset="1" id="stop3821"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3801">
|
||||
<stop style="stop-color:#ececec;stop-opacity:1;" offset="0" id="stop3803"/>
|
||||
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="1" id="stop3805"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3791">
|
||||
<stop style="stop-color:#ececec;stop-opacity:1;" offset="0" id="stop3793"/>
|
||||
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="1" id="stop3795"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3767">
|
||||
<stop style="stop-color:#2b2b2b;stop-opacity:1;" offset="0" id="stop3769"/>
|
||||
<stop style="stop-color:#666666;stop-opacity:1;" offset="1" id="stop3771"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3755">
|
||||
<stop style="stop-color:#dddddd;stop-opacity:1;" offset="0" id="stop3757"/>
|
||||
<stop style="stop-color:#b2b2b2;stop-opacity:1;" offset="1" id="stop3759"/>
|
||||
</linearGradient>
|
||||
<filter inkscape:collect="always" id="filter3781" x="-0.079655327" width="1.1593107" y="-0.23315777" height="1.4663155" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur inkscape:collect="always" stdDeviation="13.323301" id="feGaussianBlur3783"/>
|
||||
</filter>
|
||||
<filter inkscape:collect="always" id="filter3842" x="-0.1819846" width="1.3639692" y="-0.44528148" height="1.890563" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur inkscape:collect="always" stdDeviation="4.4043134" id="feGaussianBlur3844"/>
|
||||
</filter>
|
||||
<filter inkscape:collect="always" id="filter3868" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur inkscape:collect="always" stdDeviation="1.8652408" id="feGaussianBlur3870"/>
|
||||
</filter>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3884" gradientUnits="userSpaceOnUse" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3886" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 0, 57.7087)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3888" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -233.089, 102.502)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3910" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3918" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient3920" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
|
||||
<filter inkscape:collect="always" id="filter3939" x="-0.16236658" width="1.3247333" y="-0.47526056" height="1.9505211" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur inkscape:collect="always" stdDeviation="27.157745" id="feGaussianBlur3941"/>
|
||||
</filter>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3950" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3953" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3957" gradientUnits="userSpaceOnUse" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)"/>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3971" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3973" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3975" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3997" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3999" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient4001" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient4003" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient4005" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient4007" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3061" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3063" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3074" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -123.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3077" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 109.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
|
||||
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3081" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 187.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3083" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3085" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
|
||||
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient3087" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
|
||||
</defs>
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.7071068" inkscape:cx="7.436507" inkscape:cy="230.98232" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1024" inkscape:window-height="742" inkscape:window-x="-4" inkscape:window-y="-4" inkscape:window-maximized="1" fit-margin-top="10" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0">
|
||||
<inkscape:grid type="xygrid" id="grid3785" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true"/>
|
||||
</sodipodi:namedview>
|
||||
<metadata id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-246.532, -190.385)">
|
||||
<g id="g3094">
|
||||
<path transform="matrix(0.618775, 0, 0, 0.646379, 143.447, 142.829)" sodipodi:type="arc" style="fill:url(#radialGradient3083);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3781)" id="path3779" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
|
||||
<path d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path3765" style="fill:url(#radialGradient3085);fill-opacity:1;fill-rule:nonzero;stroke:none" sodipodi:type="arc" transform="matrix(0.618775, 0, 0, 0.646379, 143.447, 142.829)"/>
|
||||
<path transform="matrix(0.646379, 0, 0, 0.646379, 132.426, 136.365)" sodipodi:type="arc" style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3763" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
|
||||
<path transform="matrix(0.646379, 0, 0, 0.646379, 132.426, 133.78)" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path2985" style="fill:url(#radialGradient3087);fill-opacity:1;fill-rule:nonzero;stroke:none" sodipodi:type="arc"/>
|
||||
<path transform="matrix(0.372876, 0, 0, 0.319443, 241.631, 265.685)" sodipodi:type="arc" style="fill:#979797;fill-opacity:0.39215686;fill-rule:nonzero;stroke:#b7b7b7;stroke-width:5.61860895;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" id="path3775" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
|
||||
<path d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path3926" style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3939)" sodipodi:type="arc" transform="matrix(0.265496, 0, 0, 0.200192, 283.77, 318.155)"/>
|
||||
<path sodipodi:nodetypes="cczcczc" inkscape:connector-curvature="0" id="path3815" d="m 473.13884,312.5941 0.45571,-13.03877 c 16.79252,-1.26498 38.44962,-8.39059 38.25779,26.6325 -0.19184,35.02311 -38.64614,28.28143 -48.43686,23.68663 l 0.65784,-10.56391 c 7.09726,2.48526 35.21153,4.81796 35.59877,-13.93125 0.38725,-18.74922 -9.93254,-15.43796 -26.53325,-12.7852 z" style="fill:url(#linearGradient3081);fill-opacity:1;stroke:#d6d6d6;stroke-width:0.80041194;stroke-opacity:1"/>
|
||||
<path sodipodi:nodetypes="czczc" inkscape:connector-curvature="0" id="path3787" d="m 289.77863,270.46269 c 0,0 0,-20 100,-20 100,0 100,20 100,20 0,0 0,20 -100,20 -100,0 -100,-20 -100,-20 z" style="fill:#e6e6e6;fill-opacity:1;stroke:none"/>
|
||||
<path sodipodi:nodetypes="czczc" inkscape:connector-curvature="0" id="path3789" d="m 289.77863,270.46269 c 0,0 0,140 100,140 100,0 100,-140 100,-140 0,0 0,20 -100,20 -100,0 -100,-20 -100,-20 z" style="fill:url(#radialGradient3077);fill-opacity:1;stroke:none"/>
|
||||
<path style="fill:url(#radialGradient3074);fill-opacity:1;stroke:#d6d6d6;stroke-opacity:1" d="m 294.77863,270.46269 c 0,0 0,-15 95,-15 95,0 95,15 95,15 0,0 0,15 -95,15 -95,0 -95,-15 -95,-15 z" id="path3799" inkscape:connector-curvature="0" sodipodi:nodetypes="czczc"/>
|
||||
<path id="path3809" d="m 389.77863,265.47551 c -64.11627,0 -83.29251,7.59423 -84.875,11.90625 11.22479,3.93491 34.8967,8.09375 84.875,8.09375 49.9783,0 73.65021,-4.15884 84.875,-8.09375 -1.58249,-4.31202 -20.75873,-11.90625 -84.875,-11.90625 z" style="fill:#562c08;fill-opacity:1;stroke:none" inkscape:connector-curvature="0"/>
|
||||
<path inkscape:connector-curvature="0" id="path3827" d="m 292.10929,298.02015 c 2.55938,4.81149 16.9432,18.3125 97.68492,18.3125 80.69081,0 95.08106,-13.49308 97.65375,-18.3125 -5.46348,5.78106 -25.32465,16.3125 -97.65375,16.3125 -72.39656,0 -92.24323,-10.53491 -97.68492,-16.3125 z" style="fill:#000000;fill-opacity:1;stroke:none"/>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none" d="m 291.54108,294.02015 c 2.57427,4.81149 17.04177,18.3125 98.25322,18.3125 81.16025,0 95.63422,-13.49308 98.22187,-18.3125 -5.49527,5.78106 -25.47198,16.3125 -98.22187,16.3125 -72.81774,0 -92.77987,-10.53491 -98.25322,-16.3125 z" id="path3832" inkscape:connector-curvature="0"/>
|
||||
<path sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3834" d="m 302.9703,288.54748 25.00128,5.17703 c -2.71055,42.30856 3.19121,65.46435 23.73858,99.6263 -25.41202,-23.58673 -42.16474,-53.00575 -48.73986,-104.80333 z" style="fill:#ffffff;fill-opacity:0.35294118;stroke:none"/>
|
||||
<path transform="matrix(1.31762, 0, 0, 1.673, 20.9841, 66.1965)" d="m 308.60161,118.72869 a 29.041885,11.869292 0 1 1 -58.08377,0 29.041885,11.869292 0 1 1 58.08377,0 z" sodipodi:ry="11.869292" sodipodi:rx="29.041885" sodipodi:cy="118.72869" sodipodi:cx="279.55972" id="path3836" style="fill:#ffffff;fill-opacity:0.35294118;fill-rule:nonzero;stroke:none;filter:url(#filter3842)" sodipodi:type="arc"/>
|
||||
<path transform="matrix(0.69763, 0, 0, 0.617722, 194.056, 186.002)" sodipodi:nodetypes="cczzczzzc" inkscape:connector-curvature="0" id="path3846" d="m 307.31913,54.771706 c -11.66884,-21.1275 -16.92627,-23.67626 -27.87348,-26.494634 -20.94721,-2.318374 -29.96874,3.083901 -29.28106,17.972485 0.68768,14.888584 26.53898,25.731883 26.8681,42.599832 0.32912,16.867951 -19.63135,51.743261 -19.63135,51.743261 0,0 31.42782,-33.99061 30.98863,-51.465344 -0.43918,-17.474744 -21.70744,-27.295456 -24.67515,-34.229816 -2.96771,-6.934361 -1.4691,-17.248349 14.72575,-17.437672 16.19485,-0.189323 28.87856,17.311888 28.87856,17.311888 z" style="opacity:0.5;fill:#eaa21f;fill-opacity:0.35294118;stroke:none;filter:url(#filter3868)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
12
assets/static/appico/Cogs.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="256px" height="256px" viewBox="0 0 54.13 54.13" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.0005413">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.5413"/>
|
||||
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
|
||||
<title>cogs</title>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
3
assets/static/appico/Database.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="353" height="512" preserveAspectRatio="xMidYMid">
|
||||
<path fill-rule="evenodd" d="M299.596 492.223C266.856 504.976 223.496 512 177.5 512c-45.996 0-89.357-7.024-122.095-19.777-35.083-13.667-54.404-32.766-54.404-53.78V73.557c0-21.014 19.321-40.113 54.404-53.78C88.144 7.024 131.504 0 177.5 0c45.996 0 89.356 7.024 122.096 19.778 35.069 13.661 54.387 32.751 54.402 53.756 0 .007.001.014.001.023v364.886c0 21.014-19.321 40.113-54.403 53.78ZM293.872 34.44C262.914 22.381 221.585 15.739 177.5 15.739c-44.085 0-85.414 6.642-116.372 18.702-28.2 10.984-44.374 25.243-44.374 39.116 0 13.873 16.174 28.131 44.374 39.116 30.958 12.058 72.287 18.7 116.372 18.7 16.532 0 32.675-.934 48.031-2.738 25.592-3.005 48.992-8.425 68.341-15.963 28.199-10.985 44.372-25.243 44.372-39.116 0-13.873-16.172-28.131-44.372-39.116Zm44.375 70.393c-.015.014-.031.027-.046.041-.133.122-.275.244-.41.367-10.912 9.91-24.554 16.781-38.194 22.095-21.485 8.369-47.544 14.27-75.921 17.339-14.865 1.608-30.364 2.438-46.175 2.438-45.996 0-89.357-7.024-122.095-19.777-13.896-5.413-27.587-12.266-38.614-22.469-.012-.011-.024-.021-.036-.032v90.35c0 13.872 16.173 28.13 44.373 39.115 26.121 10.176 59.624 16.495 95.938 18.221 6.805.325 13.62.482 20.434.482 48.743 0 106.184-3.582 146.132-34.692 5.307-4.133 10.264-9.024 12.936-15.305 1.049-2.46 1.678-5.139 1.678-7.82v-90.353Zm.001 121.627c-.012.009-.024.02-.034.029-2.528 2.319-5.204 4.475-7.998 6.469-17.383 12.41-38.247 19.695-58.788 24.922-16.994 4.324-35.701 7.424-55.406 9.177-.172.014-.343.032-.515.047-6.485.568-13.077.987-19.749 1.261-6.026.248-12.117.377-18.256.377-45.996 0-89.357-7.024-122.095-19.777-13.652-5.316-27.217-12.143-38.139-22.045-.155-.141-.317-.279-.471-.421-.013-.012-.026-.024-.04-.036v90.352c0 13.873 16.173 28.131 44.373 39.116 30.958 12.059 72.287 18.701 116.372 18.701 11.881 0 23.559-.487 34.89-1.429.813-.066 1.625-.137 2.433-.208 29.767-2.655 57.032-8.487 79.049-17.064 12.028-4.685 21.859-9.967 29.189-15.559 6.171-4.708 12.25-10.671 14.436-18.344.015-.052.024-.104.039-.155.458-1.648.71-3.348.71-5.059V226.46Zm.002 121.628c-.013.012-.025.022-.038.034-.172.159-.356.317-.531.475-2.552 2.309-5.255 4.445-8.073 6.42-9.261 6.487-19.492 11.48-30.01 15.577-28.508 11.103-65.07 17.861-104.411 19.423-5.839.232-11.738.352-17.683.352-45.996 0-89.357-7.024-122.095-19.777-13.687-5.33-27.127-12.071-38.086-21.997-.174-.157-.356-.314-.528-.472-.013-.012-.025-.022-.038-.034v90.352c0 13.872 16.173 28.13 44.373 39.115 30.959 12.06 72.286 18.702 116.372 18.702s85.414-6.642 116.373-18.702c28.199-10.985 44.372-25.241 44.372-39.115h.003v-90.353Zm-28.174 80.883c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.95 0 14.394 6.439 14.394 14.381 0 7.943-6.444 14.381-14.394 14.381Zm-49.956 15.047c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.394 6.439 14.394 14.381 0 7.943-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.393 6.439 14.393 14.381 0 7.943-6.444 14.381-14.393 14.381Zm102.968-143.877c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.95 0 14.394 6.438 14.394 14.381 0 7.942-6.444 14.381-14.394 14.381Zm-49.956 15.057c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.949 0 14.394 6.438 14.394 14.381 0 7.942-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.949 0 14.393 6.438 14.393 14.381 0 7.942-6.444 14.381-14.393 14.381Zm102.968-145.986c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.95 0 14.394 6.439 14.394 14.381 0 7.943-6.444 14.381-14.394 14.381Zm-49.956 15.057c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.394 6.439 14.394 14.381 0 7.943-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.393 6.439 14.393 14.381 0 7.943-6.444 14.381-14.393 14.381Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
61
assets/static/appico/File_Person.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path style="fill:#010101; fill-opacity:0.4235"
|
||||
d="M28 64C28 64 33 64 38 64C43 64 48.2 60.96 52.75 61.12C63.12 61.5 67.25 56.37 60.87 54.37C57.04 53.17 52 53.25 49.12 51.25C45.45 48.69 38 48 38 48V55L28 64z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4"
|
||||
d="M12 18V35L16 39V56L20 58L22 58L23 59.5L28 62L36 54V43L40 39V21L21 12L12 18z"
|
||||
/>
|
||||
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="73.74" y1="-57.07" x2="116.61" y2="-8.05">
|
||||
<stop offset="0" stop-color="#ffdb97"/>
|
||||
<stop offset="1" stop-color="#fcaf29"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient0)"
|
||||
d="M12 18V35L16 39V56L20 58L22 58L23 59.5L28 62V45H32V27L12 18z"
|
||||
/>
|
||||
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="-43.97" y1="-33.98" x2="-24.83" y2="-51.95">
|
||||
<stop offset="0" stop-color="#fff7ea"/>
|
||||
<stop offset="0.9962" stop-color="#fdd17b"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient1)"
|
||||
d="M12 18L32 27L40 21L26 20L31.99 16.99L21 12L12 18z"
|
||||
/>
|
||||
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="54.23" y1="-52.61" x2="75.84" y2="-45.97">
|
||||
<stop offset="0" stop-color="#c85805"/>
|
||||
<stop offset="1" stop-color="#f06306"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient2)"
|
||||
d="M32 45H28V62L36 54V43L40 39V21L32 27V45z"
|
||||
/>
|
||||
<path style="fill:#a32904"
|
||||
d="M28 45V51L36 43L32 45H28z"
|
||||
/>
|
||||
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="28.92" y1="-64" x2="39.07" y2="-64">
|
||||
<stop offset="0" stop-color="#c85804"/>
|
||||
<stop offset="1" stop-color="#dc952f"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient3)"
|
||||
d="M26 20L40 21L32 17L26 20z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4"
|
||||
d="M26 2C22 2 18 6 18 10C18 14 22 18 26 18C30 18 34 14 34 10C34 6 30 2 26 2z"
|
||||
/>
|
||||
<radialGradient id="gradient4" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.2361,0,0,0.2321,22.625,6.375)">
|
||||
<stop offset="0" stop-color="#f2f2f2"/>
|
||||
<stop offset="1" stop-color="#bca184"/>
|
||||
<stop offset="0.6742" stop-color="#7d7a7a"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#gradient4)"
|
||||
d="M26 2C22 2 18 6 18 10C18 14 22 18 26 18C30 18 34 14 34 10C34 6 30 2 26 2z"
|
||||
/>
|
||||
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="54.23" y1="-52.61" x2="75.84" y2="-45.97">
|
||||
<stop offset="0" stop-color="#c85805"/>
|
||||
<stop offset="1" stop-color="#f06306"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient5)"
|
||||
d="M20 58H22V44L20 43V58z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
65
assets/static/appico/File_Plugin.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path style="fill:#000000; fill-opacity:0.3882"
|
||||
d="M60 35.39L32 64H38L64 37.39L60 35.39z"
|
||||
/>
|
||||
<path style="fill:none; stroke:#000000; stroke-width:4"
|
||||
d="M2 30V47L32 62L58 36V19L30 10L2 30z"
|
||||
/>
|
||||
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="45.2" y1="-3.44" x2="66.1" y2="20.77">
|
||||
<stop offset="0" stop-color="#6499e8"/>
|
||||
<stop offset="1" stop-color="#1b63ce"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient0)"
|
||||
d="M32 43L2 30V47L32 62V43z"
|
||||
/>
|
||||
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="64.29" y1="5.5" x2="75.95" y2="14.74">
|
||||
<stop offset="0" stop-color="#10489b"/>
|
||||
<stop offset="1" stop-color="#0a54c3"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient1)"
|
||||
d="M32 43L58 19V36L32 62V43z"
|
||||
/>
|
||||
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="8" y1="-64" x2="56" y2="-64">
|
||||
<stop offset="0" stop-color="#c0d9ff"/>
|
||||
<stop offset="1" stop-color="#5e95e8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient2)"
|
||||
d="M32 43L58 19L30 10L2 30L32 43z"
|
||||
/>
|
||||
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="18" y1="20" x2="34" y2="20">
|
||||
<stop offset="0" stop-color="#2a4fae"/>
|
||||
<stop offset="0.25" stop-color="#4ca0da"/>
|
||||
<stop offset="1" stop-color="#0434a1"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient3)"
|
||||
d="M15 28C15 25.79 18.58 24 23 24C27.41 24 31 25.79 31 28V31C31 33.2 27.41 35 23 35C18.58 35 15 33.2 15 31V28z"
|
||||
/>
|
||||
<linearGradient id="gradient4" gradientUnits="userSpaceOnUse" x1="4" y1="-65" x2="50" y2="-65">
|
||||
<stop offset="0" stop-color="#c0d9ff"/>
|
||||
<stop offset="1" stop-color="#5e95e8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient4)"
|
||||
d="M15 27C15 24.79 18.58 23 23 23C27.41 23 31 24.79 31 27C31 29.2 27.41 31 23 31C18.58 31 15 29.2 15 27z"
|
||||
/>
|
||||
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="18" y1="20" x2="34" y2="20">
|
||||
<stop offset="0" stop-color="#2a4fae"/>
|
||||
<stop offset="0.25" stop-color="#4ca0da"/>
|
||||
<stop offset="1" stop-color="#0434a1"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient5)"
|
||||
d="M15 28C15 25.79 18.58 24 23 24C27.41 24 31 25.79 31 28V31C31 33.2 27.41 35 23 35C18.58 35 15 33.2 15 31V28z"
|
||||
transform="matrix(0.9375,0,0,0.9375,15,-8.125)"
|
||||
/>
|
||||
<linearGradient id="gradient6" gradientUnits="userSpaceOnUse" x1="4" y1="-65" x2="50" y2="-65">
|
||||
<stop offset="0" stop-color="#c0d9ff"/>
|
||||
<stop offset="1" stop-color="#5e95e8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#gradient6)"
|
||||
d="M15 27C15 24.79 18.58 23 23 23C27.41 23 31 24.79 31 27C31 29.2 27.41 31 23 31C18.58 31 15 29.2 15 27z"
|
||||
transform="matrix(0.9375,0,0,0.9375,15,-8.125)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
1
assets/static/appico/Meal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 115.21" style="enable-background:new 0 0 122.88 115.21" xml:space="preserve"><g><path d="M29.03,100.46l20.79-25.21l9.51,12.13L41,110.69C33.98,119.61,20.99,110.21,29.03,100.46L29.03,100.46z M53.31,43.05 c1.98-6.46,1.07-11.98-6.37-20.18L28.76,1c-2.58-3.03-8.66,1.42-6.12,5.09L37.18,24c2.75,3.34-2.36,7.76-5.2,4.32L16.94,9.8 c-2.8-3.21-8.59,1.03-5.66,4.7c4.24,5.1,10.8,13.43,15.04,18.53c2.94,2.99-1.53,7.42-4.43,3.69L6.96,18.32 c-2.19-2.38-5.77-0.9-6.72,1.88c-1.02,2.97,1.49,5.14,3.2,7.34L20.1,49.06c5.17,5.99,10.95,9.54,17.67,7.53 c1.03-0.31,2.29-0.94,3.64-1.77l44.76,57.78c2.41,3.11,7.06,3.44,10.08,0.93l0.69-0.57c3.4-2.83,3.95-8,1.04-11.34L50.58,47.16 C51.96,45.62,52.97,44.16,53.31,43.05L53.31,43.05z M65.98,55.65l7.37-8.94C63.87,23.21,99-8.11,116.03,6.29 C136.72,23.8,105.97,66,84.36,55.57l-8.73,11.09L65.98,55.65L65.98,55.65z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
7
assets/static/appico/Newspaper.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" width="256px" height="256px" fill="#000000">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
7
assets/static/appico/Notepad.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 493.474 493.474" xml:space="preserve" width="256px" height="256px" fill="#000000">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
assets/static/appico/account-group.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" /></svg>
|
||||
|
After Width: | Height: | Size: 655 B |
BIN
assets/static/appico/apple.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/application_enterprise.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/static/appico/barcode.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/calendar.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/static/appico/components.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/credit_cards.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/static/appico/cup.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/static/appico/edit.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/static/appico/gear_edit.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/static/appico/house.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/static/appico/shelf.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
assets/static/appico/silverware-fork-knife.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,9H9V2H7V9H5V2H3V9C3,11.12 4.66,12.84 6.75,12.97V22H9.25V12.97C11.34,12.84 13,11.12 13,9V2H11V9M16,6V14H18.5V22H21V2C18.24,2 16,4.24 16,6Z" /></svg>
|
||||
|
After Width: | Height: | Size: 220 B |
BIN
assets/static/appico/users.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/static/appico/view.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
431
assets/static/euskaditech-css/simple.css
Normal file
@@ -0,0 +1,431 @@
|
||||
html {
|
||||
color-scheme: light only;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
/*max-width: 45rem;
|
||||
margin: 0 auto;*/
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.supermesh-indicator {
|
||||
border-top-left-radius: 15px;
|
||||
background-color: greenyellow;
|
||||
border-top: 5px solid green;
|
||||
border-left: 5px solid green;
|
||||
padding: 15px;
|
||||
max-width: 30rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: black;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.supermesh-indicator a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
details.supermesh-indicator summary {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.link,
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#articleID {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
.link,
|
||||
a {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.supermesh-indicator,
|
||||
.no_print,
|
||||
.no_print * {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: beige;
|
||||
border: 2px solid black;
|
||||
font-size: 20px;
|
||||
margin: 3px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* https://coolors.co/palette/ff0000-ff8700-ffd300-deff0a-a1ff0a-0aff99-0aefff-147df5-580aff-be0aff */
|
||||
.rojo {
|
||||
background: #ff0000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn1 {
|
||||
background: #ff0000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
background: #ff8700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn3 {
|
||||
background: #ffd300;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn4 {
|
||||
background: #deff0a;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn5 {
|
||||
background: #a1ff0a;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn6 {
|
||||
background: #0aff99;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn7 {
|
||||
background: #0aefff;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn8 {
|
||||
background: #147df5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-disabled {
|
||||
background: black !important;
|
||||
color: grey !important;
|
||||
}
|
||||
|
||||
.nav-disabled:hover {
|
||||
text-decoration: unset !important;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
width: calc(100% - 11px);
|
||||
}
|
||||
|
||||
input[type="checkbox"]{
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
details input,
|
||||
details select,
|
||||
details textarea {
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
width: calc(100% - 15px);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
line-break: loose;
|
||||
width: fit-content;
|
||||
min-width: 750px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
table tr th {
|
||||
line-break: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
border-bottom: 3px solid black !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.scase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.scase:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
table tr:hover td {
|
||||
text-decoration: underline;
|
||||
background: rgba(200, 200, 200, 0.5);
|
||||
/* color: black; */
|
||||
}
|
||||
|
||||
table tr:hover td.TextBorder {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.TextBorder {
|
||||
color: black;
|
||||
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff,
|
||||
1px 1px 0 #fff;
|
||||
-webkit-text-stroke: 0.25px #fff;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: x-small;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.activeSCButton {
|
||||
border: 7px dashed beige;
|
||||
color: beige;
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
.btn1.activeSCButton {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.btn2.activeSCButton {
|
||||
border-color: #ff8700;
|
||||
color: #ff8700;
|
||||
}
|
||||
|
||||
.btn3.activeSCButton {
|
||||
border-color: #ffd300;
|
||||
color: #ffd300;
|
||||
}
|
||||
|
||||
.btn4.activeSCButton {
|
||||
border-color: #deff0a;
|
||||
color: #deff0a;
|
||||
}
|
||||
|
||||
.btn5.activeSCButton {
|
||||
border-color: #a1ff0a;
|
||||
color: #a1ff0a;
|
||||
}
|
||||
|
||||
.btn6.activeSCButton {
|
||||
border-color: #0aff99;
|
||||
color: #0aff99;
|
||||
}
|
||||
|
||||
.btn7.activeSCButton {
|
||||
border-color: #0aefff;
|
||||
color: #0aefff;
|
||||
}
|
||||
|
||||
.btn8.activeSCButton {
|
||||
border-color: #147df5;
|
||||
color: #147df5;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
#snackbar {
|
||||
visibility: hidden;
|
||||
/* min-width: 250px; */
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
right: 70px;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
#snackbar a {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
#snackbar.show {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
display: flex;
|
||||
background: linear-gradient(to bottom, #d0d8ec, #eef2fa);
|
||||
border-bottom: 1px solid #a2a9b9;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.ribbon-orb {
|
||||
width: 50px !important;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
/* background: url(/icon512_maskable.png); */
|
||||
background-size: 50px 50px;
|
||||
background-position: center middle;
|
||||
border: 1px solid #a2a9b9;
|
||||
margin-right: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ribbon-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1px;
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.ribbon-tabs {
|
||||
display: flex;
|
||||
background: #c8d4eb;
|
||||
border: 1px solid #a2a9b9;
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ribbon-tab {
|
||||
padding: 4px 9px;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid #a2a9b9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ribbon-tab.active {
|
||||
background-color: #eaf0fb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ribbon-panel {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
background-color: #c8d4eb;
|
||||
border: 1px solid #a2a9b9;
|
||||
overflow-x: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.ribbon-button {
|
||||
width: auto;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: black;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid lightskyblue;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ribbon-button img {
|
||||
height: 60px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ribbon-button .label {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ribbon-button.orange {
|
||||
background-color: orange;
|
||||
border-radius: 3px;
|
||||
padding: 2px;
|
||||
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
details[open] .ribbon-panel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
details:not([open]) .ribbon-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset legend {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
pre {
|
||||
font-size: 15px;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
;(function(){
|
||||
// assets/static/gun.js - Deprecated. Replaced by PouchDB (DB module).
|
||||
console.warn('assets/static/gun.js is deprecated and unused.');
|
||||
|
||||
/* UNBUILD */
|
||||
function USE(arg, req){
|
||||
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
|
||||
arg(mod = {exports: {}});
|
||||
@@ -1775,7 +1775,7 @@
|
||||
var wait = 2 * 999;
|
||||
function reconnect(peer){
|
||||
clearTimeout(peer.defer);
|
||||
if(!opt.peers[peer.url]){ return }
|
||||
//if(!opt.peers[peer.url]){ return }
|
||||
if(doc && peer.retry <= 0){ return }
|
||||
peer.retry = (peer.retry || opt.retry+1 || 60) - ((-peer.tried + (peer.tried = +new Date) < wait*4)?1:0);
|
||||
peer.defer = setTimeout(function to(){
|
||||
|
||||
2
assets/static/ico/almond.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#000000" d="M428.3 27.27c-5 0-10.3.34-15.9.95-11 17-20.9 33.24-23.5 48.93l-17.8-2.94c2.5-15.07 9.2-28.82 17.1-41.86-.9.19-1.7.36-2.6.57-36.2 8.57-79.3 26.23-122.5 49.46-5.4 2.89-10.8 5.88-16.1 8.94-.8 4.97-1.8 10.98-3.1 17.58-3.2 16.3-7.8 35.2-16.8 48.1-11.5 16.2-32.6 30.4-51.2 41.6-18.6 11.2-34.7 18.9-34.7 18.9l-7.8-16.2s15.4-7.4 33.2-18.1c17.8-10.8 37.8-25.4 45.7-36.6 5.6-7.9 10.9-25.8 14-41.2.1-.7.3-1.4.4-2.1-73.3 44.9-141.29 103.3-171.34 154.8-15.1 26-23.44 58.6-23.11 90.8 13.76-26.2 29.02-52.8 54.31-77.8l12.66 12.8C68.79 314 53.9 347.3 35.71 381.3c5.89 28.2 19.41 53.8 41.59 71.8 4.04 3.3 8.49 6.4 13.29 9.2 1.75-6.2 4.27-14.6 7.45-23.7 6.46-18.5 14.86-39.8 27.26-53 15-15.9 35.8-21.4 54.5-27 18.7-5.5 35.2-11 45.2-22.6 9.3-11 17.9-36.1 23-57.9 5.1-21.7 7.4-40.2 7.4-40.2l17.8 2.2s-2.3 19.3-7.7 42.1c-5.3 22.8-12.9 49.1-26.9 65.4-14.2 16.7-34.9 22.7-53.7 28.3-18.8 5.6-35.9 10.9-46.4 22.1-8.2 8.7-17.4 28.9-23.5 46.5-3.7 10.7-6.4 20-8 25.9 14.1 5.9 30.1 10 46.8 12.3 9-14.4 16.6-22.2 30.1-76.9l17.4 4.4c-11.6 46.7-19.8 62.2-27.4 74.3 36.2 1.6 73.4-5.3 100-19.7 75.3-41.2 138.2-140.1 173.7-233.8 6.8-17.9 12.5-35.6 17.3-52.7-17.9 15.3-32.8 32-41.1 53.1l-16.8-6.6c13-32.8 38.2-55.4 65-75.5 3.5-16.6 5.9-32.2 7-46.3.5-6.82.8-13.29.7-19.34-6.5 3.66-13.9 7.91-21.7 12.71-24.4 15.03-51.9 35.33-62.8 51.93-5.1 7.7-6 18.9-6.7 31.9-.7 13.1-1 27.9-9.6 41-7.8 12-19.9 18.2-30.5 23.7-10.6 5.5-19.8 10.5-25 17.4-10.3 13.6-20.8 41-27.9 64.4-7.2 23.3-11.4 42.8-11.4 42.8l-17.6-3.8s4.4-20.2 11.8-44.3c7.3-24.1 17.3-52.1 30.7-69.9 8.5-11.3 20.6-17.1 31.1-22.5 10.4-5.5 19.1-10.5 23.8-17.6 5-7.8 6-19.1 6.6-32.1.7-13 1.1-27.9 9.7-40.9v-.1c14.5-21.8 43.1-41.68 68.4-57.25 11.1-6.84 21.4-12.65 29.7-17.11-.3-1.99-.7-3.9-1.1-5.71-2.6-11.57-7-18.85-12-22.32-7.3-5.12-18.1-8.01-31.7-8.55-1.7-.1-3.4-.1-5.2-.1zm-113 58.9l17.4 4.5s-4.1 15.83-10.7 34.63c-6.7 18.9-15.4 40.6-27.3 54.4-20.7 24.3-49.8 36.9-77.5 49-27.7 12.1-54 23.9-71.7 43.8v.1c-13.8 15.6-28.7 47.3-39.3 74.5-10.73 27.3-17.58 50.2-17.58 50.2l-17.24-5.2s7.04-23.5 18.02-51.5c11-28 25.4-60.4 42.7-79.9 21-24 50.2-36.3 77.9-48.5 27.7-12.1 53.8-23.9 71.1-44.1 8.2-9.7 17.5-30.7 23.9-48.8 6.4-18 10.3-33.13 10.3-33.13zM197.6 273.2l17.8 2.6c-2.8 19.4-11.8 33.8-23.2 44.2-11.4 10.3-25 17.1-37.9 23.5l-8-16c12.8-6.5 24.8-12.7 33.8-20.9 9-8.1 15.3-17.8 17.5-33.4zm180.3 7.3l16.4 7.2s-9.6 22-23.6 47.7c-14 25.8-31.9 55.4-51 72.3-13.6 12.1-35 21.6-53.6 28.9-18.6 7.4-34.2 12.1-34.2 12.1l-5.2-17.2s15-4.5 32.8-11.6c17.8-7 38.6-17 48.2-25.6 15.3-13.5 33.5-42.4 47.2-67.4 13.6-25.1 23-46.4 23-46.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
assets/static/ico/offline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#d92d27;}</style></defs><title>no-wifi</title><path class="cls-1" d="M101.68,32.93,32.92,101.68a49.29,49.29,0,0,0,77.83-40.24h0A49.34,49.34,0,0,0,108,45.15a48.85,48.85,0,0,0-6.32-12.22ZM24,93.5,93.49,24A49.31,49.31,0,0,0,24,93.5Z"/><path d="M30.29,52A3,3,0,0,1,26,51.63v0a3,3,0,0,1,.34-4.24h0A59.27,59.27,0,0,1,43.27,37a48,48,0,0,1,36.4.31A61,61,0,0,1,96.46,47.9a1.29,1.29,0,0,1,.17.16,3,3,0,0,1,.27,4.07,1.54,1.54,0,0,1-.17.19,3,3,0,0,1-4.16.19A55.23,55.23,0,0,0,77.47,43a41.86,41.86,0,0,0-32.08-.27A53.38,53.38,0,0,0,30.29,52ZM61.44,76.09A6.59,6.59,0,1,1,56.77,78h0a6.62,6.62,0,0,1,4.67-1.93ZM50.05,72.5a3,3,0,0,1-4.16-.35,1.37,1.37,0,0,1-.16-.18,3,3,0,0,1,.43-4.07l.17-.14a27.64,27.64,0,0,1,7.33-4.33,21.68,21.68,0,0,1,7.84-1.52,21.35,21.35,0,0,1,7.8,1.47,27.12,27.12,0,0,1,7.34,4.36A3,3,0,0,1,77.08,72h0a3,3,0,0,1-2,1.1,3.06,3.06,0,0,1-2.21-.66h0a21.27,21.27,0,0,0-5.62-3.37,15.12,15.12,0,0,0-11.47,0,22,22,0,0,0-5.7,3.41Zm-9.56-9.71-.15.13a3.06,3.06,0,0,1-2.08.67,3,3,0,0,1-2-1,1,1,0,0,1-.14-.15,3,3,0,0,1,.34-4.16,45.78,45.78,0,0,1,12.36-8,30.76,30.76,0,0,1,25.6.42,45.74,45.74,0,0,1,12.11,8.41l.08.07a3.09,3.09,0,0,1,.87,2,3,3,0,0,1-.82,2.15l-.07.08a3,3,0,0,1-2,.87,3,3,0,0,1-2.15-.81A40.13,40.13,0,0,0,72,56.28a24.75,24.75,0,0,0-21-.35,39.68,39.68,0,0,0-10.5,6.86Z"/><path class="cls-2" d="M61.44,0A61.31,61.31,0,1,1,38,4.66,61.29,61.29,0,0,1,61.44,0Zm40.24,32.93L32.93,101.68A49.44,49.44,0,0,0,80.31,107,49.53,49.53,0,0,0,107,80.3a49,49,0,0,0,3.73-18.86h0a48.93,48.93,0,0,0-9.08-28.51ZM24,93.5,93.5,24A49.32,49.32,0,0,0,24,93.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
@@ -1,4 +1,6 @@
|
||||
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
// assets/static/open.js - Deprecated. Part of Gun library, not used after migration to PouchDB.
|
||||
console.warn('assets/static/open.js is deprecated and unused.');
|
||||
var Gun = (typeof window !== "undefined")? window.Gun || {} : {};
|
||||
|
||||
Gun.chain.open = function(cb, opt, at, depth){ // this is a recursive function, BEWARE!
|
||||
depth = depth || 1;
|
||||
|
||||
1033
assets/static/qrcode/barcode.js
Normal file
1
assets/static/qrcode/html5-qrcode.min.js
vendored
Normal file
2
assets/static/qrcode/qrcode.min.js
vendored
Normal file
@@ -1,6 +1,6 @@
|
||||
;(function(){
|
||||
// assets/static/sea.js - Deprecated. Replaced by CryptoJS AES usage in app_modules.js.
|
||||
console.warn('assets/static/sea.js is deprecated and unused.');
|
||||
|
||||
/* UNBUILD */
|
||||
function USE(arg, req){
|
||||
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
|
||||
arg(mod = {exports: {}});
|
||||
|
||||
21
build.py
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
def get_all_files(directory):
|
||||
files = []
|
||||
@@ -12,27 +14,36 @@ def get_all_files(directory):
|
||||
return files
|
||||
|
||||
PREFETCH = ""
|
||||
VERSIONCO = "2025-07-31_2"
|
||||
VERSIONCO = "2025-08"
|
||||
HANDLEPARSE = get_all_files("src")
|
||||
|
||||
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
|
||||
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
|
||||
# Combine assets from JSON and recursively found files
|
||||
ASSETS = get_all_files("assets")
|
||||
|
||||
for asset in ASSETS:
|
||||
if asset != "sw.js":
|
||||
PREFETCH += f'<link rel="prefetch" href="{asset}" />\n'
|
||||
for src in HANDLEPARSE:
|
||||
if src != "sw.js":
|
||||
PREFETCH += f'<link rel="prefetch" href="{src}" />\n'
|
||||
|
||||
if os.path.exists("dist"):
|
||||
shutil.rmtree("dist")
|
||||
shutil.copytree("assets","dist", dirs_exist_ok=True)
|
||||
|
||||
os.system("rm -r dist ; mkdir -p dist ; cp -r assets/* dist/")
|
||||
def replace_handles(string):
|
||||
string = string.replace("%%PREFETCH%%", PREFETCH)
|
||||
string = string.replace("%%VERSIONCO%%", VERSIONCO)
|
||||
string = string.replace("%%TITLE%%", "TeleSec")
|
||||
string = string.replace("%%HOSTER%%", HOSTER)
|
||||
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
|
||||
return string
|
||||
|
||||
|
||||
for file in HANDLEPARSE:
|
||||
print(file)
|
||||
with open("src/" + file, "r") as f1:
|
||||
with open("src/" + file, "r", encoding="utf-8") as f1:
|
||||
out = replace_handles(f1.read())
|
||||
with open("dist/" + file, "w") as f2:
|
||||
with open("dist/" + file, "w", encoding="utf-8") as f2:
|
||||
f2.write(out)
|
||||
|
||||
175
decrypt.js
Normal file
@@ -0,0 +1,175 @@
|
||||
function TS_decrypt(input, secret, callback, table, id) {
|
||||
// Accept objects or plaintext strings. Also support legacy RSA{...} AES-encrypted entries.
|
||||
var __ts_sync = true;
|
||||
if (typeof input !== "string") {
|
||||
try {
|
||||
callback(input, false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return __ts_sync;
|
||||
}
|
||||
|
||||
// Legacy encrypted format: RSA{...}
|
||||
if (
|
||||
input.startsWith("RSA{") &&
|
||||
input.endsWith("}") &&
|
||||
typeof CryptoJS !== "undefined"
|
||||
) {
|
||||
try {
|
||||
var data = input.slice(4, -1);
|
||||
var words = CryptoJS.AES.decrypt(data, secret);
|
||||
var decryptedUtf8 = null;
|
||||
try {
|
||||
decryptedUtf8 = words.toString(CryptoJS.enc.Utf8);
|
||||
} catch (utfErr) {
|
||||
// Malformed UTF-8 — try Latin1 fallback
|
||||
try {
|
||||
decryptedUtf8 = words.toString(CryptoJS.enc.Latin1);
|
||||
} catch (latinErr) {
|
||||
console.warn(
|
||||
"TS_decrypt: failed to decode decrypted bytes",
|
||||
utfErr,
|
||||
latinErr
|
||||
);
|
||||
try {
|
||||
callback(input, false);
|
||||
} catch (ee) {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
var parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(decryptedUtf8);
|
||||
} catch (pe) {
|
||||
// If JSON parsing fails, the decrypted string may be raw Latin1 bytes.
|
||||
// Try to convert Latin1 byte string -> UTF-8 and parse again.
|
||||
try {
|
||||
if (
|
||||
typeof TextDecoder !== "undefined" &&
|
||||
typeof decryptedUtf8 === "string"
|
||||
) {
|
||||
var bytes = new Uint8Array(decryptedUtf8.length);
|
||||
for (var _i = 0; _i < decryptedUtf8.length; _i++)
|
||||
bytes[_i] = decryptedUtf8.charCodeAt(_i) & 0xff;
|
||||
var converted = new TextDecoder("utf-8").decode(bytes);
|
||||
try {
|
||||
parsed = JSON.parse(converted);
|
||||
decryptedUtf8 = converted;
|
||||
} catch (e2) {
|
||||
parsed = decryptedUtf8;
|
||||
}
|
||||
} else {
|
||||
parsed = decryptedUtf8;
|
||||
}
|
||||
} catch (convErr) {
|
||||
parsed = decryptedUtf8;
|
||||
}
|
||||
}
|
||||
try {
|
||||
callback(parsed, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Migrate to plaintext in DB if table/id provided
|
||||
if (table && id && window.DB && DB.put && typeof parsed !== "string") {
|
||||
DB.put(table, id, parsed).catch(() => {});
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error("TS_decrypt: invalid encrypted payload", e);
|
||||
try {
|
||||
callback(input, false);
|
||||
} catch (ee) {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (input.startsWith("SEA{") && input.endsWith("}")) {
|
||||
__ts_sync = false;
|
||||
SEA.decrypt(input, secret, (decrypted) => {
|
||||
try {
|
||||
callback(decrypted, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
return __ts_sync;
|
||||
}
|
||||
|
||||
// Try to parse JSON strings and migrate to object
|
||||
try {
|
||||
var parsed = JSON.parse(input);
|
||||
try {
|
||||
callback(parsed, false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (table && id && window.DB && DB.put) {
|
||||
DB.put(table, id, parsed).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, return raw string
|
||||
try {
|
||||
callback(input, false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
return __ts_sync;
|
||||
}
|
||||
|
||||
function recursiveTSDecrypt(input, secret = "") {
|
||||
// Skip null values (do not show on decrypted output)
|
||||
if (input === null) return null;
|
||||
if (typeof input === "string") {
|
||||
let result = null;
|
||||
let resolver = null;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
const sync = TS_decrypt(input, secret, (decrypted) => {
|
||||
result = decrypted;
|
||||
if (resolver) resolver(decrypted);
|
||||
});
|
||||
if (sync === false) return promise;
|
||||
return result;
|
||||
} else if (Array.isArray(input)) {
|
||||
const mapped = input.map((item) => recursiveTSDecrypt(item, secret));
|
||||
if (mapped.some((v) => v && typeof v.then === "function")) {
|
||||
return Promise.all(mapped).then((values) => values.filter((v) => v !== null && typeof v !== 'undefined'));
|
||||
}
|
||||
return mapped.filter((v) => v !== null && typeof v !== 'undefined');
|
||||
} else if (typeof input === "object" && input !== null) {
|
||||
const keys = Object.keys(input);
|
||||
const mapped = keys.map((k) => recursiveTSDecrypt(input[k], secret));
|
||||
if (mapped.some((v) => v && typeof v.then === "function")) {
|
||||
return Promise.all(mapped).then((values) => {
|
||||
const out = {};
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const val = values[i];
|
||||
if (val !== null && typeof val !== 'undefined') out[keys[i]] = val;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
} else {
|
||||
const out = {};
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const val = mapped[i];
|
||||
if (val !== null && typeof val !== 'undefined') out[keys[i]] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
gun.get(TABLE).load((DATA) => {
|
||||
var plain2 = recursiveTSDecrypt(DATA, SECRET);
|
||||
plain2.then(function (result) {
|
||||
download(
|
||||
`Export TeleSec ${GROUPID} Decrypted.json.txt`,
|
||||
JSON.stringify(result)
|
||||
);
|
||||
});
|
||||
});
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 103 KiB |
@@ -8,5 +8,6 @@
|
||||
<body>
|
||||
<h1>Si ves esto, ha ocurrido un fallo critico en la compilación de TeleSec</h1>
|
||||
<p>Esto NUNCA deberia de ser mostrado.</p>
|
||||
<a href="dist/index.html">Intenta recargar la página</a>
|
||||
</body>
|
||||
</html>
|
||||
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
181
src/app_logic.js
@@ -1,40 +1,26 @@
|
||||
function fixfloat(number) {
|
||||
return (parseFloat(number).toPrecision(8));
|
||||
}
|
||||
|
||||
function tableScroll(query) {
|
||||
$(query).doubleScroll();
|
||||
}
|
||||
//var secretTokenEl = document.getElementById("secretToken");
|
||||
var groupIdEl = document.getElementById("groupId");
|
||||
var container = document.getElementById("container");
|
||||
function LinkAccount(LinkAccount_group, LinkAccount_secret, refresh = false) {
|
||||
GROUPID = LinkAccount_group.toUpperCase();
|
||||
SECRET = LinkAccount_secret.toUpperCase();
|
||||
|
||||
localStorage.setItem("TELESEC_AUTO", "YES");
|
||||
localStorage.setItem("TELESEC_groupid", GROUPID);
|
||||
localStorage.setItem("TELESEC_secret", SECRET);
|
||||
|
||||
TABLE = GROUPID + ":telesec.tech.eus";
|
||||
//secretTokenEl.innerText = SECRET;
|
||||
groupIdEl.innerText = GROUPID;
|
||||
document.getElementById("LinkAccount_details").open = false;
|
||||
if (refresh == true) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (localStorage.getItem("TELESEC_AUTO") == "YES") {
|
||||
LinkAccount(
|
||||
localStorage.getItem("TELESEC_groupid"),
|
||||
localStorage.getItem("TELESEC_secret")
|
||||
);
|
||||
}
|
||||
if (urlParams.get("login") != null) {
|
||||
LinkAccount(
|
||||
urlParams.get("login").split(":")[0],
|
||||
urlParams.get("login").split(":")[1]
|
||||
);
|
||||
//location.search = "";
|
||||
}
|
||||
function open_page(params) {
|
||||
if (SUB_LOGGED_IN != true) {
|
||||
// Clear stored event listeners and timers
|
||||
EventListeners.GunJS = [];
|
||||
EventListeners.Timeout.forEach(ev => clearTimeout(ev));
|
||||
EventListeners.Timeout = [];
|
||||
EventListeners.Interval.forEach(ev => clearInterval(ev));
|
||||
EventListeners.Interval = [];
|
||||
EventListeners.QRScanner.forEach(ev => ev.clear());
|
||||
EventListeners.QRScanner = [];
|
||||
EventListeners.Custom.forEach(ev => ev());
|
||||
EventListeners.Custom = [];
|
||||
|
||||
if (SUB_LOGGED_IN != true && params != "login,setup") {
|
||||
PAGES["login"].index();
|
||||
return;
|
||||
}
|
||||
@@ -49,30 +35,24 @@ function open_page(params) {
|
||||
}
|
||||
PAGES[app].edit(path[1]);
|
||||
}
|
||||
|
||||
function setUrlHash(hash) {
|
||||
location.hash = "#" + hash;
|
||||
|
||||
// Handle quick search transfer
|
||||
if (hash === 'buscar') {
|
||||
const quickSearchInput = document.getElementById('quickSearchInput');
|
||||
if (quickSearchInput && quickSearchInput.value.trim()) {
|
||||
// Store the search term temporarily
|
||||
sessionStorage.setItem('telesec_quick_search', quickSearchInput.value.trim());
|
||||
quickSearchInput.value = ''; // Clear the input
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onhashchange = () => {
|
||||
try {
|
||||
if (EVENTLISTENER != null) {
|
||||
try {
|
||||
EVENTLISTENER.off();
|
||||
EVENTLISTENER = null;
|
||||
EVENTLISTENER2.off();
|
||||
EVENTLISTENER2 = null;
|
||||
// TypeError: Cannot read properties of null (reading 'off')
|
||||
} catch (error) {
|
||||
if (!error.name == "TypeError") {
|
||||
console.debug("EVENTLISTENER error", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("EVENTLISTENER onhashchange", e);
|
||||
}
|
||||
|
||||
open_page(location.hash.replace("#", ""));
|
||||
};
|
||||
|
||||
function download(filename, text) {
|
||||
var element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
@@ -80,14 +60,15 @@ function download(filename, text) {
|
||||
"data:application/octet-stream;charset=utf-8," + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
|
||||
element.click();
|
||||
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
function resizeInputImage(
|
||||
file,
|
||||
callback,
|
||||
@@ -95,70 +76,94 @@ function resizeInputImage(
|
||||
targetQuality = 0.75
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (event) {
|
||||
|
||||
reader.onload = function(event) {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
img.onload = function() {
|
||||
const aspectRatio = img.width / img.height;
|
||||
const targetWidth = targetHeight * aspectRatio;
|
||||
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
|
||||
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
||||
|
||||
|
||||
// Get resized image as Blob
|
||||
const dataURL = canvas.toDataURL("image/jpeg", targetQuality);
|
||||
callback(dataURL);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function CurrentISODate() {
|
||||
return new Date().toISOString().split("T")[0].replace("T", " ");
|
||||
}
|
||||
|
||||
function CurrentISOTime() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function fixGunLocalStorage() {
|
||||
localStorage.removeItem("radata");
|
||||
removeCache();
|
||||
location.reload();
|
||||
}
|
||||
function betterGunPut(ref, data) {
|
||||
ref.put(data, (ack) => {
|
||||
if (ack.err) {
|
||||
toastr.error(
|
||||
ack.err + "<br>Pulsa aqui para reiniciar la app",
|
||||
"Error al guardar",
|
||||
{ onclick: () => fixGunLocalStorage() }
|
||||
);
|
||||
} else {
|
||||
console.debug("Guardado correctamente");
|
||||
|
||||
// Heartbeat: store a small "last seen" doc locally and replicate to remote when available
|
||||
// setInterval(() => {
|
||||
// if (typeof DB !== 'undefined') {
|
||||
// DB.put('heartbeat', getDBName() || 'heartbeat', 'heartbeat-' + CurrentISOTime());
|
||||
// }
|
||||
// }, 5000);
|
||||
|
||||
|
||||
function betterSorter(a, b) {
|
||||
// 1. Fecha (ascending)
|
||||
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
|
||||
return a.Fecha > b.Fecha ? -1 : 1;
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
ref.put(data);
|
||||
}, 250);
|
||||
setTimeout(() => {
|
||||
ref.put(data);
|
||||
}, 500);
|
||||
}
|
||||
setInterval(() => {
|
||||
betterGunPut(
|
||||
gun.get(TABLE).get("heartbeat"),
|
||||
"heartbeat-" + CurrentISOTime()
|
||||
);
|
||||
gun.get(TABLE).get("heartbeat").load(console.debug);
|
||||
}, 5000);
|
||||
gun.get(TABLE).on((data) => {
|
||||
var e = true;
|
||||
});
|
||||
// 2. Region (ascending, from SC_Personas if Persona exists)
|
||||
const regionA =
|
||||
a.Persona && SC_Personas[a.Persona]
|
||||
? SC_Personas[a.Persona].Region || ""
|
||||
: a.Region || "";
|
||||
const regionB =
|
||||
b.Persona && SC_Personas[b.Persona]
|
||||
? SC_Personas[b.Persona].Region || ""
|
||||
: b.Region || "";
|
||||
if (regionA !== regionB) {
|
||||
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
|
||||
const nombrePersonaA =
|
||||
a.Persona && SC_Personas[a.Persona]
|
||||
? SC_Personas[a.Persona].Nombre || ""
|
||||
: "";
|
||||
const nombrePersonaB =
|
||||
b.Persona && SC_Personas[b.Persona]
|
||||
? SC_Personas[b.Persona].Nombre || ""
|
||||
: "";
|
||||
if (nombrePersonaA !== nombrePersonaB) {
|
||||
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase()
|
||||
? -1
|
||||
: 1;
|
||||
}
|
||||
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
|
||||
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
|
||||
return a.Nombre.toLowerCase() < b.Nombre.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
// 5. Asunto (ascending, from a.Asunto/b.Asunto)
|
||||
if (a.Asunto && b.Asunto && a.Asunto !== b.Asunto) {
|
||||
return a.Asunto.toLowerCase() < b.Asunto.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
1401
src/app_modules.js
@@ -1,25 +1,65 @@
|
||||
var EVENTLISTENER = null;
|
||||
var EVENTLISTENER2 = null;
|
||||
var EventListeners = {
|
||||
GunJS: [],
|
||||
Timeout: [],
|
||||
Interval: [],
|
||||
QRScanner: [],
|
||||
Custom: [],
|
||||
};
|
||||
|
||||
function safeuuid(prefix = "AXLUID_") {
|
||||
if (!crypto.randomUUID) {
|
||||
// Fallback for environments without crypto.randomUUID()
|
||||
const randomPart = Math.random().toString(36).substring(2, 10);
|
||||
return prefix + randomPart;
|
||||
}
|
||||
return prefix + crypto.randomUUID().split("-")[4];
|
||||
}
|
||||
var urlParams = new URLSearchParams(location.search);
|
||||
if (urlParams.get("hidenav") == "yes") {
|
||||
var AC_BYPASS = false;
|
||||
if (urlParams.get("ac_bypass") == "yes") {
|
||||
AC_BYPASS = true;
|
||||
}
|
||||
if (urlParams.get("hidenav") != undefined) {
|
||||
document.getElementById("header_hide_query").style.display = "none";
|
||||
}
|
||||
var GROUPID = "";
|
||||
// getDBName: prefer explicit CouchDB dbname from settings. Single-group model: default to 'telesec'
|
||||
function getDBName() {
|
||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
|
||||
if (dbname && dbname.trim() !== '') return dbname.trim();
|
||||
return 'telesec';
|
||||
}
|
||||
// const PUBLIC_KEY = "~cppGiuA4UFUPGTDoC-4r2izVC3F7MfpaCmF3iZdESN4.vntmjgbAVUpF_zfinYY6EKVFuuTYxh5xOrL4KmtdTmc"
|
||||
var TABLE = GROUPID + ":telesec.tech.eus";
|
||||
const RELAYS = [
|
||||
"https://gun-es01.tech.eus/gun",
|
||||
"https://gun-es02.tech.eus/gun",
|
||||
"https://gun-es03.tech.eus/gun",
|
||||
"https://gun-es04.tech.eus/gun",
|
||||
"https://gun-es05.tech.eus/gun",
|
||||
"https://gun-es06.tech.eus/gun",
|
||||
// "https://gun-es07.tech.eus/gun", // No he podido instalar el nodo.
|
||||
"https://gun-manhattan.herokuapp.com/gun",
|
||||
"https://peer.wallie.io/gun",
|
||||
"https://gun.defucc.me/gun",
|
||||
];
|
||||
// `TABLE` variable removed. The CouchDB database name should be configured via the login/setup form
|
||||
// and passed to `DB.init({ dbname: '<your-db>' })` so it becomes the app's primary DB.
|
||||
// Legacy relay list removed (migrated to CouchDB/PouchDB)
|
||||
const RELAYS = [];
|
||||
var SECRET = "";
|
||||
var SUB_LOGGED_IN = false;
|
||||
var SUB_LOGGED_IN_DETAILS = false;
|
||||
var SUB_LOGGED_IN_ID = false;
|
||||
var SUB_LOGGED_IN_ID = false;
|
||||
var SAVE_WAIT = 500;
|
||||
var SC_Personas = {};
|
||||
var PeerConnectionInterval = 5000;
|
||||
if (urlParams.get("sublogin") != null) {
|
||||
SUB_LOGGED_IN = true;
|
||||
SUB_LOGGED_IN_ID = urlParams.get("sublogin");
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||
var sli = 15;
|
||||
var slii = setInterval(() => {
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||
sli-=1;
|
||||
if (sli < 0) {
|
||||
clearInterval(slii);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
function LogOutTeleSec() {
|
||||
SUB_LOGGED_IN = false;
|
||||
SUB_LOGGED_IN_DETAILS = false;
|
||||
SUB_LOGGED_IN_ID = false;
|
||||
document.getElementById("loading").style.display = "block";
|
||||
//Remove sublogin from URL and reload
|
||||
urlParams.delete("sublogin");
|
||||
history.replaceState(null, "", "?" + urlParams.toString());
|
||||
location.reload();
|
||||
}
|
||||
|
||||
619
src/db.js
Normal file
@@ -0,0 +1,619 @@
|
||||
// TeleSec Database Layer - Supports both PouchDB and remoteStorage.js
|
||||
// - Users can choose their preferred backend via login settings
|
||||
// - Provides unified API: init, put, get, del, map, list, replicate
|
||||
// - Stores records as docs with _id = "<table>:<id>" and field `data` containing either plain object or encrypted string
|
||||
|
||||
var DB = (function () {
|
||||
// Constants
|
||||
const ENCRYPTED_PREFIX = 'RSA{';
|
||||
const ENCRYPTED_SUFFIX = '}';
|
||||
|
||||
let backend = null; // 'pouchdb' or 'remotestorage'
|
||||
let callbacks = {}; // table -> [cb]
|
||||
let docCache = {}; // _id -> last data snapshot (stringified)
|
||||
|
||||
// PouchDB-specific state
|
||||
let pouchLocal = null;
|
||||
let pouchRemote = null;
|
||||
let pouchChanges = null;
|
||||
let pouchRepPush = null;
|
||||
let pouchRepPull = null;
|
||||
|
||||
// remoteStorage-specific state
|
||||
let rsClient = null;
|
||||
let rsModule = null;
|
||||
|
||||
// ==================== PouchDB Backend Implementation ====================
|
||||
|
||||
function ensurePouchLocal() {
|
||||
if (pouchLocal) return;
|
||||
try {
|
||||
const localName = 'telesec';
|
||||
pouchLocal = new PouchDB(localName);
|
||||
if (pouchChanges) {
|
||||
try { pouchChanges.cancel(); } catch (e) {}
|
||||
}
|
||||
pouchChanges = pouchLocal.changes({ live: true, since: 'now', include_docs: true }).on('change', onPouchChange);
|
||||
} catch (e) {
|
||||
console.warn('ensurePouchLocal error', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onPouchChange(change) {
|
||||
const doc = change.doc;
|
||||
if (!doc || !doc._id) return;
|
||||
const [table, id] = doc._id.split(':');
|
||||
|
||||
// handle deletes
|
||||
if (change.deleted || doc._deleted) {
|
||||
delete docCache[doc._id];
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach(cb => {
|
||||
try { cb(null, id); } catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle insert/update
|
||||
try {
|
||||
const now = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data);
|
||||
const prev = docCache[doc._id];
|
||||
if (prev === now) return; // no meaningful change
|
||||
docCache[doc._id] = now;
|
||||
} catch (e) { /* ignore cache errors */ }
|
||||
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach(cb => {
|
||||
try { cb(doc.data, id); } catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function initPouchDB(opts) {
|
||||
const localName = 'telesec';
|
||||
try {
|
||||
if (opts && opts.secret) {
|
||||
SECRET = opts.secret;
|
||||
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
pouchLocal = new PouchDB(localName);
|
||||
|
||||
if (opts.remoteServer) {
|
||||
try {
|
||||
const server = opts.remoteServer.replace(/\/$/, '');
|
||||
const dbname = encodeURIComponent((opts.dbname || localName));
|
||||
let authPart = '';
|
||||
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
|
||||
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
|
||||
if (opts.username) pouchRemote = new PouchDB(remoteUrl.replace(/:\/\//, '://' + authPart));
|
||||
else pouchRemote = new PouchDB(remoteUrl);
|
||||
replicatePouchToRemote();
|
||||
} catch (e) {
|
||||
console.warn('PouchDB Remote init error', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pouchChanges) pouchChanges.cancel();
|
||||
pouchChanges = pouchLocal.changes({ live: true, since: 'now', include_docs: true }).on('change', onPouchChange);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function replicatePouchToRemote() {
|
||||
ensurePouchLocal();
|
||||
if (!pouchLocal || !pouchRemote) return;
|
||||
try { if (pouchRepPush && pouchRepPush.cancel) pouchRepPush.cancel(); } catch (e) {}
|
||||
try { if (pouchRepPull && pouchRepPull.cancel) pouchRepPull.cancel(); } catch (e) {}
|
||||
|
||||
pouchRepPush = PouchDB.replicate(pouchLocal, pouchRemote, { live: true, retry: true })
|
||||
.on('error', function (err) { console.warn('Replication push error', err); });
|
||||
pouchRepPull = PouchDB.replicate(pouchRemote, pouchLocal, { live: true, retry: true })
|
||||
.on('error', function (err) { console.warn('Replication pull error', err); });
|
||||
}
|
||||
|
||||
async function pouchPut(table, id, data) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
const existing = await pouchLocal.get(_id).catch(() => null);
|
||||
if (data === null) {
|
||||
if (existing) await pouchLocal.remove(existing);
|
||||
return;
|
||||
}
|
||||
const doc = existing || { _id: _id };
|
||||
var toStore = data;
|
||||
try {
|
||||
var isEncryptedString = (typeof data === 'string' && data.startsWith(ENCRYPTED_PREFIX) && data.endsWith(ENCRYPTED_SUFFIX));
|
||||
if (!isEncryptedString && typeof TS_encrypt === 'function' && typeof SECRET !== 'undefined' && SECRET) {
|
||||
toStore = await new Promise(resolve => {
|
||||
try { TS_encrypt(data, SECRET, enc => resolve(enc)); } catch (e) { resolve(data); }
|
||||
});
|
||||
}
|
||||
} catch (e) { toStore = data; }
|
||||
doc.data = toStore;
|
||||
doc.table = table;
|
||||
doc.ts = new Date().toISOString();
|
||||
if (existing) doc._rev = existing._rev;
|
||||
await pouchLocal.put(doc);
|
||||
|
||||
// manually trigger callbacks for local update
|
||||
onPouchChange({ doc: doc });
|
||||
} catch (e) {
|
||||
console.error('PouchDB.put error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function pouchGet(table, id) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
const doc = await pouchLocal.get(_id);
|
||||
return doc.data;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function pouchList(table) {
|
||||
ensurePouchLocal();
|
||||
try {
|
||||
const res = await pouchLocal.allDocs({ include_docs: true, startkey: table + ':', endkey: table + ':\uffff' });
|
||||
return res.rows.map(r => {
|
||||
const id = r.id.split(':')[1];
|
||||
try { docCache[r.id] = typeof r.doc.data === 'string' ? r.doc.data : JSON.stringify(r.doc.data); } catch (e) {}
|
||||
return { id: id, data: r.doc.data };
|
||||
});
|
||||
} catch (e) { return []; }
|
||||
}
|
||||
|
||||
function pouchMap(table, cb) {
|
||||
ensurePouchLocal();
|
||||
callbacks[table] = callbacks[table] || [];
|
||||
callbacks[table].push(cb);
|
||||
pouchList(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
|
||||
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
|
||||
}
|
||||
|
||||
// PouchDB Attachment methods
|
||||
function dataURLtoBlob(dataurl) {
|
||||
const arr = dataurl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) u8arr[n] = bstr.charCodeAt(n);
|
||||
return new Blob([u8arr], { type: mime });
|
||||
}
|
||||
|
||||
async function pouchPutAttachment(table, id, name, dataUrlOrBlob, contentType) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
let doc = await pouchLocal.get(_id).catch(() => null);
|
||||
if (!doc) {
|
||||
await pouchLocal.put({ _id: _id, table: table, ts: new Date().toISOString(), data: {} });
|
||||
doc = await pouchLocal.get(_id);
|
||||
}
|
||||
let blob = dataUrlOrBlob;
|
||||
if (typeof dataUrlOrBlob === 'string' && dataUrlOrBlob.indexOf('data:') === 0) blob = dataURLtoBlob(dataUrlOrBlob);
|
||||
const type = contentType || (blob && blob.type) || 'application/octet-stream';
|
||||
await pouchLocal.putAttachment(_id, name, doc._rev, blob, type);
|
||||
return true;
|
||||
} catch (e) { console.error('putAttachment error', e); return false; }
|
||||
}
|
||||
|
||||
async function pouchGetAttachment(table, id, name) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
const blob = await pouchLocal.getAttachment(_id, name);
|
||||
if (!blob) return null;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => resolve(e.target.result);
|
||||
reader.onerror = e => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function pouchListAttachments(table, id) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
const doc = await pouchLocal.get(_id, { attachments: true });
|
||||
if (!doc || !doc._attachments) return [];
|
||||
const out = [];
|
||||
for (const name of Object.keys(doc._attachments)) {
|
||||
try {
|
||||
const att = doc._attachments[name];
|
||||
if (att && att.data) {
|
||||
const content_type = att.content_type || 'application/octet-stream';
|
||||
const durl = 'data:' + content_type + ';base64,' + att.data;
|
||||
out.push({ name: name, dataUrl: durl, content_type: content_type });
|
||||
continue;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const durl = await pouchGetAttachment(table, id, name);
|
||||
out.push({ name: name, dataUrl: durl, content_type: null });
|
||||
} catch (e) {
|
||||
out.push({ name: name, dataUrl: null, content_type: null });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
try {
|
||||
const doc = await pouchLocal.get(_id).catch(() => null);
|
||||
if (!doc || !doc._attachments) return [];
|
||||
const out = [];
|
||||
for (const name of Object.keys(doc._attachments)) {
|
||||
try {
|
||||
const durl = await pouchGetAttachment(table, id, name);
|
||||
out.push({ name: name, dataUrl: durl, content_type: null });
|
||||
} catch (e) { out.push({ name: name, dataUrl: null, content_type: null }); }
|
||||
}
|
||||
return out;
|
||||
} catch (e2) { return []; }
|
||||
}
|
||||
}
|
||||
|
||||
async function pouchDeleteAttachment(table, id, name) {
|
||||
ensurePouchLocal();
|
||||
const _id = table + ':' + id;
|
||||
try {
|
||||
const doc = await pouchLocal.get(_id);
|
||||
if (!doc || !doc._attachments || !doc._attachments[name]) return false;
|
||||
delete doc._attachments[name];
|
||||
await pouchLocal.put(doc);
|
||||
return true;
|
||||
} catch (e) { console.error('deleteAttachment error', e); return false; }
|
||||
}
|
||||
|
||||
// ==================== remoteStorage Backend Implementation ====================
|
||||
|
||||
function initRemoteStorage(opts) {
|
||||
try {
|
||||
if (opts && opts.secret) {
|
||||
SECRET = opts.secret;
|
||||
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
|
||||
}
|
||||
|
||||
// Initialize RemoteStorage client
|
||||
rsClient = new RemoteStorage({ logging: opts.logging || false });
|
||||
|
||||
// Define TeleSec data module
|
||||
rsClient.access.claim('telesec', 'rw');
|
||||
rsClient.caching.enable('/telesec/');
|
||||
|
||||
// Define module to handle our data structure
|
||||
rsModule = rsClient.scope('/telesec/');
|
||||
|
||||
// Listen for changes
|
||||
rsModule.on('change', onRemoteStorageChange);
|
||||
|
||||
// Auto-connect if user address is provided
|
||||
if (opts.rsUserAddress) {
|
||||
rsClient.connect(opts.rsUserAddress, opts.rsToken || undefined).then(() => {
|
||||
console.log('RemoteStorage connected successfully');
|
||||
}).catch((err) => {
|
||||
console.warn('RemoteStorage connection error', err);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error('RemoteStorage init error', e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoteStorageChange(event) {
|
||||
try {
|
||||
if (!event || !event.path) return;
|
||||
|
||||
// Parse path: /telesec/table/id
|
||||
const pathParts = event.path.split('/').filter(p => p);
|
||||
if (pathParts.length < 3 || pathParts[0] !== 'telesec') return;
|
||||
|
||||
const table = pathParts[1];
|
||||
const id = pathParts[2];
|
||||
const _id = table + ':' + id;
|
||||
|
||||
// Handle deletion
|
||||
if (!event.newValue) {
|
||||
delete docCache[_id];
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach(cb => {
|
||||
try { cb(null, id); } catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle insert/update
|
||||
try {
|
||||
const data = event.newValue;
|
||||
const now = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
const prev = docCache[_id];
|
||||
if (prev === now) return; // no meaningful change
|
||||
docCache[_id] = now;
|
||||
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach(cb => {
|
||||
try { cb(data, id); } catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('RemoteStorage change handler error', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('onRemoteStorageChange error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function rsPut(table, id, data) {
|
||||
if (!rsModule) throw new Error('RemoteStorage not initialized');
|
||||
try {
|
||||
const path = table + '/' + id;
|
||||
if (data === null) {
|
||||
await rsModule.remove(path);
|
||||
// Trigger change manually
|
||||
onRemoteStorageChange({ path: '/telesec/' + path, newValue: null });
|
||||
return;
|
||||
}
|
||||
|
||||
var toStore = data;
|
||||
try {
|
||||
var isEncryptedString = (typeof data === 'string' && data.startsWith(ENCRYPTED_PREFIX) && data.endsWith(ENCRYPTED_SUFFIX));
|
||||
if (!isEncryptedString && typeof TS_encrypt === 'function' && typeof SECRET !== 'undefined' && SECRET) {
|
||||
toStore = await new Promise(resolve => {
|
||||
try { TS_encrypt(data, SECRET, enc => resolve(enc)); } catch (e) { resolve(data); }
|
||||
});
|
||||
}
|
||||
} catch (e) { toStore = data; }
|
||||
|
||||
await rsModule.storeObject('application/json', path, toStore);
|
||||
// Trigger change manually for immediate UI update
|
||||
onRemoteStorageChange({ path: '/telesec/' + path, newValue: toStore });
|
||||
} catch (e) {
|
||||
console.error('RemoteStorage.put error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function rsGet(table, id) {
|
||||
if (!rsModule) return null;
|
||||
try {
|
||||
const path = table + '/' + id;
|
||||
const data = await rsModule.getObject(path);
|
||||
return data || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function rsList(table) {
|
||||
if (!rsModule) return [];
|
||||
try {
|
||||
const listing = await rsModule.getListing(table + '/');
|
||||
if (!listing) return [];
|
||||
|
||||
const results = [];
|
||||
for (const filename of Object.keys(listing)) {
|
||||
// Remove trailing slash if present, otherwise use as-is
|
||||
const id = filename.endsWith('/') ? filename.slice(0, -1) : filename;
|
||||
try {
|
||||
const data = await rsGet(table, id);
|
||||
if (data !== null) {
|
||||
results.push({ id: id, data: data });
|
||||
const _id = table + ':' + id;
|
||||
try { docCache[_id] = typeof data === 'string' ? data : JSON.stringify(data); } catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error loading item ' + id, e);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (e) {
|
||||
console.warn('RemoteStorage.list error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function rsMap(table, cb) {
|
||||
if (!rsModule) return () => {};
|
||||
callbacks[table] = callbacks[table] || [];
|
||||
callbacks[table].push(cb);
|
||||
rsList(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
|
||||
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
|
||||
}
|
||||
|
||||
// RemoteStorage doesn't have built-in attachment support, store as base64 strings
|
||||
async function rsPutAttachment(table, id, name, dataUrlOrBlob, contentType) {
|
||||
if (!rsModule) return false;
|
||||
try {
|
||||
let dataUrl = dataUrlOrBlob;
|
||||
if (dataUrlOrBlob instanceof Blob) {
|
||||
dataUrl = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => resolve(e.target.result);
|
||||
reader.readAsDataURL(dataUrlOrBlob);
|
||||
});
|
||||
}
|
||||
const path = table + '/' + id + '/attachments/' + name;
|
||||
await rsModule.storeObject('application/json', path, { dataUrl: dataUrl, contentType: contentType });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('RemoteStorage putAttachment error', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rsGetAttachment(table, id, name) {
|
||||
if (!rsModule) return null;
|
||||
try {
|
||||
const path = table + '/' + id + '/attachments/' + name;
|
||||
const obj = await rsModule.getObject(path);
|
||||
return obj ? obj.dataUrl : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function rsListAttachments(table, id) {
|
||||
if (!rsModule) return [];
|
||||
try {
|
||||
const path = table + '/' + id + '/attachments/';
|
||||
const listing = await rsModule.getListing(path);
|
||||
if (!listing) return [];
|
||||
|
||||
const results = [];
|
||||
for (const filename of Object.keys(listing)) {
|
||||
// Remove trailing slash if present, otherwise use as-is
|
||||
const name = filename.endsWith('/') ? filename.slice(0, -1) : filename;
|
||||
try {
|
||||
const att = await rsGetAttachment(table, id, name);
|
||||
results.push({ name: name, dataUrl: att, content_type: null });
|
||||
} catch (e) {
|
||||
results.push({ name: name, dataUrl: null, content_type: null });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function rsDeleteAttachment(table, id, name) {
|
||||
if (!rsModule) return false;
|
||||
try {
|
||||
const path = table + '/' + id + '/attachments/' + name;
|
||||
await rsModule.remove(path);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('RemoteStorage deleteAttachment error', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Unified Public API ====================
|
||||
|
||||
async function init(opts) {
|
||||
backend = opts.backend || localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
|
||||
|
||||
// Save backend preference
|
||||
try {
|
||||
localStorage.setItem('TELESEC_BACKEND', backend);
|
||||
} catch (e) {}
|
||||
|
||||
if (backend === 'remotestorage') {
|
||||
return initRemoteStorage(opts);
|
||||
} else {
|
||||
return initPouchDB(opts);
|
||||
}
|
||||
}
|
||||
|
||||
async function put(table, id, data) {
|
||||
if (backend === 'remotestorage') return rsPut(table, id, data);
|
||||
else return pouchPut(table, id, data);
|
||||
}
|
||||
|
||||
async function get(table, id) {
|
||||
if (backend === 'remotestorage') return rsGet(table, id);
|
||||
else return pouchGet(table, id);
|
||||
}
|
||||
|
||||
async function del(table, id) {
|
||||
return put(table, id, null);
|
||||
}
|
||||
|
||||
async function list(table) {
|
||||
if (backend === 'remotestorage') return rsList(table);
|
||||
else return pouchList(table);
|
||||
}
|
||||
|
||||
function map(table, cb) {
|
||||
if (backend === 'remotestorage') return rsMap(table, cb);
|
||||
else return pouchMap(table, cb);
|
||||
}
|
||||
|
||||
function replicateToRemote() {
|
||||
if (backend === 'pouchdb') replicatePouchToRemote();
|
||||
// remoteStorage syncs automatically
|
||||
}
|
||||
|
||||
async function putAttachment(table, id, name, dataUrlOrBlob, contentType) {
|
||||
if (backend === 'remotestorage') return rsPutAttachment(table, id, name, dataUrlOrBlob, contentType);
|
||||
else return pouchPutAttachment(table, id, name, dataUrlOrBlob, contentType);
|
||||
}
|
||||
|
||||
async function getAttachment(table, id, name) {
|
||||
if (backend === 'remotestorage') return rsGetAttachment(table, id, name);
|
||||
else return pouchGetAttachment(table, id, name);
|
||||
}
|
||||
|
||||
async function listAttachments(table, id) {
|
||||
if (backend === 'remotestorage') return rsListAttachments(table, id);
|
||||
else return pouchListAttachments(table, id);
|
||||
}
|
||||
|
||||
async function deleteAttachment(table, id, name) {
|
||||
if (backend === 'remotestorage') return rsDeleteAttachment(table, id, name);
|
||||
else return pouchDeleteAttachment(table, id, name);
|
||||
}
|
||||
|
||||
// Listen for online event (PouchDB only)
|
||||
if (typeof window !== 'undefined' && window.addEventListener) {
|
||||
window.addEventListener('online', function () {
|
||||
try { setTimeout(replicateToRemote, 1000); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
put,
|
||||
get,
|
||||
del,
|
||||
list,
|
||||
map,
|
||||
replicateToRemote,
|
||||
listAttachments,
|
||||
deleteAttachment,
|
||||
putAttachment,
|
||||
getAttachment,
|
||||
getBackend: () => backend,
|
||||
_internal: {
|
||||
pouchLocal: () => pouchLocal,
|
||||
rsClient: () => rsClient
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.DB = DB;
|
||||
|
||||
// Auto-initialize DB on startup using saved settings (non-blocking)
|
||||
(function autoInitDB() {
|
||||
try {
|
||||
const savedBackend = localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
|
||||
const remoteServer = localStorage.getItem('TELESEC_COUCH_URL') || '';
|
||||
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
|
||||
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
|
||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
|
||||
const rsUserAddress = localStorage.getItem('TELESEC_RS_USER') || '';
|
||||
const rsToken = localStorage.getItem('TELESEC_RS_TOKEN') || '';
|
||||
|
||||
try { SECRET = localStorage.getItem('TELESEC_SECRET') || ''; } catch (e) { SECRET = ''; }
|
||||
|
||||
const opts = {
|
||||
backend: savedBackend,
|
||||
secret: SECRET,
|
||||
remoteServer,
|
||||
username,
|
||||
password,
|
||||
dbname,
|
||||
rsUserAddress,
|
||||
rsToken
|
||||
};
|
||||
|
||||
DB.init(opts).catch(e => console.warn('DB.autoInit error', e));
|
||||
} catch (e) { console.warn('DB.autoInit unexpected error', e); }
|
||||
})();
|
||||
@@ -1,61 +1,4 @@
|
||||
window.rtcRoom = "telesec.tech.eus";
|
||||
var gun = Gun(RELAYS, {
|
||||
axe: false,
|
||||
localStorage: true,
|
||||
// radisk: true,
|
||||
});
|
||||
var SEA = Gun.SEA;
|
||||
var user = gun.user();
|
||||
function removeCache() {
|
||||
caches.keys().then(function (names) {
|
||||
for (let name of names) caches.delete(name);
|
||||
console.log("Removing cache " + name);
|
||||
console.log("OK");
|
||||
location.reload(true);
|
||||
});
|
||||
}
|
||||
function getPeers() {
|
||||
var peerCount = 0;
|
||||
var peerCountEl = document.getElementById("peerCount");
|
||||
var peerListEl = document.getElementById("peerList");
|
||||
var list = document.createElement("ul");
|
||||
document.getElementById("peerPID").innerText = "PID " + gun.back("opt.pid");
|
||||
Object.values(gun.back("opt.peers")).forEach((peer) => {
|
||||
if (
|
||||
peer.wire != undefined &&
|
||||
(peer.wire.readyState == 1 || peer.wire.readyState == "open")
|
||||
) {
|
||||
peerCount += 1;
|
||||
var wireType = peer.wire.constructor.name;
|
||||
var wireHType = peer.wire.constructor.name;
|
||||
var wireID = peer.id;
|
||||
switch (wireType) {
|
||||
case "WebSocket":
|
||||
wireHType = "Web";
|
||||
wireID = wireID.split("/")[2];
|
||||
break;
|
||||
case "RTCDataChannel":
|
||||
wireHType = "Mesh";
|
||||
wireID = peer.id;
|
||||
}
|
||||
var el = document.createElement("li");
|
||||
el.innerText = `Nodo ${wireHType}: ${wireID}`;
|
||||
list.append(el);
|
||||
}
|
||||
});
|
||||
peerListEl.innerHTML = list.innerHTML;
|
||||
peerCountEl.innerText = peerCount;
|
||||
if (peerCount < 3) {
|
||||
document.getElementById("connectStatus").src = "static/ico/connect_ko.svg";
|
||||
gun.opt({ peers: RELAYS });
|
||||
} else {
|
||||
document.getElementById("connectStatus").src = "static/ico/connect_ok.svg";
|
||||
}
|
||||
}
|
||||
getPeers();
|
||||
setInterval(() => {
|
||||
getPeers();
|
||||
}, 2500);
|
||||
function safeuuid(prefix = "AXLUID_") {
|
||||
return prefix + crypto.randomUUID().split("-")[4];
|
||||
}
|
||||
// gun_init.js - Deprecated
|
||||
// Gun/GunDB has been replaced by PouchDB/CouchDB in this project.
|
||||
// This file is kept for reference only and should not be used.
|
||||
console.warn('gun_init.js is deprecated; using PouchDB/DB module instead.');
|
||||
231
src/index.html
@@ -1,89 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>TeleSec</title>
|
||||
<link rel="icon" type="image/png" href="static/TeleSec.jpg" />
|
||||
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
|
||||
<link href="static/toastr.min.css" rel="stylesheet" />
|
||||
%%PREFETCH%%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<details class="supermesh-indicator">
|
||||
<summary>
|
||||
<b>SuperMesh</b><br />
|
||||
<br /><small id="peerPID" style="font-family: monospace"
|
||||
>PID ??????????</small
|
||||
>
|
||||
</summary>
|
||||
<ul id="peerList"></ul>
|
||||
<i>Todos los datos están encriptados.</i>
|
||||
</details>
|
||||
<main>
|
||||
<header class="no_print" id="header_hide_query">
|
||||
<details id="LinkAccount_details" open>
|
||||
<summary>
|
||||
<b
|
||||
>TeleSec - <span id="groupId">???</span> - (<span id="peerCount"
|
||||
>?</span
|
||||
>
|
||||
nodos)</b
|
||||
>
|
||||
</summary>
|
||||
<fieldset id="auth_fieldSet">
|
||||
<legend>Credenciales</legend>
|
||||
<br />
|
||||
<label
|
||||
>Codigo de grupo:<br />
|
||||
<input type="text" id="LinkAccount_group"
|
||||
/></label>
|
||||
<br />
|
||||
<br />
|
||||
<label
|
||||
>Clave secreta:<br />
|
||||
<input type="text" id="LinkAccount_secret"
|
||||
/></label>
|
||||
<br /><br />
|
||||
<button
|
||||
type="button"
|
||||
onclick='LinkAccount(document.getElementById("LinkAccount_group").value, document.getElementById("LinkAccount_secret").value, true)'
|
||||
>
|
||||
Iniciar sesión
|
||||
</button>
|
||||
</fieldset>
|
||||
</details>
|
||||
<!-- <button onclick="displayPost('index')">Ir a la pagina de inicio</button> -->
|
||||
<div id="appendApps">
|
||||
<!--<a class="button nav-supercafe nav-disabled" disabled>SuperCafé</a>
|
||||
<a class="button nav-comedor nav-disabled" disabled>Menú Comedor</a>
|
||||
<a class="button nav-recetas nav-disabled" disabled>Recetas</a>-->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>%%TITLE%%</title>
|
||||
<link rel="icon" type="image/png" href="static/logo.jpg" />
|
||||
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
|
||||
<link href="static/toastr.min.css" rel="stylesheet" />
|
||||
%%PREFETCH%%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="ribbon no_print" id="header_hide_query">
|
||||
<img class="ribbon-orb" id="connectStatus" src="static/logo.jpg" />
|
||||
|
||||
<div class="ribbon-content">
|
||||
<div class="ribbon-tabs">
|
||||
<div class="ribbon-tab active" data-tab="modulos">Modulos</div>
|
||||
<div class="ribbon-tab" data-tab="buscar">Buscar</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Modulos -->
|
||||
<details id="tab-modulos" open>
|
||||
<summary hidden>Modulos</summary>
|
||||
<div class="ribbon-panel" id="appendApps2">
|
||||
</div>
|
||||
<hr />
|
||||
</header>
|
||||
<div id="container"></div>
|
||||
<!-- <br><br><br>
|
||||
<footer>
|
||||
<hr>
|
||||
<details>
|
||||
<summary><b>Apps SuperMesh</b></summary>
|
||||
<button type="button">
|
||||
<img src="static/TeleSec.jpg" alt="" width="100px">
|
||||
<br>TeleSec
|
||||
</button>
|
||||
</details>
|
||||
</footer> -->
|
||||
<img
|
||||
id="connectStatus"
|
||||
style="bottom: 15px; right: 15px; position: fixed; width: 50px"
|
||||
/>
|
||||
</main>
|
||||
<img
|
||||
id="actionStatus"
|
||||
src="static/ico/statusok.png"
|
||||
style="
|
||||
|
||||
<!-- Tab: Buscar -->
|
||||
<details id="tab-buscar">
|
||||
<summary hidden>Buscar</summary>
|
||||
<div class="ribbon-panel">
|
||||
<input type="text" id="quickSearchInput" placeholder="Búsqueda rápida..."
|
||||
onkeypress="if(event.key==='Enter') document.getElementById('quickSearchBtn').click()">
|
||||
<button id="quickSearchBtn" onclick="setUrlHash('buscar')" class="btn5">
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<small style="margin-top:10px;">Base de datos: <b id="peerLink">(no configurado)</b></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img id="loading" src="load.gif" style="display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 50px); max-width: 400px;" />
|
||||
<details class="supermesh-indicator">
|
||||
<summary>
|
||||
<b>Sincronización</b><br />
|
||||
<br /><small id="peerPID" style="font-family: monospace">Estado: local</small>
|
||||
</summary>
|
||||
<ul id="peerList"></ul>
|
||||
<i>Todos los datos están encriptados.</i>
|
||||
</details>
|
||||
<main id="container"></main>
|
||||
<img id="actionStatus" src="static/ico/statusok.png" style="
|
||||
z-index: 2048;
|
||||
margin: 0px;
|
||||
position: fixed;
|
||||
@@ -93,62 +65,41 @@
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: none;
|
||||
"
|
||||
/>
|
||||
<div id="snackbar">
|
||||
Hay una nueva versión de TeleSec.<br /><a id="reload"
|
||||
>Pulsa aqui para actualizar.</a
|
||||
>
|
||||
</div>
|
||||
<script src="static/showdown.min.js"></script>
|
||||
<script src="static/jquery.js"></script>
|
||||
<script src="static/gun.js"></script>
|
||||
<script src="static/webrtc.js"></script>
|
||||
<script src="static/sea.js"></script>
|
||||
<script src="static/yson.js"></script>
|
||||
<script src="static/radix.js"></script>
|
||||
<!-- <script src="static/radisk.js"></script> -->
|
||||
<!-- <script src="static/store.js"></script> -->
|
||||
<script src="static/rindexed.js"></script>
|
||||
<script src="static/path.js"></script>
|
||||
<script src="static/open.js"></script>
|
||||
<script src="static/load.js"></script>
|
||||
<!--<script src="static/synchronous.js"></script>-->
|
||||
<!--<script src="static/axe.js"></script>-->
|
||||
<script src="static/toastr.min.js"></script>
|
||||
<script src="static/doublescroll.js"></script>
|
||||
<!--<script src="static/simplemde.min.js"></script>-->
|
||||
<script async>
|
||||
async function getQuota(cb = () => {}) {
|
||||
if (navigator.storage && navigator.storage.estimate) {
|
||||
const quota = await navigator.storage.estimate();
|
||||
// quota.usage -> Number of bytes used.
|
||||
// quota.quota -> Maximum number of bytes available.
|
||||
const percentageUsed = (quota.usage / quota.quota) * 100;
|
||||
console.log(
|
||||
`You've used ${percentageUsed}% of the available storage.`
|
||||
);
|
||||
const remaining = quota.quota - quota.usage;
|
||||
cb(percentageUsed, remaining);
|
||||
console.log(`You can write up to ${remaining} more bytes.`);
|
||||
}
|
||||
}
|
||||
getQuota();
|
||||
</script>
|
||||
<script src="pwa.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="gun_init.js"></script>
|
||||
<script src="app_logic.js"></script>
|
||||
<script src="app_modules.js"></script>
|
||||
<script src="page/login.js"></script>
|
||||
<script src="page/index.js"></script>
|
||||
<script src="page/importar.js"></script>
|
||||
<script src="page/exportar.js"></script>
|
||||
<script src="page/materiales.js"></script>
|
||||
<script src="page/resumen_diario.js"></script>
|
||||
<script src="page/personas.js"></script>
|
||||
<script src="page/supercafe.js"></script>
|
||||
<script src="page/notificaciones.js"></script>
|
||||
<script src="page/comedor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
" />
|
||||
<div id="snackbar">
|
||||
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js" integrity="sha256-/H4YS+7aYb9kJ5OKhFYPUjSJdrtV6AeyJOtTkw6X72o=" crossorigin="anonymous"></script>
|
||||
<script src="static/showdown.min.js"></script>
|
||||
<script src="static/qrcode/html5-qrcode.min.js"></script>
|
||||
<script src="static/qrcode/barcode.js"></script>
|
||||
<script src="static/jquery.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.3.1/dist/pouchdb.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/remotestoragejs@2.0.0/release/remotestorage.min.js"></script>
|
||||
<!--<script src="static/synchronous.js"></script>-->
|
||||
<!--<script src="static/axe.js"></script>-->
|
||||
<script src="static/toastr.min.js"></script>
|
||||
<script src="static/doublescroll.js"></script>
|
||||
<!--<script src="static/simplemde.min.js"></script>-->
|
||||
<script src="pwa.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="db.js"></script>
|
||||
<script src="app_logic.js"></script>
|
||||
<script src="app_modules.js"></script>
|
||||
<script src="page/login.js"></script>
|
||||
<script src="page/index.js"></script>
|
||||
<script src="page/dataman.js"></script>
|
||||
<script src="page/aulas.js"></script>
|
||||
<script src="page/materiales.js"></script>
|
||||
<script src="page/resumen_diario.js"></script>
|
||||
<script src="page/personas.js"></script>
|
||||
<script src="page/supercafe.js"></script>
|
||||
<!-- <script src="page/avisos.js"></script> -->
|
||||
<script src="page/comedor.js"></script>
|
||||
<script src="page/notas.js"></script>
|
||||
<!-- <script src="page/chat.js"></script> -->
|
||||
<script src="page/buscar.js"></script>
|
||||
<script src="page/pagos.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
28
src/manifest.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"theme_color": "#23365e",
|
||||
"background_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"purpose": "maskable",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_maskable.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"purpose": "any",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_rounded.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"dir": "auto",
|
||||
"lang": "es-ES",
|
||||
"start_url": "index.html",
|
||||
"scope": "/",
|
||||
"description": "%%TITLE%% - Comunicación Hipersegura",
|
||||
"id": "telesec.tech.eus",
|
||||
"name": "%%TITLE%%",
|
||||
"short_name": "%%TITLE%%"
|
||||
}
|
||||
416
src/page/aulas.js
Normal file
@@ -0,0 +1,416 @@
|
||||
PERMS["aulas"] = "Aulas (Solo docentes!)";
|
||||
PAGES.aulas = {
|
||||
//navcss: "btn1",
|
||||
Title: "Gest-Aula",
|
||||
icon: "static/appico/components.png",
|
||||
AccessControl: true,
|
||||
index: function () {
|
||||
if (!checkRole("aulas")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
var data_Comedor = safeuuid();
|
||||
var data_Tareas = safeuuid();
|
||||
var data_Diario = safeuuid();
|
||||
var data_Weather = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Gestión del Aula</h1>
|
||||
<div>
|
||||
<fieldset style="float: left;">
|
||||
<legend><img src="${PAGES.notas.icon}" height="20"> Notas esenciales</legend>
|
||||
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia">Como iniciar el día</a>
|
||||
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe">Como realizar el café</a>
|
||||
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
|
||||
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
|
||||
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
|
||||
</fieldset>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Acciones</legend>
|
||||
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"><img src="${PAGES.materiales.icon}" height="20"> Solicitudes de material</a>
|
||||
<a class="button" style="font-size: 25px;" href="#aulas,informes,diario-${CurrentISODate()}">Diario de hoy</a>
|
||||
<a class="button rojo" style="font-size: 25px;" href="#notas,alertas"><img src="${PAGES.notas.icon}" height="20"> Ver Alertas</a>
|
||||
<a class="button" style="font-size: 25px;" href="#aulas,informes"><img src="${PAGES.aulas.icon}" height="20"> Informes y diarios</a>
|
||||
<a class="button btn4" style="font-size: 25px;" href="#supercafe"><img src="${PAGES.supercafe.icon}" height="20"> Ver comandas</a>
|
||||
</fieldset>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Datos de hoy</legend>
|
||||
|
||||
<span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
|
||||
<span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
|
||||
<span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
|
||||
<span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; width: 245px;" id="${data_Weather}"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
//#region Cargar Clima
|
||||
// Get location from DB settings.weather_location; if missing ask user and save it
|
||||
// url format: https://wttr.in/<loc>?F0m
|
||||
DB.get('settings','weather_location').then((loc) => {
|
||||
if (!loc) {
|
||||
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
|
||||
if (loc) {
|
||||
DB.put('settings','weather_location', loc);
|
||||
}
|
||||
}
|
||||
if (loc) {
|
||||
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
|
||||
} else {
|
||||
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Clima
|
||||
//#region Cargar Comedor
|
||||
DB.get('comedor', CurrentISODate()).then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Platos = data.Platos || "No hay platos registrados para hoy.";
|
||||
// Display platos
|
||||
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'comedor', CurrentISODate());
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Comedor
|
||||
//#region Cargar Tareas
|
||||
DB.get('notas', 'tareas').then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Contenido = data.Contenido || "No hay tareas.";
|
||||
// Display platos
|
||||
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'notas', 'tareas');
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Tareas
|
||||
//#region Cargar Diario
|
||||
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Contenido = data.Contenido || "No hay un diario.";
|
||||
// Display platos
|
||||
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'aulas_informes', 'diario-' + CurrentISODate());
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Diario
|
||||
},
|
||||
_solicitudes: function () {
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
||||
<h1>Solicitudes de Material</h1>
|
||||
<button id="${btn_new}">Nueva solicitud</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"aulas,solicitudes",
|
||||
[
|
||||
{
|
||||
key: "Solicitante",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Solicitante",
|
||||
},
|
||||
{
|
||||
key: "Asunto",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Asunto",
|
||||
},
|
||||
],
|
||||
"aulas_solicitudes",
|
||||
document.querySelector("#cont")
|
||||
);
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("aulas,solicitudes," + safeuuid(""));
|
||||
};
|
||||
},
|
||||
_solicitudes__edit: function (mid) {
|
||||
var nameh1 = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_contenido = safeuuid();
|
||||
var field_autor = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
container.innerHTML = `
|
||||
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
|
||||
<h1>Solicitud <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||
<legend>Valores</legend>
|
||||
<div style="max-width: 400px;">
|
||||
<label>
|
||||
Asunto<br>
|
||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
||||
</label>
|
||||
<input type="hidden" id="${field_autor}" readonly value="">
|
||||
</div>
|
||||
<label>
|
||||
Contenido - ¡Incluye el material a solicitar!<br>
|
||||
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
|
||||
</label>
|
||||
<hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
(async () => {
|
||||
const data = await DB.get('aulas_solicitudes', mid);
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
||||
document.getElementById(field_contenido).value = data["Contenido"] || "";
|
||||
document.getElementById(field_autor).value = data["Solicitante"] || SUB_LOGGED_IN_ID || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'aulas_solicitudes', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
})();
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Solicitante: document.getElementById(field_autor).value,
|
||||
Contenido: document.getElementById(field_contenido).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
};
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('aulas_solicitudes', mid, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("aulas,solicitudes");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar la solicitud");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta solicitud?") == true) {
|
||||
DB.del('aulas_solicitudes', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("aulas,solicitudes");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
_informes: function () {
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
var field_new_byday = safeuuid();
|
||||
var btn_new_byday = safeuuid();
|
||||
container.innerHTML = `
|
||||
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
||||
<h1>Informes</h1>
|
||||
<div style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;">
|
||||
<b>Diario:</b><br>
|
||||
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}">
|
||||
<button id="${btn_new_byday}">Abrir / Nuevo</button>
|
||||
</div><br>
|
||||
<button id="${btn_new}">Nuevo informe</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"aulas,informes",
|
||||
[
|
||||
{
|
||||
key: "Autor",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Autor",
|
||||
},
|
||||
{
|
||||
key: "Fecha",
|
||||
type: "fecha",
|
||||
default: "",
|
||||
label: "Fecha",
|
||||
},
|
||||
{
|
||||
key: "Asunto",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Asunto",
|
||||
},
|
||||
],
|
||||
"aulas_informes",
|
||||
document.querySelector("#cont")
|
||||
);
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("aulas,informes," + safeuuid(""));
|
||||
};
|
||||
document.getElementById(btn_new_byday).onclick = () => {
|
||||
const day = document.getElementById(field_new_byday).value;
|
||||
if (day) {
|
||||
setUrlHash("aulas,informes,diario-" + day);
|
||||
} else {
|
||||
toastr.error("Selecciona un día válido");
|
||||
}
|
||||
}
|
||||
},
|
||||
_informes__edit: function (mid) {
|
||||
var nameh1 = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_contenido = safeuuid();
|
||||
var field_autor = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var title = "";
|
||||
if (mid.startsWith("diario-")) {
|
||||
var date = mid.replace("diario-", "").split("-");
|
||||
title = "Diario " + date[2] + "/" + date[1] + "/" + date[0];
|
||||
}
|
||||
container.innerHTML = `
|
||||
<a class="button" href="#aulas,informes">← Volver a informes</a>
|
||||
<h1>Informe <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||
<legend>Valores</legend>
|
||||
<div style="max-width: 400px;">
|
||||
<label>
|
||||
Asunto<br>
|
||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
||||
</label>
|
||||
<input type="hidden" id="${field_autor}" readonly value="">
|
||||
<input type="hidden" id="${field_fecha}" value="">
|
||||
</div>
|
||||
<label>
|
||||
Contenido<br>
|
||||
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
|
||||
</label>
|
||||
<hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
(async () => {
|
||||
const data = await DB.get('aulas_informes', mid);
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_asunto).value = data["Asunto"] || title || "";
|
||||
document.getElementById(field_contenido).value = data["Contenido"] || "";
|
||||
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
|
||||
document.getElementById(field_fecha).value = data["Fecha"] || mid.startsWith("diario-") ? mid.replace("diario-", "") : CurrentISODate();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
})();
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Autor: document.getElementById(field_autor).value,
|
||||
Contenido: document.getElementById(field_contenido).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
Fecha: document.getElementById(field_fecha).value || CurrentISODate(),
|
||||
};
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('aulas_informes', mid, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("aulas,informes");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar el informe");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar este informe?") == true) {
|
||||
DB.del('aulas_informes', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("aulas,informes");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
edit: function (section) {
|
||||
if (!checkRole("aulas")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
var item = location.hash.replace("#", "").split(",")[2];
|
||||
if (!item) {
|
||||
// No item, show section
|
||||
switch (section) {
|
||||
case "solicitudes":
|
||||
this._solicitudes();
|
||||
break;
|
||||
case "informes":
|
||||
this._informes();
|
||||
break;
|
||||
default:
|
||||
this.index();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Show section__edit
|
||||
switch (section) {
|
||||
case "solicitudes":
|
||||
this._solicitudes__edit(item);
|
||||
break;
|
||||
case "informes":
|
||||
this._informes__edit(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
234
src/page/avisos.js
Normal file
@@ -0,0 +1,234 @@
|
||||
PERMS["avisos"] = "Avisos"
|
||||
PERMS["avisos:edit"] = "> Editar"
|
||||
PAGES.avisos = {
|
||||
navcss: "btn5",
|
||||
icon: "static/appico/File_Plugin.svg",
|
||||
AccessControl: true,
|
||||
Title: "Avisos",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("avisos:edit")) {setUrlHash("avisos");return}
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_origen = safeuuid();
|
||||
var field_destino = safeuuid();
|
||||
var field_estado = safeuuid();
|
||||
var field_mensaje = safeuuid();
|
||||
var field_respuesta = safeuuid();
|
||||
var btn_leer = safeuuid();
|
||||
var btn_desleer = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Aviso <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Valores</legend>
|
||||
<label>
|
||||
Fecha<br>
|
||||
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Asunto<br>
|
||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
||||
</label>
|
||||
<input type="hidden" id="${field_origen}">
|
||||
<input type="hidden" id="${field_destino}">
|
||||
<div id="${div_actions}"></div>
|
||||
<label>
|
||||
Mensaje<br>
|
||||
<textarea id="${field_mensaje}"></textarea><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Respuesta<br>
|
||||
<textarea id="${field_respuesta}"></textarea><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Estado<br>
|
||||
<input readonly disabled type="text" id="${field_estado}" value="">
|
||||
<br>
|
||||
<button id="${btn_leer}">Leido</button>
|
||||
<button id="${btn_desleer}">No leido</button>
|
||||
<br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(btn_leer).onclick = () => {
|
||||
document.getElementById(field_estado).value = "leido";
|
||||
};
|
||||
document.getElementById(btn_desleer).onclick = () => {
|
||||
document.getElementById(field_estado).value = "por_leer";
|
||||
};
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
"",
|
||||
(value) => {
|
||||
document.getElementById(field_origen).value = value;
|
||||
},
|
||||
"Origen"
|
||||
);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
"",
|
||||
(value) => {
|
||||
document.getElementById(field_destino).value = value;
|
||||
},
|
||||
"Destino"
|
||||
);
|
||||
(async () => {
|
||||
const data = await DB.get('notificaciones', mid);
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate() || "";
|
||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
||||
document.getElementById(field_mensaje).value = data["Mensaje"] || "";
|
||||
document.getElementById(field_origen).value = data["Origen"] || SUB_LOGGED_IN_ID || "";
|
||||
document.getElementById(field_destino).value = data["Destino"] || "";
|
||||
document.getElementById(field_estado).value = data["Estado"] || "%%" || "";
|
||||
document.getElementById(field_respuesta).value = data["Respuesta"] || "";
|
||||
|
||||
// Persona select
|
||||
divact.innerHTML = "";
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data["Origen"] || "",
|
||||
(value) => {
|
||||
document.getElementById(field_origen).value = value;
|
||||
},
|
||||
"Origen"
|
||||
);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data["Destino"] || "",
|
||||
(value) => {
|
||||
document.getElementById(field_destino).value = value;
|
||||
},
|
||||
"Destino"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'notificaciones', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
})();
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Check if button is already disabled to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
// Validate before disabling button
|
||||
if (document.getElementById(field_origen).value == "") {
|
||||
alert("¡Hay que elegir una persona de origen!");
|
||||
return;
|
||||
}
|
||||
if (document.getElementById(field_destino).value == "") {
|
||||
alert("¡Hay que elegir una persona de origen!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button after validation passes
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Fecha: document.getElementById(field_fecha).value,
|
||||
Origen: document.getElementById(field_origen).value,
|
||||
Destino: document.getElementById(field_destino).value,
|
||||
Mensaje: document.getElementById(field_mensaje).value,
|
||||
Respuesta: document.getElementById(field_respuesta).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
Estado: document
|
||||
.getElementById(field_estado)
|
||||
.value.replace("%%", "por_leer"),
|
||||
};
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('notificaciones', mid, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("avisos");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar la notificación");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta notificación?") == true) {
|
||||
DB.del('notificaciones', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("avisos");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole("avisos")) {setUrlHash("index");return}
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Avisos</h1>
|
||||
<button id="${btn_new}">Nuevo aviso</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"avisos",
|
||||
[
|
||||
{
|
||||
key: "Origen",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Origen",
|
||||
},
|
||||
{
|
||||
key: "Destino",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Destino",
|
||||
},
|
||||
{
|
||||
key: "Asunto",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Asunto",
|
||||
},
|
||||
{
|
||||
key: "Estado",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Estado",
|
||||
},
|
||||
],
|
||||
"notificaciones",
|
||||
document.querySelector("#cont"),
|
||||
(data, new_tr) => {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
if (data.Estado == "leido") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!checkRole("avisos:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none"
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("avisos," + safeuuid(""));
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
266
src/page/buscar.js
Normal file
@@ -0,0 +1,266 @@
|
||||
PAGES.buscar = {
|
||||
navcss: "btn1",
|
||||
icon: "static/appico/view.svg",
|
||||
Title: "Buscar",
|
||||
AccessControl: true,
|
||||
Esconder: true,
|
||||
|
||||
index: function () {
|
||||
const searchInput = safeuuid();
|
||||
const resultsContainer = safeuuid();
|
||||
const searchButton = safeuuid();
|
||||
const recentSearches = safeuuid();
|
||||
const moduleFilter = safeuuid();
|
||||
|
||||
container.innerHTML = `
|
||||
<h1>🔍 Búsqueda Global</h1>
|
||||
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Opciones de búsqueda</legend>
|
||||
<input type="text" id="${searchInput}"
|
||||
placeholder="Escribe aquí para buscar..."
|
||||
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()">
|
||||
<select id="${moduleFilter}">
|
||||
<option value="">Todos los módulos</option>
|
||||
<!-- Options will be populated dynamically based on user permissions -->
|
||||
</select>
|
||||
<button id="${searchButton}" class="btn5">
|
||||
Buscar
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<div id="${recentSearches}"></div>
|
||||
|
||||
<div id="${resultsContainer}">
|
||||
<fieldset>
|
||||
<legend>Resultados</legend>
|
||||
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
||||
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
|
||||
|
||||
<details>
|
||||
<summary>💡 Consejos de búsqueda</summary>
|
||||
<ul>
|
||||
<li><strong>Busca por nombres:</strong> "María", "García"</li>
|
||||
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
|
||||
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
|
||||
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
|
||||
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
|
||||
</ul>
|
||||
</details>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize global search
|
||||
const globalSearch = GlobalSearch();
|
||||
globalSearch.loadAllData();
|
||||
|
||||
// Get accessible modules for the current user
|
||||
const accessibleModules = globalSearch.getAccessibleModules();
|
||||
|
||||
const searchInputEl = document.getElementById(searchInput);
|
||||
const resultsEl = document.getElementById(resultsContainer);
|
||||
const searchButtonEl = document.getElementById(searchButton);
|
||||
const recentSearchesEl = document.getElementById(recentSearches);
|
||||
const moduleFilterEl = document.getElementById(moduleFilter);
|
||||
|
||||
// Populate module filter dropdown with only accessible modules
|
||||
function populateModuleFilter() {
|
||||
// Clear existing options except "Todos los módulos"
|
||||
moduleFilterEl.innerHTML = '<option value="">Todos los módulos</option>';
|
||||
|
||||
// Add only accessible modules
|
||||
accessibleModules.forEach((module) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = module.key;
|
||||
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
|
||||
moduleFilterEl.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get module icons (fallback for older module mappings)
|
||||
function getModuleIcon(moduleKey) {
|
||||
const iconMap = {
|
||||
personas: "👤",
|
||||
materiales: "📦",
|
||||
supercafe: "☕",
|
||||
comedor: "🍽️",
|
||||
avisos: "🔔",
|
||||
aulas: "🏫",
|
||||
resumen_diario: "📊",
|
||||
};
|
||||
return iconMap[moduleKey] || "📋";
|
||||
}
|
||||
|
||||
// Load recent searches from localStorage
|
||||
function loadRecentSearches() {
|
||||
const recent = JSON.parse(
|
||||
localStorage.getItem("telesec_recent_searches") || "[]"
|
||||
);
|
||||
if (recent.length > 0) {
|
||||
recentSearchesEl.innerHTML = `
|
||||
<fieldset>
|
||||
<legend>Búsquedas recientes</legend>
|
||||
${recent
|
||||
.map(
|
||||
(term) => `
|
||||
<button onclick="document.getElementById('${searchInput}').value='${term}'; document.getElementById('${searchButton}').click();" class="btn4">
|
||||
${term}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
<button onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';" class="rojo">
|
||||
Limpiar
|
||||
</button>
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the module filter dropdown
|
||||
populateModuleFilter();
|
||||
|
||||
// Save search term to recent searches
|
||||
function saveToRecent(term) {
|
||||
if (!term || term.length < 2) return;
|
||||
|
||||
let recent = JSON.parse(
|
||||
localStorage.getItem("telesec_recent_searches") || "[]"
|
||||
);
|
||||
recent = recent.filter((t) => t !== term); // Remove if exists
|
||||
recent.unshift(term); // Add to beginning
|
||||
recent = recent.slice(0, 5); // Keep only 5 most recent
|
||||
|
||||
localStorage.setItem("telesec_recent_searches", JSON.stringify(recent));
|
||||
loadRecentSearches();
|
||||
}
|
||||
|
||||
// Perform search
|
||||
function performSearch() {
|
||||
const searchTerm = searchInputEl.value.trim();
|
||||
const selectedModule = moduleFilterEl.value;
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
resultsEl.innerHTML = `
|
||||
<fieldset>
|
||||
<legend>Error</legend>
|
||||
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
|
||||
</fieldset>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading
|
||||
resultsEl.innerHTML = `
|
||||
<fieldset>
|
||||
<legend>Buscando...</legend>
|
||||
<div>⏳ Procesando búsqueda...</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
// Add small delay to show loading state
|
||||
setTimeout(() => {
|
||||
let results = globalSearch.performSearch(searchTerm);
|
||||
|
||||
// Filter by module if selected
|
||||
if (selectedModule) {
|
||||
results = results.filter(
|
||||
(result) => result._module === selectedModule
|
||||
);
|
||||
}
|
||||
|
||||
globalSearch.renderResults(results, resultsEl);
|
||||
saveToRecent(searchTerm);
|
||||
|
||||
// Add stats
|
||||
if (results.length > 0) {
|
||||
const statsDiv = document.createElement("fieldset");
|
||||
const legend = document.createElement("legend");
|
||||
legend.textContent = "Estadísticas";
|
||||
statsDiv.appendChild(legend);
|
||||
|
||||
let filterText = selectedModule
|
||||
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
|
||||
: "";
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = `📊 Se encontraron <strong>${results.length}</strong> resultados para "<strong>${searchTerm}</strong>"${filterText}`;
|
||||
statsDiv.appendChild(content);
|
||||
|
||||
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
searchButtonEl.onclick = performSearch;
|
||||
|
||||
// Filter change listener
|
||||
moduleFilterEl.onchange = () => {
|
||||
if (searchInputEl.value.trim().length >= 2) {
|
||||
performSearch();
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-search as user types (with debounce)
|
||||
let searchTimeout;
|
||||
searchInputEl.oninput = () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (searchInputEl.value.trim().length >= 2) {
|
||||
performSearch();
|
||||
}
|
||||
}, 1501);
|
||||
};
|
||||
|
||||
// Focus on search input
|
||||
searchInputEl.focus();
|
||||
|
||||
// Add keyboard shortcuts
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Ctrl+F or Cmd+F to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
searchInputEl.focus();
|
||||
searchInputEl.select();
|
||||
}
|
||||
|
||||
// Escape to clear search
|
||||
if (e.key === "Escape") {
|
||||
searchInputEl.value = "";
|
||||
searchInputEl.focus();
|
||||
resultsEl.innerHTML = `
|
||||
<fieldset>
|
||||
<legend>Resultados</legend>
|
||||
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
||||
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
|
||||
|
||||
<details>
|
||||
<summary>💡 Consejos de búsqueda</summary>
|
||||
<ul>
|
||||
<li><strong>Busca por nombres:</strong> "María", "García"</li>
|
||||
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
|
||||
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
|
||||
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
|
||||
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
|
||||
</ul>
|
||||
</details>
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Check for quick search term from header
|
||||
const quickSearchTerm = sessionStorage.getItem("telesec_quick_search");
|
||||
if (quickSearchTerm) {
|
||||
searchInputEl.value = quickSearchTerm;
|
||||
sessionStorage.removeItem("telesec_quick_search");
|
||||
// Perform search automatically
|
||||
setTimeout(performSearch, 100);
|
||||
}
|
||||
|
||||
// Load recent searches
|
||||
loadRecentSearches();
|
||||
},
|
||||
};
|
||||
@@ -1,48 +1,56 @@
|
||||
PERMS["comedor"] = "Comedor"
|
||||
PERMS["comedor:edit"] = "> Editar"
|
||||
PAGES.comedor = {
|
||||
navcss: "btn7",
|
||||
Title: "Menú comedor",
|
||||
navcss: "btn6",
|
||||
icon: "static/appico/apple.png",
|
||||
AccessControl: true,
|
||||
Title: "Comedor",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("comedor:edit")) {setUrlHash("comedor");return}
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_platos = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Valores</legend>
|
||||
<label>
|
||||
Fecha<br>
|
||||
<input type="date" id="${field_fecha}" value="${CurrentISODate()}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Platos<br>
|
||||
<textarea id="${field_platos}"></textarea><br><br>
|
||||
</label>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("comedor")
|
||||
.get(mid)
|
||||
.once((data, key) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = key;
|
||||
document.getElementById(field_fecha).value = data["Fecha"];
|
||||
document.getElementById(field_platos).value =
|
||||
data["Platos"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data);
|
||||
}
|
||||
});
|
||||
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Valores</legend>
|
||||
<label>
|
||||
Fecha<br>
|
||||
<input type="date" id="${field_fecha}" value=""><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Platos<br>
|
||||
<textarea id="${field_platos}"></textarea><br><br>
|
||||
</label>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
DB.get('comedor', mid).then((data) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_fecha).value = data["Fecha"] || mid || CurrentISODate();
|
||||
document.getElementById(field_platos).value =
|
||||
data["Platos"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'comedor', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
const newDate = document.getElementById(field_fecha).value;
|
||||
var data = {
|
||||
Fecha: newDate,
|
||||
@@ -51,37 +59,44 @@ PAGES.comedor = {
|
||||
|
||||
// If the date has changed, we need to delete the old entry
|
||||
if (mid !== newDate && mid !== "") {
|
||||
betterGunPut(gun.get(TABLE).get("comedor").get(mid), null);
|
||||
DB.del('comedor', mid);
|
||||
}
|
||||
|
||||
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
betterGunPut(gun.get(TABLE).get("comedor").get(newDate), encrypted);
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('comedor', newDate, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("comedor");
|
||||
}, 1500);
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar el menú");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta entrada?") == true) {
|
||||
betterGunPut(gun.get(TABLE).get("comedor").get(mid), null);
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("comedor");
|
||||
}, 1500);
|
||||
DB.del('comedor', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("comedor");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
const tablebody = safeuuid();
|
||||
if (!checkRole("comedor")) {setUrlHash("index");return}
|
||||
const cont = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Menú del comedor</h1>
|
||||
<button id="${btn_new}">Nueva entrada</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
<h1>Menú del comedor</h1>
|
||||
<button id="${btn_new}">Nueva entrada</button>
|
||||
<div id="${cont}"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"comedor",
|
||||
[
|
||||
@@ -98,8 +113,8 @@ PAGES.comedor = {
|
||||
label: "Platos",
|
||||
}
|
||||
],
|
||||
gun.get(TABLE).get("comedor"),
|
||||
document.querySelector("#cont"),
|
||||
"comedor",
|
||||
document.getElementById(cont),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
if (data.Fecha == CurrentISODate()) {
|
||||
@@ -107,8 +122,13 @@ PAGES.comedor = {
|
||||
}
|
||||
}
|
||||
);
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("comedor," + safeuuid(""));
|
||||
};
|
||||
|
||||
if (!checkRole("comedor:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none"
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("comedor," + safeuuid(""));
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
232
src/page/dataman.js
Normal file
@@ -0,0 +1,232 @@
|
||||
PAGES.dataman = {
|
||||
navcss: "btn1",
|
||||
icon: "static/appico/gear_edit.png",
|
||||
AccessControl: true,
|
||||
Title: "Ajustes",
|
||||
edit: function (mid) {
|
||||
switch (mid) {
|
||||
case "export":
|
||||
PAGES.dataman.__export();
|
||||
break;
|
||||
case "import":
|
||||
PAGES.dataman.__import();
|
||||
break;
|
||||
case "config":
|
||||
PAGES.dataman.__config();
|
||||
break;
|
||||
case "labels":
|
||||
PAGES.dataman.__labels();
|
||||
break;
|
||||
default:
|
||||
// Tab to edit
|
||||
}
|
||||
},
|
||||
__config: function () {
|
||||
var form = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Ajustes</h1>
|
||||
<h2>No disponible</h2>
|
||||
<form id="${form}">
|
||||
<label>
|
||||
<input type="checkbox" name="block_add_account" value="yes">
|
||||
<b>Bloquear crear cuenta de administrador?</b>
|
||||
</label>
|
||||
<button type="submit">Aplicar ajustes</button>
|
||||
</form>
|
||||
`;
|
||||
document.getElementById(form).onsubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
var ford = new FormData(document.getElementById(form));
|
||||
if (ford.get("block_add_account") == "yes") {
|
||||
config["block_add_account"] = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
__export: function () {
|
||||
var select_type = safeuuid();
|
||||
var textarea_content = safeuuid();
|
||||
var button_export_local = safeuuid();
|
||||
var button_export_safe = safeuuid();
|
||||
var button_export_safe_cloud = safeuuid();
|
||||
var button_clear = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Exportar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Exportar datos</legend>
|
||||
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
|
||||
<br>
|
||||
<br>
|
||||
<button id="${button_export_local}" type="button">Exportar sin cifrar</button>
|
||||
<button id="${button_export_safe}" type="button">Exportar con cifrado</button>
|
||||
<button id="${button_export_safe_cloud}" style="display: none;" type="button">Exportar a EuskadiTech - cifrado</button>
|
||||
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${getDBName()}".</em>-->
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_export_local).onclick = () => {
|
||||
var data_export = {};
|
||||
var output = {
|
||||
materiales: {},
|
||||
personas: {},
|
||||
};
|
||||
(async () => {
|
||||
const materiales = await DB.list('materiales');
|
||||
materiales.forEach(entry => {
|
||||
const key = entry.id;
|
||||
const value = entry.data;
|
||||
if (value != null) {
|
||||
if (typeof value == 'string') {
|
||||
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
|
||||
output.materiales[key] = data;
|
||||
}, 'materiales', key);
|
||||
} else {
|
||||
output.materiales[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
const personas = await DB.list('personas');
|
||||
personas.forEach(entry => {
|
||||
const key = entry.id;
|
||||
const value = entry.data;
|
||||
if (value != null) {
|
||||
if (typeof value == 'string') {
|
||||
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
|
||||
output.personas[key] = data;
|
||||
}, 'personas', key);
|
||||
} else {
|
||||
output.personas[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
toastr.success("Exportado todo, descargando!");
|
||||
download(
|
||||
`Export %%TITLE%% ${getDBName()}.json.txt`,
|
||||
JSON.stringify(output)
|
||||
);
|
||||
})();
|
||||
};
|
||||
document.getElementById(button_export_safe).onclick = () => {
|
||||
(async () => {
|
||||
const result = { materiales: {}, personas: {} };
|
||||
const materiales = await DB.list('materiales');
|
||||
materiales.forEach(entry => { result.materiales[entry.id] = entry.data; });
|
||||
const personas = await DB.list('personas');
|
||||
personas.forEach(entry => { result.personas[entry.id] = entry.data; });
|
||||
toastr.success("Exportado todo, descargado!");
|
||||
download(
|
||||
`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`,
|
||||
JSON.stringify(result)
|
||||
);
|
||||
})();
|
||||
};
|
||||
// document.getElementById(button_export_safe_cloud).onclick = () => {
|
||||
// var download_data = (DATA) => {
|
||||
// toastr.info("Exportado todo, subiendo!");
|
||||
// fetch(
|
||||
// "https://telesec-sync.tech.eus/upload_backup.php?table=" + getDBName(),
|
||||
// {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify(DATA),
|
||||
// }
|
||||
// )
|
||||
// .then(() => {
|
||||
// toastr.success("Subido correctamente!");
|
||||
// })
|
||||
// .catch(() => {
|
||||
// toastr.error("Ha ocurrido un error en la subida.");
|
||||
// });
|
||||
// };
|
||||
// gun.get(TABLE).load(download_data);
|
||||
// };
|
||||
},
|
||||
__import: function () {
|
||||
var select_type = safeuuid();
|
||||
var textarea_content = safeuuid();
|
||||
var button_import = safeuuid();
|
||||
var button_clear = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Importar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Importar datos</legend>
|
||||
<em>Espera hasta que se vacien todas las notificaciones.</em>
|
||||
<select id="${select_type}">
|
||||
<option value="" disabled selected>Tipo de archivo...</option>
|
||||
<option value="comedor">Galileo - db.comedor.axd</option>
|
||||
<option value="recetas">Galileo - db.recetas.axd</option>
|
||||
<option value="materiales">Galileo - db.materiales.axd</option>
|
||||
<option value="personas">Galileo - db.personas.axd</option>
|
||||
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
|
||||
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
|
||||
</select>
|
||||
<textarea id="${textarea_content}" style="height: 100px;" placeholder="Introduce el contenido del archivo"></textarea>
|
||||
<button id="${button_import}" type="button">Importar</button>
|
||||
<button id="${button_clear}" type="button">Vaciar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_import).onclick = () => {
|
||||
toastr.info("Importando datos...");
|
||||
var val = document.getElementById(textarea_content).value;
|
||||
var sel = document.getElementById(select_type).value;
|
||||
if (sel == "%telesec") {
|
||||
// legacy import, store entire payload as-is
|
||||
// for each top-level key, store their items in DB
|
||||
var parsed = JSON.parse(val);
|
||||
Object.entries(parsed).forEach((section) => {
|
||||
const sectionName = section[0];
|
||||
const sectionData = section[1];
|
||||
Object.entries(sectionData).forEach((entry) => {
|
||||
DB.put(sectionName, entry[0], entry[1]).catch((e) => { console.warn('DB.put error', e); });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.entries(JSON.parse(val)["data"]).forEach((entry) => {
|
||||
DB.put(sel, entry[0], entry[1]).catch((e) => { console.warn('DB.put error', e); });
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
toastr.info("Importado todo!");
|
||||
|
||||
if (sel == "%telesec") {
|
||||
setUrlHash("inicio");
|
||||
} else {
|
||||
setUrlHash(sel);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
},
|
||||
__labels: function (mid) {
|
||||
var div_materiales = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Imprimir Etiquetas QR</h1>
|
||||
<button onclick="print()">Imprimir</button>
|
||||
<h2>Materiales</h2>
|
||||
<div id="${div_materiales}"></div>
|
||||
<br><br>`;
|
||||
div_materiales = document.getElementById(div_materiales);
|
||||
DB.map('materiales', (data, key) => {
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
div_materiales.innerHTML += BuildQR(
|
||||
"materiales," + key,
|
||||
data["Nombre"] || key
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
} else {
|
||||
add_row(data, key);
|
||||
}
|
||||
});
|
||||
},
|
||||
index: function () {
|
||||
container.innerHTML = `
|
||||
<h1>Administración de datos</h1>
|
||||
<a class="button" href="#dataman,import">Importar datos</a>
|
||||
<a class="button" href="#dataman,export">Exportar datos</a>
|
||||
<a class="button" href="#dataman,labels">Imprimir etiquetas</a>
|
||||
<a class="button" href="#dataman,config">Ajustes</a>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
PAGES.exportar = {
|
||||
navcss: "btn1",
|
||||
Title: "Exportar",
|
||||
index: function () {
|
||||
var select_type = safeuuid();
|
||||
var textarea_content = safeuuid();
|
||||
var button_export_local = safeuuid();
|
||||
var button_export_safe = safeuuid();
|
||||
var button_export_safe_cloud = safeuuid();
|
||||
var button_clear = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Exportar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Exportar datos</legend>
|
||||
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
|
||||
<br>
|
||||
<br>
|
||||
<button id="${button_export_local}" type="button">Exportar sin cifrar</button>
|
||||
<button id="${button_export_safe}" type="button">Exportar con cifrado</button>
|
||||
<button id="${button_export_safe_cloud}" style="display: none;" type="button">Exportar a EuskadiTech - cifrado</button>
|
||||
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${GROUPID}".</em>-->
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_export_local).onclick = () => {
|
||||
var data_export = {};
|
||||
var output = {
|
||||
materiales: {},
|
||||
personas: {},
|
||||
};
|
||||
var download_data = (DATA) => {
|
||||
Object.keys(DATA).forEach((modul) => {
|
||||
Object.entries(DATA[modul] || {}).forEach((entry) => {
|
||||
var key = entry[0];
|
||||
var value = entry[1];
|
||||
if (value != null) {
|
||||
if (typeof value == "string") {
|
||||
SEA.decrypt(value, SECRET, (data) => {
|
||||
output[modul][key] = data;
|
||||
});
|
||||
} else {
|
||||
output[modul][key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
toastr.success("Exportado todo, descargando!");
|
||||
console.error(output);
|
||||
download(
|
||||
`Export TeleSec ${GROUPID}.json.txt`,
|
||||
JSON.stringify(output)
|
||||
);
|
||||
//setUrlHash(sel);
|
||||
}, 2500);
|
||||
};
|
||||
gun.get(TABLE).load(download_data);
|
||||
};
|
||||
document.getElementById(button_export_safe).onclick = () => {
|
||||
var download_data = (DATA) => {
|
||||
toastr.success("Exportado todo, descargado!");
|
||||
console.error(DATA);
|
||||
download(
|
||||
`Export TeleSec Encriptado ${GROUPID}.json.txt`,
|
||||
JSON.stringify(DATA)
|
||||
);
|
||||
//setUrlHash(sel);
|
||||
};
|
||||
gun.get(TABLE).load(download_data);
|
||||
};
|
||||
document.getElementById(button_export_safe_cloud).onclick = () => {
|
||||
var download_data = (DATA) => {
|
||||
toastr.info("Exportado todo, subiendo!");
|
||||
console.error(DATA);
|
||||
fetch(
|
||||
"https://telesec-sync.tech.eus/upload_backup.php?table=" + GROUPID,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(DATA),
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
toastr.success("Subido correctamente!");
|
||||
})
|
||||
.catch(() => {
|
||||
toastr.error("Ha ocurrido un error en la subida.");
|
||||
});
|
||||
};
|
||||
gun.get(TABLE).load(download_data);
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
PAGES.importar = {
|
||||
navcss: "btn1",
|
||||
Title: "Importar",
|
||||
index: function () {
|
||||
var select_type = safeuuid();
|
||||
var textarea_content = safeuuid();
|
||||
var button_import = safeuuid();
|
||||
var button_clear = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Importar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Importar datos</legend>
|
||||
<em>Espera hasta que se vacien todas las notificaciones.</em>
|
||||
<select id="${select_type}">
|
||||
<option value="" disabled selected>Tipo de archivo...</option>
|
||||
<option value="comedor">Galileo - db.comedor.axd</option>
|
||||
<option value="recetas">Galileo - db.recetas.axd</option>
|
||||
<option value="materiales">Galileo - db.materiales.axd</option>
|
||||
<option value="personas">Galileo - db.personas.axd</option>
|
||||
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
|
||||
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
|
||||
</select>
|
||||
<textarea id="${textarea_content}" style="height: 100px;" placeholder="Introduce el contenido del archivo"></textarea>
|
||||
<button id="${button_import}" type="button">Importar</button>
|
||||
<button id="${button_clear}" type="button">Vaciar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_import).onclick = () => {
|
||||
toastr.info("Importando datos...");
|
||||
var val = document.getElementById(textarea_content).value;
|
||||
var sel = document.getElementById(select_type).value;
|
||||
if (sel == "%telesec") {
|
||||
gun.get(TABLE).put(JSON.parse(val), (ack) => {
|
||||
toastr.info("Importado " + entry[0] + ".");
|
||||
});
|
||||
} else {
|
||||
Object.entries(JSON.parse(val)["data"]).forEach((entry) => {
|
||||
var enc = SEA.encrypt(entry[1], SECRET, (encrypted) => {
|
||||
betterGunPut(gun.get(TABLE).get(sel).get(entry[0]), encrypted);
|
||||
});
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
toastr.info("Importado todo!");
|
||||
|
||||
if (sel == "%telesec") {
|
||||
setUrlHash("inicio");
|
||||
} else {
|
||||
setUrlHash(sel);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,44 @@
|
||||
PAGES.index = {
|
||||
//navcss: "btn1",
|
||||
Title: "Inicio",
|
||||
index: function () {
|
||||
icon: "static/appico/house.png",
|
||||
index: function() {
|
||||
container.innerHTML = `
|
||||
<h1>Inicio</h1>
|
||||
<em>Utiliza el menú superior para abrir un modulo</em>
|
||||
<br><br>
|
||||
`;
|
||||
<h1>¡Hola, ${SUB_LOGGED_IN_DETAILS.Nombre}!<br>Bienvenidx a %%TITLE%%</h1>
|
||||
<h2>Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el monedero.</h2>
|
||||
<em>Utiliza el menú superior para abrir un modulo</em>
|
||||
<br><br>
|
||||
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
|
||||
`;
|
||||
},
|
||||
};
|
||||
edit: function(mid) {
|
||||
switch (mid) {
|
||||
case 'qr':
|
||||
PAGES.index.__scan()
|
||||
break;
|
||||
}
|
||||
},
|
||||
__scan: function(mid) {
|
||||
var qrscan = safeuuid()
|
||||
container.innerHTML = `
|
||||
<h1>Escanear Codigo QR</h1>
|
||||
<div style="max-width: 400px;" id="${qrscan}"></div>
|
||||
<br><br>`;
|
||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
qrscan, { fps: 10, qrbox: 250 });
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
html5QrcodeScanner.clear();
|
||||
// Handle on success condition with the decoded text or result.
|
||||
// alert(`Scan result: ${decodedText}`, decodedResult);
|
||||
setUrlHash(decodedText)
|
||||
// ...
|
||||
|
||||
// ^ this will stop the scanner (video feed) and clear the scan area.
|
||||
|
||||
}
|
||||
|
||||
html5QrcodeScanner.render(onScanSuccess);
|
||||
EventListeners.QRScanner.push(html5QrcodeScanner)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,292 @@
|
||||
PAGES.login = {
|
||||
Esconder: true,
|
||||
Title: "Login",
|
||||
edit: function (mid) {
|
||||
// Setup form to configure backend (PouchDB or remoteStorage) and credentials
|
||||
var field_backend = safeuuid();
|
||||
var field_couch = safeuuid();
|
||||
var field_couch_dbname = safeuuid();
|
||||
var field_couch_user = safeuuid();
|
||||
var field_couch_pass = safeuuid();
|
||||
var field_rs_user = safeuuid();
|
||||
var field_rs_token = safeuuid();
|
||||
var field_secret = safeuuid();
|
||||
var btn_import_json = safeuuid();
|
||||
var div_import_area = safeuuid();
|
||||
var field_json = safeuuid();
|
||||
var field_file = safeuuid();
|
||||
var btn_parse_json = safeuuid();
|
||||
var btn_start_scan = safeuuid();
|
||||
var div_scan = safeuuid();
|
||||
var div_pouchdb_fields = safeuuid();
|
||||
var div_remotestorage_fields = safeuuid();
|
||||
var btn_save = safeuuid();
|
||||
|
||||
var savedBackend = localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
|
||||
|
||||
container.innerHTML = `
|
||||
<h1>Configuración de almacenamiento</h1>
|
||||
<b>Aviso: Después de guardar, la aplicación intentará sincronizar en segundo plano. Tenga paciencia.</b>
|
||||
<fieldset>
|
||||
<legend>Tipo de almacenamiento</legend>
|
||||
<label>
|
||||
<input type="radio" name="${field_backend}" value="pouchdb" ${savedBackend === 'pouchdb' ? 'checked' : ''}>
|
||||
PouchDB + CouchDB (sincronización con servidor CouchDB)
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="radio" name="${field_backend}" value="remotestorage" ${savedBackend === 'remotestorage' ? 'checked' : ''}>
|
||||
remoteStorage (sincronización con servidor remoteStorage)
|
||||
</label><br><br>
|
||||
|
||||
<div id="${div_pouchdb_fields}" style="display:${savedBackend === 'pouchdb' ? 'block' : 'none'};">
|
||||
<fieldset>
|
||||
<legend>Configuración CouchDB</legend>
|
||||
<label>Servidor CouchDB (ej: couch.example.com)
|
||||
<input type="text" id="${field_couch}" value="${(localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/^https?:\/\//, '')}"><br><br>
|
||||
</label>
|
||||
<label>Nombre de la base (opcional, por defecto usa telesec)
|
||||
<input type="text" id="${field_couch_dbname}" value="${localStorage.getItem('TELESEC_COUCH_DBNAME') || ''}"><br><br>
|
||||
</label>
|
||||
<label>Usuario
|
||||
<input type="text" id="${field_couch_user}" value="${localStorage.getItem('TELESEC_COUCH_USER') || ''}"><br><br>
|
||||
</label>
|
||||
<label>Contraseña
|
||||
<input type="password" id="${field_couch_pass}" value="${localStorage.getItem('TELESEC_COUCH_PASS') || ''}"><br><br>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div id="${div_remotestorage_fields}" style="display:${savedBackend === 'remotestorage' ? 'block' : 'none'};">
|
||||
<fieldset>
|
||||
<legend>Configuración remoteStorage</legend>
|
||||
<label>Dirección de usuario (ej: user@5apps.com o user@example.com)
|
||||
<input type="text" id="${field_rs_user}" value="${localStorage.getItem('TELESEC_RS_USER') || ''}"><br><br>
|
||||
</label>
|
||||
<label>Token de acceso (opcional, se pedirá al conectar si no se proporciona)
|
||||
<input type="password" id="${field_rs_token}" value="${localStorage.getItem('TELESEC_RS_TOKEN') || ''}"><br><br>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<label>Clave de encriptación (opcional) - usada para cifrar datos en reposo
|
||||
<input type="password" id="${field_secret}" value="${localStorage.getItem('TELESEC_SECRET') || ''}"><br><br>
|
||||
</label>
|
||||
<div style="margin-top:8px;">
|
||||
<button id="${btn_import_json}" class="btn4">Importar desde JSON / QR</button>
|
||||
</div>
|
||||
<div id="${div_import_area}" style="display:none;margin-top:10px;border:1px solid #eee;padding:8px;">
|
||||
<label>Pegar JSON de configuración (o usar archivo / QR):</label><br>
|
||||
<textarea id="${field_json}" style="width:100%;height:120px;margin-top:6px;" placeholder='{"backend":"pouchdb","server":"couch.example.com","dbname":"telesec-test","username":"user","password":"pass","secret":"SECRET123"}'></textarea>
|
||||
<div style="margin-top:6px;">
|
||||
<input type="file" id="${field_file}" accept="application/json">
|
||||
<button id="${btn_parse_json}" class="btn5">Aplicar JSON</button>
|
||||
<button id="${btn_start_scan}" class="btn3">Escanear QR (si disponible)</button>
|
||||
</div>
|
||||
<div id="${div_scan}" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
|
||||
</fieldset>
|
||||
<p>Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.</p>
|
||||
`;
|
||||
|
||||
// Toggle fields based on backend selection
|
||||
var radios = document.getElementsByName(field_backend);
|
||||
for (var i = 0; i < radios.length; i++) {
|
||||
radios[i].addEventListener('change', function() {
|
||||
var selectedBackend = document.querySelector('input[name="' + field_backend + '"]:checked').value;
|
||||
document.getElementById(div_pouchdb_fields).style.display = selectedBackend === 'pouchdb' ? 'block' : 'none';
|
||||
document.getElementById(div_remotestorage_fields).style.display = selectedBackend === 'remotestorage' ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
// Helper: normalize and apply config object
|
||||
function applyConfig(cfg) {
|
||||
try {
|
||||
if (!cfg) throw new Error('JSON vacío');
|
||||
|
||||
var backend = cfg.backend || 'pouchdb';
|
||||
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
|
||||
|
||||
localStorage.setItem('TELESEC_BACKEND', backend);
|
||||
|
||||
if (backend === 'remotestorage') {
|
||||
var rsUser = cfg.rsUserAddress || cfg.rsUser || cfg.rs_user || cfg.user || '';
|
||||
var rsToken = cfg.rsToken || cfg.rs_token || cfg.token || '';
|
||||
if (!rsUser) throw new Error('Falta campo "rsUserAddress" o "user" en JSON para remoteStorage');
|
||||
localStorage.setItem('TELESEC_RS_USER', rsUser);
|
||||
if (rsToken) localStorage.setItem('TELESEC_RS_TOKEN', rsToken);
|
||||
if (secret) {
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
}
|
||||
DB.init({ backend: 'remotestorage', secret: SECRET, rsUserAddress: rsUser, rsToken: rsToken });
|
||||
} else {
|
||||
var url = cfg.server || cfg.couch || cfg.url || cfg.host || cfg.hostname || cfg.server_url;
|
||||
var dbname = cfg.dbname || cfg.database || cfg.db || cfg.name;
|
||||
var user = cfg.username || cfg.user || cfg.u;
|
||||
var pass = cfg.password || cfg.pass || cfg.p;
|
||||
if (!url) throw new Error('Falta campo "server" en JSON para PouchDB');
|
||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + url.replace(/^https?:\/\//, ''));
|
||||
if (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
if (secret) {
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
}
|
||||
DB.init({ backend: 'pouchdb', secret: SECRET, remoteServer: 'https://' + url.replace(/^https?:\/\//, ''), username: user, password: pass, dbname: dbname || undefined });
|
||||
}
|
||||
|
||||
toastr.success('Configuración aplicada e iniciando sincronización');
|
||||
location.hash = '#login';
|
||||
setTimeout(function(){ location.reload(); }, 400);
|
||||
} catch (e) {
|
||||
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle import area
|
||||
document.getElementById(btn_import_json).onclick = function () {
|
||||
var el = document.getElementById(div_import_area);
|
||||
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
|
||||
};
|
||||
|
||||
// Parse textarea JSON
|
||||
document.getElementById(btn_parse_json).onclick = function () {
|
||||
var txt = document.getElementById(field_json).value.trim();
|
||||
if (!txt) { toastr.error('JSON vacío'); return; }
|
||||
try {
|
||||
var obj = JSON.parse(txt);
|
||||
applyConfig(obj);
|
||||
} catch (e) {
|
||||
toastr.error('JSON inválido: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// File input: read JSON file and apply
|
||||
document.getElementById(field_file).addEventListener('change', function (ev) {
|
||||
var f = ev.target.files && ev.target.files[0];
|
||||
if (!f) return;
|
||||
var r = new FileReader();
|
||||
r.onload = function (e) {
|
||||
try {
|
||||
var txt = e.target.result;
|
||||
document.getElementById(field_json).value = txt;
|
||||
var obj = JSON.parse(txt);
|
||||
applyConfig(obj);
|
||||
} catch (err) {
|
||||
toastr.error('Error leyendo archivo JSON: ' + (err && err.message ? err.message : err));
|
||||
}
|
||||
};
|
||||
r.readAsText(f);
|
||||
});
|
||||
|
||||
// QR scanning (if html5-qrcode available)
|
||||
document.getElementById(btn_start_scan).onclick = function () {
|
||||
var scanDiv = document.getElementById(div_scan);
|
||||
scanDiv.innerHTML = '';
|
||||
if (window.Html5QrcodeScanner || window.Html5Qrcode) {
|
||||
try {
|
||||
var targetId = div_scan + '-cam';
|
||||
scanDiv.innerHTML = '<div id="' + targetId + '"></div><div style="margin-top:6px;"><button id="' + targetId + '-stop" class="btn3">Detener</button></div>';
|
||||
var html5Qr;
|
||||
if (window.Html5Qrcode) {
|
||||
html5Qr = new Html5Qrcode(targetId);
|
||||
Html5Qrcode.getCameras().then(function(cameras){
|
||||
var camId = (cameras && cameras[0] && cameras[0].id) ? cameras[0].id : undefined;
|
||||
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
|
||||
try {
|
||||
var obj = JSON.parse(decodedText);
|
||||
html5Qr.stop();
|
||||
applyConfig(obj);
|
||||
} catch (e) {
|
||||
toastr.error('QR no contiene JSON válido');
|
||||
}
|
||||
}, function(err){ /* ignore scan errors */ }).catch(function(err){ toastr.error('Error iniciando cámara: ' + err); });
|
||||
}).catch(function(){
|
||||
// fallback: start without camera list
|
||||
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
|
||||
try { applyConfig(JSON.parse(decodedText)); } catch(e){ toastr.error('QR no contiene JSON válido'); }
|
||||
}, function(){}).catch(function(err){
|
||||
toastr.error('Error iniciando cámara: ' + (err && err.message ? err.message : err));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Html5QrcodeScanner fallback
|
||||
var scanner = new Html5QrcodeScanner(targetId, { fps: 10, qrbox: 250 });
|
||||
scanner.render(function(decodedText){
|
||||
try { applyConfig(JSON.parse(decodedText)); scanner.clear(); } catch(e){ toastr.error('QR no contiene JSON válido'); }
|
||||
});
|
||||
}
|
||||
// stop button
|
||||
document.getElementById(targetId + '-stop').onclick = function () {
|
||||
if (html5Qr && html5Qr.getState && html5Qr.getState() === Html5Qrcode.ScanStatus.SCANNING) {
|
||||
html5Qr.stop().catch(function(){});
|
||||
}
|
||||
scanDiv.innerHTML = '';
|
||||
};
|
||||
} catch (e) {
|
||||
toastr.error('Error al iniciar escáner: ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
scanDiv.innerHTML = '<p>Escáner no disponible. Copia/pega el JSON o sube un archivo.</p>';
|
||||
}
|
||||
};
|
||||
document.getElementById(btn_save).onclick = () => {
|
||||
var backend = document.querySelector('input[name="' + field_backend + '"]:checked').value;
|
||||
var secret = document.getElementById(field_secret).value || '';
|
||||
|
||||
localStorage.setItem('TELESEC_BACKEND', backend);
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
|
||||
try {
|
||||
if (backend === 'remotestorage') {
|
||||
var rsUser = document.getElementById(field_rs_user).value.trim();
|
||||
var rsToken = document.getElementById(field_rs_token).value || '';
|
||||
if (!rsUser) {
|
||||
toastr.error('Debe proporcionar una dirección de usuario remoteStorage');
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('TELESEC_RS_USER', rsUser);
|
||||
localStorage.setItem('TELESEC_RS_TOKEN', rsToken);
|
||||
DB.init({ backend: 'remotestorage', secret: SECRET, rsUserAddress: rsUser, rsToken: rsToken });
|
||||
toastr.success('Iniciando sincronización con remoteStorage');
|
||||
} else {
|
||||
var url = document.getElementById(field_couch).value.trim();
|
||||
var dbname = document.getElementById(field_couch_dbname).value.trim();
|
||||
var user = document.getElementById(field_couch_user).value.trim();
|
||||
var pass = document.getElementById(field_couch_pass).value;
|
||||
localStorage.setItem('TELESEC_COUCH_URL', "https://" + url);
|
||||
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
DB.init({ backend: 'pouchdb', secret: SECRET, remoteServer: "https://" + url, username: user, password: pass, dbname: dbname || undefined });
|
||||
toastr.success('Iniciando sincronización con CouchDB');
|
||||
}
|
||||
|
||||
location.hash = "#login";
|
||||
location.reload();
|
||||
} catch (e) {
|
||||
toastr.error('Error al iniciar sincronización: ' + e.message);
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function (mid) {
|
||||
var field_persona = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_reload = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Iniciar sesión</h1>
|
||||
<fieldset>
|
||||
<legend>Valores</legend>
|
||||
<input type="hidden" id="${field_persona}">
|
||||
<div id="${div_actions}"></div>
|
||||
<button class="btn5" id="${btn_guardar}">Acceder</button>
|
||||
<button class="btn1" id="${btn_reload}">Recargar lista</button>
|
||||
</fieldset>
|
||||
<a style="color: rgb(240,240,240)">Acceso sin cuenta</a>
|
||||
`;
|
||||
<h1>Iniciar sesión</h1>
|
||||
<fieldset>
|
||||
<legend>Valores</legend>
|
||||
<input type="hidden" id="${field_persona}">
|
||||
<div id="${div_actions}"></div>
|
||||
<button class="btn5" id="${btn_guardar}">Acceder</button>
|
||||
<button class="btn3" id="${btn_reload}">Recargar lista</button>
|
||||
<a class="button btn1" href="#login,setup">Configurar base de datos</a>
|
||||
</fieldset>
|
||||
`;
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
@@ -27,9 +297,8 @@ PAGES.login = {
|
||||
},
|
||||
"¿Quién eres?",
|
||||
true,
|
||||
"- Pulsa recargar -"
|
||||
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
|
||||
);
|
||||
document.getElementById("appendApps").style.display = "none"
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
if (document.getElementById(field_persona).value == "") {
|
||||
alert("Tienes que elegir tu cuenta!");
|
||||
@@ -38,13 +307,40 @@ PAGES.login = {
|
||||
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID]
|
||||
SUB_LOGGED_IN = true
|
||||
setUrlHash("index")
|
||||
document.getElementById("appendApps").style.display = "unset"
|
||||
SetPages()
|
||||
if (location.hash.replace("#", "").startsWith("login")) {
|
||||
open_page("index");
|
||||
setUrlHash("index")
|
||||
} else{
|
||||
open_page(location.hash.replace("#", ""));
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById(btn_reload).onclick = () => {
|
||||
setUrlHash("login," + safeuuid(""))
|
||||
open_page("login")
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
// AC_BYPASS: allow creating a local persona from the login screen
|
||||
if (AC_BYPASS) {
|
||||
var btn_bypass_create = safeuuid();
|
||||
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
|
||||
document.getElementById(btn_bypass_create).onclick = () => {
|
||||
var name = prompt("Nombre de la persona (ej: Admin):");
|
||||
if (!name) return;
|
||||
var id = 'bypass-' + Date.now();
|
||||
var persona = { Nombre: name, Roles: 'ADMIN,' };
|
||||
DB.put('personas', id, persona).then(() => {
|
||||
toastr.success('Persona creada: ' + id);
|
||||
localStorage.setItem('TELESEC_BYPASS_ID', id);
|
||||
SUB_LOGGED_IN_ID = id;
|
||||
SUB_LOGGED_IN_DETAILS = persona;
|
||||
SUB_LOGGED_IN = true;
|
||||
SetPages();
|
||||
open_page('index');
|
||||
}).catch((e) => {
|
||||
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,192 +1,265 @@
|
||||
PERMS["materiales"] = "Almacén";
|
||||
PERMS["materiales:edit"] = "> Editar";
|
||||
PAGES.materiales = {
|
||||
navcss: "btn2",
|
||||
Title: "Materiales",
|
||||
icon: "static/appico/shelf.png",
|
||||
AccessControl: true,
|
||||
Title: "Almacén",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("materiales:edit")) {
|
||||
setUrlHash("materiales");
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var field_nombre = safeuuid();
|
||||
var field_revision = safeuuid();
|
||||
var field_cantidad = safeuuid();
|
||||
var field_unidad = safeuuid();
|
||||
var field_cantidad_min = safeuuid();
|
||||
var field_abierto = safeuuid();
|
||||
var field_ubicacion = safeuuid();
|
||||
var field_referencia = safeuuid();
|
||||
var field_notas = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var FECHA_ISO = new Date().toISOString().split("T")[0];
|
||||
container.innerHTML = `
|
||||
<h1>Material <code id="${nameh1}"></code></h1>
|
||||
<fieldset>
|
||||
<label>
|
||||
Referencia<br>
|
||||
<input type="text" id="${field_referencia}" value="?"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Nombre<br>
|
||||
<input type="text" id="${field_nombre}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Unidad<br>
|
||||
<input type="text" id="${field_unidad}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Cantidad Actual<br>
|
||||
<input type="number" step="0.5" id="${field_cantidad}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Cantidad Minima<br>
|
||||
<input type="number" step="0.5" id="${field_cantidad_min}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Ubicación<br>
|
||||
<input type="text" id="${field_ubicacion}" value="-"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("materiales")
|
||||
.get(mid)
|
||||
.once((data, key) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = key;
|
||||
document.getElementById(field_nombre).value = data["Nombre"] || "";
|
||||
document.getElementById(field_unidad).value = data["Unidad"] || "";
|
||||
document.getElementById(field_cantidad).value =
|
||||
data["Cantidad"] || "";
|
||||
document.getElementById(field_cantidad_min).value =
|
||||
data["Cantidad_Minima"] || "";
|
||||
document.getElementById(field_ubicacion).value =
|
||||
data["Ubicacion"] || "-";
|
||||
document.getElementById(field_referencia).value =
|
||||
data["Referencia"] || "?";
|
||||
document.getElementById(field_notas).value = data["Notas"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data);
|
||||
}
|
||||
});
|
||||
<h1>Material <code id="${nameh1}"></code></h1>
|
||||
${BuildQR("materiales," + mid, "Este Material")}
|
||||
<fieldset>
|
||||
<label>
|
||||
Fecha Revisión<br>
|
||||
<input type="date" id="${field_revision}"> <a onclick='document.getElementById("${field_revision}").value = "${FECHA_ISO}";'>Hoy - Contado todas las existencias</a><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Nombre<br>
|
||||
<input type="text" id="${field_nombre}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Unidad<br>
|
||||
<input type="text" id="${field_unidad}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Cantidad Actual<br>
|
||||
<input type="number" step="0.5" id="${field_cantidad}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Cantidad Minima<br>
|
||||
<input type="number" step="0.5" id="${field_cantidad_min}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Ubicación<br>
|
||||
<input type="text" id="${field_ubicacion}" value="-"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
DB.get('materiales', mid).then((data) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_nombre).value = data["Nombre"] || "";
|
||||
document.getElementById(field_unidad).value =
|
||||
data["Unidad"] || "unidad(es)";
|
||||
document.getElementById(field_cantidad).value =
|
||||
data["Cantidad"] || "";
|
||||
document.getElementById(field_cantidad_min).value =
|
||||
data["Cantidad_Minima"] || "";
|
||||
document.getElementById(field_ubicacion).value =
|
||||
data["Ubicacion"] || "-";
|
||||
document.getElementById(field_revision).value =
|
||||
data["Revision"] || "-";
|
||||
document.getElementById(field_notas).value = data["Notas"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'materiales', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Nombre: document.getElementById(field_nombre).value,
|
||||
Unidad: document.getElementById(field_unidad).value,
|
||||
Cantidad: document.getElementById(field_cantidad).value,
|
||||
Cantidad_Minima: document.getElementById(field_cantidad_min).value,
|
||||
Ubicacion: document.getElementById(field_ubicacion).value,
|
||||
Referencia: document.getElementById(field_referencia).value,
|
||||
Revision: document.getElementById(field_revision).value,
|
||||
Notas: document.getElementById(field_notas).value,
|
||||
};
|
||||
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
betterGunPut(gun.get(TABLE).get("materiales").get(mid), encrypted);
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('materiales', mid, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("materiales");
|
||||
}, 1500);
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar el material");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar este material?") == true) {
|
||||
betterGunPut(gun.get(TABLE).get("materiales").get(mid), null);
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("materiales");
|
||||
}, 1500);
|
||||
DB.del('materiales', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("materiales");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Materiales</h1>
|
||||
<button id="${btn_new}">Nuevo Material</button>
|
||||
<div id="scrolltable"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Referencia</th>
|
||||
<th>Nombre</th>
|
||||
<th>Ubicación</th>
|
||||
<th>Cantidad</th>
|
||||
<th>Notas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="${tablebody}">
|
||||
</tbody>
|
||||
</table></div>
|
||||
`;
|
||||
tableScroll("#scrolltable");
|
||||
var tablebody_EL = document.getElementById(tablebody);
|
||||
var rows = {};
|
||||
function render() {
|
||||
function sorter(a, b) {
|
||||
if (a.Nombre < b.Nombre) {
|
||||
return -1;
|
||||
}
|
||||
if (a.Nombre > b.Nombre) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
var tablebody_EL = document.getElementById(tablebody);
|
||||
tablebody_EL.innerHTML = "";
|
||||
Object.values(rows)
|
||||
.sort(sorter)
|
||||
.forEach((data) => {
|
||||
var new_tr = document.createElement("tr");
|
||||
new_tr.innerHTML = `
|
||||
<td>${data.Referencia || "?"}</td>
|
||||
<td>${data.Nombre || "?"}</td>
|
||||
<td>${data.Ubicacion || "?"}</td>
|
||||
<td>${data.Cantidad || "?"} ${data.Unidad || "?"}</td>
|
||||
<td>${data.Notas || "?"}</td>
|
||||
`;
|
||||
var min = parseFloat(data.Cantidad_Minima);
|
||||
var act = parseFloat(data.Cantidad);
|
||||
if (act < min) {
|
||||
new_tr.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
new_tr.onclick = () => {
|
||||
setUrlHash("materiales," + data._key);
|
||||
};
|
||||
tablebody_EL.append(new_tr);
|
||||
});
|
||||
if (!checkRole("materiales")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("materiales")
|
||||
.map()
|
||||
.on((data, key, _msg, _ev) => {
|
||||
EVENTLISTENER = _ev;
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
data["_key"] = key;
|
||||
rows[key] = data;
|
||||
} else {
|
||||
delete rows[key];
|
||||
var btn_new = safeuuid();
|
||||
var select_ubicacion = safeuuid();
|
||||
var check_lowstock = safeuuid();
|
||||
var tableContainer = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Materiales del Almacén</h1>
|
||||
<label>
|
||||
<b>Solo lo que falta:</b>
|
||||
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;">
|
||||
</label><br>
|
||||
<label>Filtrar por ubicación:
|
||||
<select id="${select_ubicacion}">
|
||||
<option value="">(Todas)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="${btn_new}">Nuevo Material</button>
|
||||
<div id="${tableContainer}"></div>
|
||||
`;
|
||||
|
||||
const config = [
|
||||
{ key: "Revision", label: "Ult. Revisión", type: "fecha-diff", default: "" },
|
||||
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
|
||||
{ key: "Ubicacion", label: "Ubicación", type: "text", default: "--" },
|
||||
{
|
||||
key: "Cantidad",
|
||||
label: "Cantidad",
|
||||
type: "template",
|
||||
template: (data, element) => {
|
||||
const min = parseFloat(data.Cantidad_Minima);
|
||||
const act = parseFloat(data.Cantidad);
|
||||
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || "?"}</small>` : ""
|
||||
element.innerHTML = `${data.Cantidad || "?"} ${
|
||||
data.Unidad || "?"
|
||||
} ${sma}`;
|
||||
},
|
||||
default: "?",
|
||||
},
|
||||
{ key: "Notas", label: "Notas", type: "text", default: "" },
|
||||
];
|
||||
|
||||
// Obtener todas las ubicaciones únicas y poblar el <select>, desencriptando si es necesario
|
||||
DB.map("materiales", (data, key) => {
|
||||
try {
|
||||
if (!data) return;
|
||||
|
||||
function addUbicacion(d) {
|
||||
const ubicacion = d.Ubicacion || "-";
|
||||
const select = document.getElementById(select_ubicacion);
|
||||
|
||||
if (!select) {
|
||||
console.warn(`Element with ID "${select_ubicacion}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const optionExists = Array.from(select.options).some(
|
||||
(opt) => opt.value === ubicacion
|
||||
);
|
||||
if (!optionExists) {
|
||||
const option = document.createElement("option");
|
||||
option.value = ubicacion;
|
||||
option.textContent = ubicacion;
|
||||
select.appendChild(option);
|
||||
}
|
||||
render();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
|
||||
if (typeof data === "string") {
|
||||
TS_decrypt(data, SECRET, (dec, wasEncrypted) => {
|
||||
if (dec && typeof dec === "object") {
|
||||
addUbicacion(dec);
|
||||
}
|
||||
}, 'materiales', key);
|
||||
} else {
|
||||
add_row(data, key);
|
||||
addUbicacion(data);
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("materiales," + safeuuid(""));
|
||||
} catch (error) {
|
||||
console.warn("Error processing ubicacion:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Función para renderizar la tabla filtrada
|
||||
function renderTable(filtroUbicacion) {
|
||||
TS_IndexElement(
|
||||
"materiales",
|
||||
config,
|
||||
"materiales",
|
||||
document.getElementById(tableContainer),
|
||||
function (data, new_tr) {
|
||||
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
|
||||
new_tr.style.background = "#fcfcb0";
|
||||
}
|
||||
if (parseFloat(data.Cantidad) <= 0) {
|
||||
new_tr.style.background = "#ffc0c0";
|
||||
}
|
||||
if ((data.Cantidad || "?") == "?") {
|
||||
new_tr.style.background = "#d0d0ff";
|
||||
}
|
||||
if ((data.Revision || "?") == "?") {
|
||||
new_tr.style.background = "#d0d0ff";
|
||||
}
|
||||
},
|
||||
function (data) {
|
||||
var is_low_stock =
|
||||
!document.getElementById(check_lowstock).checked ||
|
||||
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
|
||||
|
||||
var is_region =
|
||||
filtroUbicacion === "" || data.Ubicacion === filtroUbicacion;
|
||||
|
||||
return !(is_low_stock && is_region);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inicializar tabla sin filtro
|
||||
renderTable("");
|
||||
|
||||
// Evento para filtrar por ubicación
|
||||
document.getElementById(select_ubicacion).onchange = function () {
|
||||
renderTable(this.value);
|
||||
};
|
||||
// Recargar al cambiar filtro
|
||||
document.getElementById(check_lowstock).onchange = function () {
|
||||
renderTable(document.getElementById(select_ubicacion).value);
|
||||
};
|
||||
|
||||
if (!checkRole("materiales:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none";
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("materiales," + safeuuid(""));
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
252
src/page/notas.js
Normal file
@@ -0,0 +1,252 @@
|
||||
PERMS["notas"] = "Notas"
|
||||
PERMS["notas:edit"] = "> Editar"
|
||||
PAGES.notas = {
|
||||
navcss: "btn5",
|
||||
icon: "static/appico/edit.png",
|
||||
AccessControl: true,
|
||||
Title: "Notas",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("notas:edit")) {setUrlHash("notas");return}
|
||||
var nameh1 = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_contenido = safeuuid();
|
||||
var field_autor = safeuuid();
|
||||
var field_files = safeuuid();
|
||||
var attachments_list = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Nota <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||
<legend>Valores</legend>
|
||||
<div style="max-width: 400px;">
|
||||
<label>
|
||||
Asunto<br>
|
||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
||||
</label>
|
||||
<input type="hidden" id="${field_autor}" value="">
|
||||
<div id="${div_actions}"></div>
|
||||
</div>
|
||||
<label>
|
||||
Contenido<br>
|
||||
<textarea id="${field_contenido}" style="width: calc(100% - 15px); height: 400px;"></textarea><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Adjuntos (Fotos o archivos)<br>
|
||||
<input type="file" id="${field_files}" multiple><br><br>
|
||||
<div id="${attachments_list}"></div>
|
||||
</label>
|
||||
<hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
SUB_LOGGED_IN_ID,
|
||||
(value) => {
|
||||
document.getElementById(field_autor).value = value;
|
||||
},
|
||||
"Autor"
|
||||
);
|
||||
DB.get('notas', mid).then((data) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
||||
document.getElementById(field_contenido).value =
|
||||
data["Contenido"] || "";
|
||||
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
|
||||
|
||||
// Persona select
|
||||
divact.innerHTML = "";
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data["Autor"] || SUB_LOGGED_IN_ID || "",
|
||||
(value) => {
|
||||
document.getElementById(field_autor).value = value;
|
||||
},
|
||||
"Autor"
|
||||
);
|
||||
// Mostrar adjuntos existentes (si los hay).
|
||||
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
attachContainer.innerHTML = "";
|
||||
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
|
||||
DB.listAttachments('notas', mid).then((list) => {
|
||||
if (!list || !Array.isArray(list)) return;
|
||||
list.forEach((att) => {
|
||||
addAttachmentRow(att.name, att.dataUrl);
|
||||
});
|
||||
}).catch((e) => { console.warn('listAttachments error', e); });
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
// gestión de archivos seleccionados antes de guardar
|
||||
const attachmentsToUpload = [];
|
||||
function addAttachmentRow(name, url) {
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
const idRow = safeuuid();
|
||||
const isImage = url && url.indexOf('data:image') === 0;
|
||||
const preview = isImage ? `<img src="${url}" height="80" style="margin-right:8px;">` : `<a href="${url}" target="_blank">${name}</a>`;
|
||||
const html = `
|
||||
<div id="${idRow}" style="display:flex;align-items:center;margin:6px 0;border:1px solid #ddd;padding:6px;border-radius:6px;">
|
||||
<div style="flex:1">${preview}<strong style="margin-left:8px">${name}</strong></div>
|
||||
<div><button type="button" class="rojo" data-name="${name}">Borrar</button></div>
|
||||
</div>`;
|
||||
attachContainer.insertAdjacentHTML('beforeend', html);
|
||||
attachContainer.querySelectorAll(`button[data-name="${name}"]`).forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
if (!confirm('¿Borrar este adjunto?')) return;
|
||||
// Usar API pública en DB para borrar metadata del attachment
|
||||
DB.deleteAttachment('notas', mid, name).then((ok) => {
|
||||
if (ok) {
|
||||
document.getElementById(idRow).remove();
|
||||
toastr.error('Adjunto borrado');
|
||||
} else {
|
||||
toastr.error('No se pudo borrar el adjunto');
|
||||
}
|
||||
}).catch((e) => { console.warn('deleteAttachment error', e); toastr.error('Error borrando adjunto'); });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById(field_files).addEventListener('change', function (e) {
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
const dataUrl = ev.target.result;
|
||||
attachmentsToUpload.push({ name: file.name, data: dataUrl, type: file.type || 'application/octet-stream' });
|
||||
// mostrar preview temporal
|
||||
addAttachmentRow(file.name, dataUrl);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
// limpiar input para permitir re-subidas del mismo archivo
|
||||
e.target.value = '';
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Autor: document.getElementById(field_autor).value,
|
||||
Contenido: document.getElementById(field_contenido).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
};
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('notas', mid, data).then(() => {
|
||||
// subir attachments si los hay
|
||||
const uploadPromises = [];
|
||||
attachmentsToUpload.forEach((att) => {
|
||||
if (DB.putAttachment) {
|
||||
uploadPromises.push(DB.putAttachment('notas', mid, att.name, att.data, att.type).catch((e) => { console.warn('putAttachment error', e); }));
|
||||
}
|
||||
});
|
||||
Promise.all(uploadPromises).then(() => {
|
||||
// limpiar lista temporal y recargar attachments
|
||||
attachmentsToUpload.length = 0;
|
||||
try { // recargar lista actual sin salir
|
||||
const pouchId = 'notas:' + mid;
|
||||
if (DB && DB._internal && DB._internal.local) {
|
||||
DB._internal.local.get(pouchId, { attachments: true }).then((doc) => {
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
attachContainer.innerHTML = '';
|
||||
if (doc && doc._attachments) {
|
||||
Object.keys(doc._attachments).forEach((name) => {
|
||||
try {
|
||||
const att = doc._attachments[name];
|
||||
if (att && att.data) {
|
||||
const durl = 'data:' + (att.content_type || 'application/octet-stream') + ';base64,' + att.data;
|
||||
addAttachmentRow(name, durl);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
DB.getAttachment('notas', mid, name).then((durl) => { addAttachmentRow(name, durl); }).catch(() => {});
|
||||
});
|
||||
}
|
||||
}).catch(() => { /* ignore reload errors */ });
|
||||
}
|
||||
} catch (e) {}
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("notas");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('Attachment upload error', e);
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
toastr.error("Error al guardar los adjuntos");
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
toastr.error("Error al guardar la nota");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta nota?") == true) {
|
||||
DB.del('notas', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("notas");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole("notas")) {setUrlHash("index");return}
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Notas</h1>
|
||||
<button id="${btn_new}">Nueva nota</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"notas",
|
||||
[
|
||||
{
|
||||
key: "Autor",
|
||||
type: "persona-nombre",
|
||||
default: "",
|
||||
label: "Autor",
|
||||
},
|
||||
{
|
||||
key: "Asunto",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Asunto",
|
||||
},
|
||||
],
|
||||
"notas",
|
||||
document.querySelector("#cont"),
|
||||
);
|
||||
if (!checkRole("notas:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none"
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("notas," + safeuuid(""));
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
PAGES.notificaciones = {
|
||||
navcss: "btn6",
|
||||
Title: "Notificaciones",
|
||||
edit: function (mid) {
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_origen = safeuuid();
|
||||
var field_destino = safeuuid();
|
||||
var field_estado = safeuuid();
|
||||
var field_mensaje = safeuuid();
|
||||
var field_respuesta = safeuuid();
|
||||
var btn_leer = safeuuid();
|
||||
var btn_desleer = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Notificación <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Valores</legend>
|
||||
<label>
|
||||
Fecha<br>
|
||||
<input readonly disabled type="text" id="${field_fecha}" value="${CurrentISODate()}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Asunto<br>
|
||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
||||
</label>
|
||||
<input type="hidden" id="${field_origen}">
|
||||
<input type="hidden" id="${field_destino}">
|
||||
<div id="${div_actions}"></div>
|
||||
<label>
|
||||
Mensaje<br>
|
||||
<textarea id="${field_mensaje}"></textarea><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Respuesta<br>
|
||||
<textarea id="${field_respuesta}"></textarea><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Estado<br>
|
||||
<input readonly disabled type="text" id="${field_estado}" value="%%">
|
||||
<br>
|
||||
<button id="${btn_leer}">Leido</button>
|
||||
<button id="${btn_desleer}">No leido</button>
|
||||
<br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(btn_leer).onclick = () => {
|
||||
document.getElementById(field_estado).value = "leido";
|
||||
};
|
||||
document.getElementById(btn_desleer).onclick = () => {
|
||||
document.getElementById(field_estado).value = "por_leer";
|
||||
};
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
"",
|
||||
(value) => {
|
||||
document.getElementById(field_origen).value = value;
|
||||
},
|
||||
"Origen"
|
||||
);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
"",
|
||||
(value) => {
|
||||
document.getElementById(field_destino).value = value;
|
||||
},
|
||||
"Destino"
|
||||
);
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("notificaciones")
|
||||
.get(mid)
|
||||
.once((data, key) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = key;
|
||||
document.getElementById(field_fecha).value = data["Fecha"];
|
||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
||||
document.getElementById(field_mensaje).value =
|
||||
data["Mensaje"] || "";
|
||||
document.getElementById(field_origen).value = data["Origen"] || "";
|
||||
document.getElementById(field_destino).value =
|
||||
data["Destino"] || "";
|
||||
document.getElementById(field_estado).value = data["Estado"] || "";
|
||||
document.getElementById(field_respuesta).value =
|
||||
data["Respuesta"] || "";
|
||||
|
||||
// Persona select
|
||||
divact.innerHTML = "";
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data["Origen"] || "",
|
||||
(value) => {
|
||||
document.getElementById(field_origen).value = value;
|
||||
},
|
||||
"Origen"
|
||||
);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data["Destino"] || "",
|
||||
(value) => {
|
||||
document.getElementById(field_destino).value = value;
|
||||
},
|
||||
"Destino"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data);
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
if (document.getElementById(field_origen).value == "") {
|
||||
alert("¡Hay que elegir una persona de origen!");
|
||||
return;
|
||||
}
|
||||
if (document.getElementById(field_destino).value == "") {
|
||||
alert("¡Hay que elegir una persona de origen!");
|
||||
return;
|
||||
}
|
||||
var data = {
|
||||
Fecha: document.getElementById(field_fecha).value,
|
||||
Origen: document.getElementById(field_origen).value,
|
||||
Destino: document.getElementById(field_destino).value,
|
||||
Mensaje: document.getElementById(field_mensaje).value,
|
||||
Respuesta: document.getElementById(field_respuesta).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
Estado: document
|
||||
.getElementById(field_estado)
|
||||
.value.replace("%%", "por_leer"),
|
||||
};
|
||||
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
betterGunPut(
|
||||
gun.get(TABLE).get("notificaciones").get(mid),
|
||||
encrypted
|
||||
);
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("notificaciones");
|
||||
}, 1500);
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta notificación?") == true) {
|
||||
betterGunPut(gun.get(TABLE).get("notificaciones").get(mid), null);
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("notificaciones");
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Notificaciones</h1>
|
||||
<button id="${btn_new}">Nueva notificación</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
"notificaciones",
|
||||
[
|
||||
{
|
||||
key: "Origen",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Origen",
|
||||
},
|
||||
{
|
||||
key: "Destino",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Destino",
|
||||
},
|
||||
{
|
||||
key: "Asunto",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Asunto",
|
||||
},
|
||||
{
|
||||
key: "Estado",
|
||||
type: "raw",
|
||||
default: "",
|
||||
label: "Estado",
|
||||
},
|
||||
],
|
||||
gun.get(TABLE).get("notificaciones"),
|
||||
document.querySelector("#cont"),
|
||||
(data, new_tr) => {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
if (data.Estado == "leido") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
}
|
||||
);
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("notificaciones," + safeuuid(""));
|
||||
};
|
||||
},
|
||||
}
|
||||
1420
src/page/pagos.js
Normal file
@@ -1,222 +1,255 @@
|
||||
PERMS["personas"] = "Personas";
|
||||
PERMS["personas:edit"] = "> Editar";
|
||||
PAGES.personas = {
|
||||
navcss: "btn4",
|
||||
navcss: "btn3",
|
||||
icon: "static/appico/users.png",
|
||||
AccessControl: true,
|
||||
Title: "Personas",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("personas:edit")) {
|
||||
setUrlHash("personas");
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var permisosdet = safeuuid();
|
||||
var field_nombre = safeuuid();
|
||||
var field_zona = safeuuid();
|
||||
var field_roles = safeuuid();
|
||||
var field_puntos = safeuuid();
|
||||
var field_notas = safeuuid();
|
||||
var field_anilla = safeuuid();
|
||||
var field_foto = safeuuid();
|
||||
var render_foto = safeuuid();
|
||||
var field_monedero_balance = safeuuid();
|
||||
var field_monedero_notas = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var btn_ver_monedero = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Persona <code id="${nameh1}"></code></h1>
|
||||
<fieldset>
|
||||
<label>
|
||||
Nombre<br>
|
||||
<input type="text" id="${field_nombre}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Zona<br>
|
||||
<input type="text" id="${field_zona}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Permisos<br>
|
||||
<input type="text" id="${field_roles}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Puntos<br>
|
||||
<input type="number" id="${field_puntos}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Anilla<br>
|
||||
<input type="color" id="${field_anilla}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Foto (PNG o JPG)<br>
|
||||
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px;" src="static/camera2.png">
|
||||
<input type="file" accept="image/*" id="${field_foto}" style="display: none;"><br><br>
|
||||
</label>
|
||||
<h1>Persona <code id="${nameh1}"></code></h1>
|
||||
${BuildQR("personas," + mid, "Esta Persona")}
|
||||
<fieldset>
|
||||
<label>
|
||||
Nombre<br>
|
||||
<input type="text" id="${field_nombre}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Zona<br>
|
||||
<input type="text" id="${field_zona}"><br><br>
|
||||
</label>
|
||||
</label>
|
||||
<details>
|
||||
<summary>Permisos</summary>
|
||||
<form id="${permisosdet}">
|
||||
</form>
|
||||
</details>
|
||||
<label>
|
||||
Anilla<br>
|
||||
<input type="color" id="${field_anilla}"><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Foto (PNG o JPG)<br>
|
||||
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px;" src="static/ico/user_generic.png">
|
||||
<input type="file" accept="image/*" id="${field_foto}" style="display: none;"><br><br>
|
||||
</label>
|
||||
|
||||
<details style="background: #e3f2fd; border: 2px solid #2196f3; border-radius: 8px; padding: 10px; margin: 15px 0;">
|
||||
<summary style="cursor: pointer; font-weight: bold; color: #1976d2;">💳 Tarjeta Monedero</summary>
|
||||
<div style="padding: 15px;">
|
||||
<label>
|
||||
Balance Actual<br>
|
||||
<input type="number" step="0.01" id="${field_monedero_balance}" style="font-size: 24px; font-weight: bold; color: #1976d2;"><br>
|
||||
<small>Se actualiza automáticamente con las transacciones</small><br><br>
|
||||
</label>
|
||||
<label>
|
||||
Notas del Monedero<br>
|
||||
<textarea id="${field_monedero_notas}" rows="3" placeholder="Notas adicionales sobre el monedero..."></textarea><br><br>
|
||||
</label>
|
||||
<button type="button" id="${btn_ver_monedero}" class="btn5">Ver Transacciones del Monedero</button>
|
||||
</div>
|
||||
</details>
|
||||
<details style="background: #e3fde3ff; border: 2px solid #21f328ff; border-radius: 8px; padding: 10px; margin: 15px 0; display: none;">
|
||||
<summary style="cursor: pointer; font-weight: bold; color: rgba(26, 141, 3, 1);">🔗 Generar enlaces</summary>
|
||||
<div style="padding: 15px;">
|
||||
<label>
|
||||
Este servidor<br>
|
||||
<input type="url" value="${location.protocol}//${location.hostname}:${location.port}${location.pathname}?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
|
||||
</label>
|
||||
<label>
|
||||
Cualquier Servidor<br>
|
||||
<input type="url" value="https://tech.eus/ts/?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label><hr>
|
||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
var resized = "";
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("personas")
|
||||
.get(mid)
|
||||
.once((data, key) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = key;
|
||||
document.getElementById(field_nombre).value = data["Nombre"] || "";
|
||||
document.getElementById(field_zona).value = data["Region"] || "";
|
||||
document.getElementById(field_roles).value = data["Roles"] || "";
|
||||
document.getElementById(field_puntos).value = data["Puntos"] || 0;
|
||||
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
|
||||
// document.getElementById(field_foto).value = "";
|
||||
document.getElementById(render_foto).src =
|
||||
data["Foto"] || "static/ico/user_generic.png";
|
||||
resized = data["Foto"] || "static/ico/user_generic.png";
|
||||
document.getElementById(field_notas).value = data["markdown"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data);
|
||||
}
|
||||
});
|
||||
document
|
||||
.getElementById(field_foto)
|
||||
.addEventListener("change", function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
resizeInputImage(
|
||||
file,
|
||||
function (url) {
|
||||
document.getElementById(render_foto).src = url;
|
||||
resized = url;
|
||||
},
|
||||
125,
|
||||
0.7
|
||||
);
|
||||
});
|
||||
var pdel = document.getElementById(permisosdet);
|
||||
DB.get('personas', mid).then((data) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
var pot = "<ul>";
|
||||
Object.entries(PERMS).forEach((page) => {
|
||||
var c = "";
|
||||
if ((data["Roles"] || ",").split(",").includes(page[0])) {
|
||||
c = "checked";
|
||||
}
|
||||
pot += `
|
||||
<li><label>
|
||||
<input name="perm" value="${page[0]}" type="checkbox" ${c}>
|
||||
${page[1]}
|
||||
</label></li>
|
||||
`;
|
||||
});
|
||||
pdel.innerHTML = pot + "</ul>";
|
||||
document.getElementById(field_nombre).value = data["Nombre"] || "";
|
||||
document.getElementById(field_zona).value = data["Region"] || "";
|
||||
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
|
||||
// set fallback image immediately
|
||||
document.getElementById(render_foto).src = data["Foto"] || "static/ico/user_generic.png";
|
||||
resized = data["Foto"] || "static/ico/user_generic.png";
|
||||
// try to load attachment 'foto' if present (preferred storage)
|
||||
DB.getAttachment('personas', mid, 'foto').then((durl) => {
|
||||
if (durl) {
|
||||
document.getElementById(render_foto).src = durl;
|
||||
resized = durl;
|
||||
}
|
||||
}).catch(() => {});
|
||||
document.getElementById(field_notas).value = data["markdown"] || "";
|
||||
document.getElementById(field_monedero_balance).value =
|
||||
data["Monedero_Balance"] || 0;
|
||||
document.getElementById(field_monedero_notas).value =
|
||||
data["Monedero_Notas"] || "";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'personas', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
document.getElementById(field_foto).addEventListener("change", function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
// Do NOT resize — keep original uploaded image
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
const url = ev.target.result;
|
||||
document.getElementById(render_foto).src = url;
|
||||
resized = url;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var dt = new FormData(pdel);
|
||||
var data = {
|
||||
Nombre: document.getElementById(field_nombre).value,
|
||||
Region: document.getElementById(field_zona).value,
|
||||
Roles: document.getElementById(field_roles).value,
|
||||
Puntos: document.getElementById(field_puntos).value,
|
||||
Roles: dt.getAll("perm").join(",") + ",",
|
||||
SC_Anilla: document.getElementById(field_anilla).value,
|
||||
Foto: resized,
|
||||
// Foto moved to PouchDB attachment named 'foto'
|
||||
markdown: document.getElementById(field_notas).value,
|
||||
Monedero_Balance:
|
||||
parseFloat(document.getElementById(field_monedero_balance).value) ||
|
||||
0,
|
||||
Monedero_Notas: document.getElementById(field_monedero_notas).value,
|
||||
};
|
||||
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
betterGunPut(gun.get(TABLE).get("personas").get(mid), encrypted);
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('personas', mid, data).then(() => {
|
||||
// if resized is a data URL (new/updated image), save as attachment
|
||||
var attachPromise = Promise.resolve(true);
|
||||
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
|
||||
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
|
||||
}
|
||||
attachPromise.then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("personas");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('putAttachment error', e);
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("personas");
|
||||
}, 1500);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
toastr.error("Error al guardar la foto");
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
toastr.error("Error al guardar la persona");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_ver_monedero).onclick = () => {
|
||||
setUrlHash("pagos"); // Navigate to pagos and show transactions for this person
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm("¿Quieres borrar esta persona?") == true) {
|
||||
betterGunPut(gun.get(TABLE).get("personas").get(mid), null);
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("personas");
|
||||
}, 1500);
|
||||
DB.del('personas', mid).then(() => {
|
||||
toastr.error("Borrado!");
|
||||
setTimeout(() => {
|
||||
setUrlHash("personas");
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
const tablebody = safeuuid();
|
||||
if (!checkRole("personas")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Personas</h1>
|
||||
<button id="${btn_new}">Nueva Persona</button>
|
||||
<div id="scrolltable"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Zona</th>
|
||||
<th>Puntos</th>
|
||||
<th>Permisos</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="${tablebody}">
|
||||
</tbody>
|
||||
</table></div>
|
||||
`;
|
||||
tableScroll("#scrolltable"); // id="scrolltable"
|
||||
var tablebody_EL = document.getElementById(tablebody);
|
||||
var rows = {};
|
||||
function render() {
|
||||
function sorter(a, b) {
|
||||
if (a.Region.toUpperCase() < b.Region.toUpperCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.Region.toUpperCase() > b.Region.toUpperCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
var tablebody_EL = document.getElementById(tablebody);
|
||||
tablebody_EL.innerHTML = "";
|
||||
// SC_Personas = rows
|
||||
Object.values(rows)
|
||||
.sort(sorter)
|
||||
.forEach((data) => {
|
||||
var btn_comanda = safeuuid();
|
||||
var new_tr = document.createElement("tr");
|
||||
new_tr.innerHTML = `
|
||||
<td class="TextBorder" style="background-color: ${
|
||||
data.SC_Anilla
|
||||
}; text-align: center"><img src="${
|
||||
data.Foto || "static/ico/user_generic.png"
|
||||
}" height="50"> <br> ${data.Nombre || ""}</td>
|
||||
<td>${data.Region || "?"}</td>
|
||||
<td>${data.Puntos || 0}</td>
|
||||
<td>${data.Roles || ""}</td>
|
||||
`;
|
||||
<h1>Personas</h1>
|
||||
<button id="${btn_new}">Nueva Persona</button>
|
||||
<div id="tableContainer"></div>
|
||||
`;
|
||||
|
||||
// <button id="${btn_comanda}" class="${PAGES.ventas.navcss}">Nueva venta</button>
|
||||
var act = parseFloat(data.Puntos);
|
||||
if (act >= 10) {
|
||||
new_tr.style.backgroundColor = "gold";
|
||||
}
|
||||
new_tr.onclick = () => {
|
||||
setUrlHash("personas," + data._key);
|
||||
};
|
||||
tablebody_EL.append(new_tr);
|
||||
// document.getElementById(btn_comanda).onclick = (e) => {
|
||||
// setUrlHash("ventas," + data._key);
|
||||
// if (!e) var e = window.event;
|
||||
// e.cancelBubble = true;
|
||||
// if (e.stopPropagation) e.stopPropagation();
|
||||
// };
|
||||
});
|
||||
const config = [
|
||||
// {
|
||||
// label: "Persona",
|
||||
// type: "persona",
|
||||
// self: true,
|
||||
// },
|
||||
{ key: "Foto", label: "Foto", type: "attachment-persona", default: "", self: true },
|
||||
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
|
||||
{ key: "Region", label: "Zona", type: "text", default: "" },
|
||||
{ key: "Monedero_Balance", label: "Saldo Monedero", type: "moneda" },
|
||||
//{ key: "markdown", label: "Notas", type: "markdown", default: "" },
|
||||
//{ key: "Roles", label: "Permisos", type: "text", default: "" }
|
||||
];
|
||||
|
||||
TS_IndexElement(
|
||||
"personas",
|
||||
config,
|
||||
"personas",
|
||||
document.getElementById("tableContainer"),
|
||||
undefined,
|
||||
undefined,
|
||||
true // Enable global search bar
|
||||
);
|
||||
if (!checkRole("personas:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none";
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("personas," + safeuuid(""));
|
||||
};
|
||||
}
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("personas")
|
||||
.map()
|
||||
.on((data, key, _msg, _ev) => {
|
||||
EVENTLISTENER = _ev;
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
data["_key"] = key;
|
||||
rows[key] = data;
|
||||
} else {
|
||||
delete rows[key];
|
||||
}
|
||||
render();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
} else {
|
||||
add_row(data, key);
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("personas," + safeuuid(""));
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,167 +1,103 @@
|
||||
PERMS["resumen_diario"] = "Resumen diario (Solo docentes!)";
|
||||
PAGES.resumen_diario = {
|
||||
icon: "static/appico/calendar.png",
|
||||
navcss: "btn3",
|
||||
AccessControl: true,
|
||||
Title: "Resumen Diario",
|
||||
index: function () {
|
||||
var table_materialesLow = safeuuid();
|
||||
var table_personasHigh = safeuuid();
|
||||
var table_comedor = safeuuid();
|
||||
var data_Comedor = safeuuid();
|
||||
var data_Tareas = safeuuid();
|
||||
var data_Diario = safeuuid();
|
||||
var data_Weather = safeuuid();
|
||||
if (!checkRole("resumen_diario")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<h1>Resumen Diario</h1>
|
||||
<h2>Menú del comedor de hoy</h2>
|
||||
<span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;" id="${table_comedor}"></span>
|
||||
<h2>Personas con café gratis (para el Viernes)</h2>
|
||||
<div id="${table_personasHigh}"></div>
|
||||
<h2>Materiales faltantes (o por llegar)</h2>
|
||||
<div id="${table_materialesLow}"></div>
|
||||
`;
|
||||
var materiales_low = {};
|
||||
var personas_high = {};
|
||||
function render_materialesLow() {
|
||||
function sorter(a, b) {
|
||||
if (a.Nombre < b.Nombre) {
|
||||
return -1;
|
||||
<h1>Resumen Diario ${CurrentISODate()}</h1>
|
||||
<button onclick="print()">Imprimir</button>
|
||||
<br><span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
|
||||
<br><span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
|
||||
<br><span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
|
||||
<br><span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; height: 75px;" id="${data_Weather}"></span>
|
||||
`;
|
||||
|
||||
//#region Cargar Clima
|
||||
// Get location from DB settings.weather_location; if missing ask user and save it
|
||||
// url format: https://wttr.in/<loc>?F0m
|
||||
DB.get('settings','weather_location').then((loc) => {
|
||||
if (!loc) {
|
||||
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
|
||||
if (loc) {
|
||||
DB.put('settings','weather_location', loc);
|
||||
}
|
||||
if (a.Nombre > b.Nombre) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
var tablebody_EL = document.getElementById(table_materialesLow);
|
||||
tablebody_EL.innerHTML = "";
|
||||
Object.values(materiales_low)
|
||||
.sort(sorter)
|
||||
.forEach((data) => {
|
||||
var min = parseFloat(data.Cantidad_Minima);
|
||||
var act = parseFloat(data.Cantidad);
|
||||
var falta = min - act;
|
||||
if (act < min) {
|
||||
var new_tr = document.createElement("span");
|
||||
new_tr.innerHTML = `<b>${data.Nombre || "?"}</b><br>Faltan ${
|
||||
falta || "?"
|
||||
} ${data.Unidad || "?"} <br><i style="font-size: 75%">${
|
||||
data.Ubicacion || "?"
|
||||
}</i>`;
|
||||
new_tr.className = PAGES["materiales"].navcss;
|
||||
new_tr.style.display = "inline-block";
|
||||
new_tr.style.margin = "5px";
|
||||
new_tr.style.padding = "5px";
|
||||
new_tr.style.borderRadius = "5px";
|
||||
new_tr.style.border = "2px solid black";
|
||||
new_tr.style.cursor = "pointer";
|
||||
new_tr.onclick = () => {
|
||||
setUrlHash("materiales," + data._key);
|
||||
};
|
||||
tablebody_EL.append(new_tr);
|
||||
}
|
||||
});
|
||||
}
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("materiales")
|
||||
.map()
|
||||
.on((data, key, _msg, _ev) => {
|
||||
EVENTLISTENER2 = _ev;
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
data["_key"] = key;
|
||||
materiales_low[key] = data;
|
||||
} else {
|
||||
delete materiales_low[key];
|
||||
}
|
||||
render_materialesLow();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
} else {
|
||||
add_row(data, key);
|
||||
}
|
||||
});
|
||||
function render_personasHigh() {
|
||||
function sorter(a, b) {
|
||||
if (a.Nombre < b.Nombre) {
|
||||
return -1;
|
||||
}
|
||||
if (a.Nombre > b.Nombre) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
if (loc) {
|
||||
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
|
||||
} else {
|
||||
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
|
||||
}
|
||||
var tablebody_EL = document.getElementById(table_personasHigh);
|
||||
tablebody_EL.innerHTML = "";
|
||||
Object.values(personas_high)
|
||||
.sort(sorter)
|
||||
.forEach((data) => {
|
||||
if (data.Puntos >= 10) {
|
||||
var new_tr = document.createElement("span");
|
||||
new_tr.innerHTML = `<img src="${
|
||||
data.Foto || ""
|
||||
}" alt="" height="55" style="float: left; margin-right: 5px;"><b>${
|
||||
data.Nombre || "?"
|
||||
}</b><br>Tiene ${
|
||||
data.Puntos || "?"
|
||||
} puntos <br><i style="font-size: 75%">${data.Region || "?"}</i>`;
|
||||
new_tr.className = PAGES["personas"].navcss;
|
||||
new_tr.style.display = "inline-block";
|
||||
new_tr.style.margin = "5px";
|
||||
new_tr.style.padding = "5px";
|
||||
new_tr.style.borderRadius = "5px";
|
||||
new_tr.style.border = "2px solid black";
|
||||
new_tr.style.cursor = "pointer";
|
||||
new_tr.style.width = "200px";
|
||||
|
||||
new_tr.onclick = () => {
|
||||
setUrlHash("personas," + data._key);
|
||||
};
|
||||
tablebody_EL.append(new_tr);
|
||||
}
|
||||
});
|
||||
}
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("personas")
|
||||
.map()
|
||||
.on((data, key, _msg, _ev) => {
|
||||
EVENTLISTENER = _ev;
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
data["_key"] = key;
|
||||
personas_high[key] = data;
|
||||
} else {
|
||||
delete personas_high[key];
|
||||
}
|
||||
render_personasHigh();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
} else {
|
||||
add_row(data, key);
|
||||
}
|
||||
});
|
||||
|
||||
// Comedor (.get("comedor").get(<current iso day>))
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("comedor")
|
||||
.get(CurrentISODate())
|
||||
.once((data, key) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Platos = data.Platos.replace(/\n/g, "<br>");
|
||||
// Display platos
|
||||
document.getElementById(table_comedor).innerHTML += data.Platos || "No hay platos registrados para hoy.";
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
add_row(data);
|
||||
});
|
||||
} else {
|
||||
add_row(data);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
//#endregion Cargar Clima
|
||||
//#region Cargar Comedor
|
||||
DB.get('comedor', CurrentISODate()).then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Platos = data.Platos || "No hay platos registrados para hoy.";
|
||||
// Display platos
|
||||
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'comedor', CurrentISODate());
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Comedor
|
||||
//#region Cargar Tareas
|
||||
DB.get('notas', 'tareas').then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Contenido = data.Contenido || "No hay tareas.";
|
||||
// Display platos
|
||||
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'notas', 'tareas');
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Tareas
|
||||
//#region Cargar Diario
|
||||
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
||||
function add_row(data) {
|
||||
// Fix newlines
|
||||
data.Contenido = data.Contenido || "No hay un diario.";
|
||||
// Display platos
|
||||
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
|
||||
/\n/g,
|
||||
"<br>"
|
||||
);
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
add_row(data || {});
|
||||
}, 'aulas_informes', 'diario-' + CurrentISODate());
|
||||
} else {
|
||||
add_row(data || {});
|
||||
}
|
||||
});
|
||||
//#endregion Cargar Diario
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,314 +1,346 @@
|
||||
PERMS["supercafe"] = "Cafetería";
|
||||
PERMS["supercafe:edit"] = "> Editar";
|
||||
PAGES.supercafe = {
|
||||
navcss: "btn5",
|
||||
Title: "SuperCafé",
|
||||
edit: function (mid) {
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_persona = safeuuid();
|
||||
var field_comanda = safeuuid();
|
||||
var field_notas = safeuuid();
|
||||
var field_estado = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
var btn_pagos = safeuuid();
|
||||
var btn_cocina = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_guardar2 = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Comanda <code id="${nameh1}"></code></h1>
|
||||
<button onclick="setUrlHash('supercafe');">Salir</button>
|
||||
<fieldset style="text-align: center;">
|
||||
<legend>Rellenar comanda</legend>
|
||||
<label style="display: none;">
|
||||
Fecha<br>
|
||||
<input readonly disabled type="text" id="${field_fecha}" value="${CurrentISODate()}"><br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Persona<br>
|
||||
<input type="hidden" id="${field_persona}">
|
||||
<br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Comanda (utiliza el panel de relleno)<br>
|
||||
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
|
||||
</label>
|
||||
<div id="${div_actions}" open>
|
||||
<!--<summary>Mostrar botones de relleno</summary>-->
|
||||
</div>
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Estado<br>
|
||||
<input readonly disabled type="text" id="${field_estado}" value="%%">
|
||||
<br>Modificar en el listado de comandas<br>
|
||||
</label>
|
||||
<button id=${btn_guardar} class="btn5">Guardar</button>
|
||||
<button id=${btn_borrar} class="rojo">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
var currentData = {};
|
||||
var currentPersonaID = "";
|
||||
var divact = document.getElementById(div_actions);
|
||||
navcss: "btn4",
|
||||
icon: "static/appico/cup.png",
|
||||
AccessControl: true,
|
||||
Title: "Cafetería",
|
||||
edit: function (mid) {
|
||||
if (!checkRole("supercafe:edit")) {
|
||||
setUrlHash("supercafe");
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_persona = safeuuid();
|
||||
var field_comanda = safeuuid();
|
||||
var field_notas = safeuuid();
|
||||
var field_estado = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
container.innerHTML = `
|
||||
<h1>Comanda <code id="${nameh1}"></code></h1>
|
||||
<button onclick="setUrlHash('supercafe');">Salir</button>
|
||||
<fieldset style="text-align: center;">
|
||||
<legend>Rellenar comanda</legend>
|
||||
<label style="display: none;">
|
||||
Fecha<br>
|
||||
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Persona<br>
|
||||
<input type="hidden" id="${field_persona}">
|
||||
<br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Comanda (utiliza el panel de relleno)<br>
|
||||
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
|
||||
</label>
|
||||
<div id="${div_actions}" open>
|
||||
<!--<summary>Mostrar botones de relleno</summary>-->
|
||||
</div>
|
||||
<label>
|
||||
Notas<br>
|
||||
<textarea id="${field_notas}"></textarea><br><br>
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
Estado<br>
|
||||
<input readonly disabled type="text" id="${field_estado}" value="%%">
|
||||
<br>Modificar en el listado de comandas<br>
|
||||
</label>
|
||||
<button id=${btn_guardar} class="btn5">Guardar</button>
|
||||
<button id=${btn_borrar} class="rojo">Borrar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
var currentData = {};
|
||||
var currentPersonaID = "";
|
||||
var divact = document.getElementById(div_actions);
|
||||
|
||||
function loadActions() {
|
||||
divact.innerHTML = "";
|
||||
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
|
||||
document.getElementById(field_persona).value = value;
|
||||
});
|
||||
Object.entries(SC_actions).forEach((category) => {
|
||||
addCategory(
|
||||
divact,
|
||||
category[0],
|
||||
SC_actions_icons[category[0]],
|
||||
category[1],
|
||||
currentData,
|
||||
(values) => {
|
||||
document.getElementById(field_comanda).value = SC_parse(values);
|
||||
}
|
||||
);
|
||||
});
|
||||
function loadActions() {
|
||||
divact.innerHTML = "";
|
||||
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
|
||||
document.getElementById(field_persona).value = value;
|
||||
});
|
||||
Object.entries(SC_actions).forEach((category) => {
|
||||
addCategory(
|
||||
divact,
|
||||
category[0],
|
||||
SC_actions_icons[category[0]],
|
||||
category[1],
|
||||
currentData,
|
||||
(values) => {
|
||||
document.getElementById(field_comanda).value = SC_parse(values);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
loadActions();
|
||||
DB.get('supercafe', mid).then((data) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate();
|
||||
document.getElementById(field_persona).value = data["Persona"] || "";
|
||||
currentPersonaID = data["Persona"] || "";
|
||||
document.getElementById(field_comanda).value =
|
||||
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
|
||||
document.getElementById(field_notas).value = data["Notas"] || "";
|
||||
document.getElementById(field_estado).value = data["Estado"] || "%%";
|
||||
currentData = JSON.parse(data["Comanda"] || "{}");
|
||||
|
||||
loadActions();
|
||||
}
|
||||
loadActions();
|
||||
gun
|
||||
.get(TABLE)
|
||||
.get("supercafe")
|
||||
.get(mid)
|
||||
.once((data, key) => {
|
||||
function load_data(data, ENC = "") {
|
||||
document.getElementById(nameh1).innerText = key;
|
||||
document.getElementById(field_fecha).value = data["Fecha"];
|
||||
document.getElementById(field_persona).value =
|
||||
data["Persona"] || "";
|
||||
currentPersonaID = data["Persona"] || "";
|
||||
document.getElementById(field_comanda).value =
|
||||
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
|
||||
document.getElementById(field_notas).value = data["Notas"] || "";
|
||||
document.getElementById(field_estado).value = data["Estado"] || "";
|
||||
currentData = JSON.parse(data["Comanda"] || "{}");
|
||||
|
||||
loadActions();
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
SEA.decrypt(data, SECRET, (data) => {
|
||||
load_data(data, "%E");
|
||||
});
|
||||
} else {
|
||||
load_data(data);
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
if (document.getElementById(field_persona).value == "") {
|
||||
alert("¡Hay que elegir una persona!");
|
||||
return;
|
||||
}
|
||||
var data = {
|
||||
Fecha: document.getElementById(field_fecha).value,
|
||||
Persona: document.getElementById(field_persona).value,
|
||||
Comanda: JSON.stringify(currentData),
|
||||
Notas: document.getElementById(field_notas).value,
|
||||
Estado: document
|
||||
.getElementById(field_estado)
|
||||
.value.replace("%%", "Pedido"),
|
||||
};
|
||||
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
betterGunPut(gun.get(TABLE).get("supercafe").get(mid), encrypted);
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("supercafe");
|
||||
}, 1500);
|
||||
});
|
||||
if (typeof data == "string") {
|
||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
||||
load_data(data, "%E");
|
||||
}, 'supercafe', mid);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Check if button is already disabled to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
// Validate before disabling button
|
||||
if (document.getElementById(field_persona).value == "") {
|
||||
alert("¡Hay que elegir una persona!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button after validation passes
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = "0.5";
|
||||
|
||||
var data = {
|
||||
Fecha: document.getElementById(field_fecha).value,
|
||||
Persona: document.getElementById(field_persona).value,
|
||||
Comanda: JSON.stringify(currentData),
|
||||
Notas: document.getElementById(field_notas).value,
|
||||
Estado: document
|
||||
.getElementById(field_estado)
|
||||
.value.replace("%%", "Pedido"),
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (
|
||||
confirm(
|
||||
"¿Quieres borrar esta comanda? - NO se actualizaran los puntos de la persona asignada."
|
||||
) == true
|
||||
) {
|
||||
betterGunPut(gun.get(TABLE).get("supercafe").get(mid), null);
|
||||
document.getElementById("actionStatus").style.display = "block";
|
||||
DB.put('supercafe', mid, data).then(() => {
|
||||
toastr.success("Guardado!");
|
||||
setTimeout(() => {
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
setUrlHash("supercafe");
|
||||
}, SAVE_WAIT);
|
||||
}).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = "1";
|
||||
document.getElementById("actionStatus").style.display = "none";
|
||||
toastr.error("Error al guardar la comanda");
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (
|
||||
confirm(
|
||||
"¿Quieres borrar esta comanda? - NO se actualizará el monedero de la persona asignada."
|
||||
) == true
|
||||
) {
|
||||
DB.del('supercafe', mid).then(() => {
|
||||
setTimeout(() => {
|
||||
setUrlHash("supercafe");
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
var tts = false;
|
||||
var sc_nobtn = "";
|
||||
if (urlParams.get("sc_nobtn") == "yes") {
|
||||
sc_nobtn = "pointer-events: none; opacity: 0.5";
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
tts = true;
|
||||
console.log("TTS Enabled");
|
||||
toastr.info("Texto a voz disponible");
|
||||
}, 6500);
|
||||
const tablebody = safeuuid();
|
||||
const tablebody2 = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
var tts_check = safeuuid();
|
||||
var old = {};
|
||||
container.innerHTML = `
|
||||
<h1>SuperCafé</h1>
|
||||
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
|
||||
<br>
|
||||
<label>
|
||||
<b>Habilitar avisos:</b>
|
||||
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;">
|
||||
</label>
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole("supercafe")) {
|
||||
setUrlHash("index");
|
||||
return;
|
||||
}
|
||||
var tts = false;
|
||||
var sc_nobtn = "";
|
||||
if (urlParams.get("sc_nobtn") == "yes") {
|
||||
sc_nobtn = "pointer-events: none; opacity: 0.5";
|
||||
}
|
||||
var ev = setTimeout(() => {
|
||||
tts = true;
|
||||
console.log("TTS Enabled");
|
||||
toastr.info("Texto a voz disponible");
|
||||
}, 6500);
|
||||
EventListeners.Timeout.push(ev);
|
||||
const tablebody = safeuuid();
|
||||
const tablebody2 = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
var totalprecio = safeuuid();
|
||||
var tts_check = safeuuid();
|
||||
var old = {};
|
||||
container.innerHTML = `
|
||||
<h1>Cafetería - Total: <span id="${totalprecio}">0</span>c</h1>
|
||||
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
|
||||
<br>
|
||||
<label>
|
||||
<b>Habilitar avisos:</b>
|
||||
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;">
|
||||
</label>
|
||||
|
||||
<details style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black" open>
|
||||
<summary>Todas las comandas</summary>
|
||||
<div id="cont1"></div>
|
||||
</details>
|
||||
<br>
|
||||
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
|
||||
<summary>Deudas</summary>
|
||||
<div id="cont2"></div>
|
||||
</details>
|
||||
`;
|
||||
|
||||
//Todas las comandas
|
||||
TS_IndexElement(
|
||||
"supercafe",
|
||||
[
|
||||
{
|
||||
key: "Persona",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Persona",
|
||||
},
|
||||
{
|
||||
key: "Estado",
|
||||
type: "comanda-status",
|
||||
default: "",
|
||||
label: "Estado",
|
||||
},
|
||||
{
|
||||
key: "Comanda",
|
||||
type: "comanda",
|
||||
default: "",
|
||||
label: "Comanda",
|
||||
},
|
||||
],
|
||||
gun.get(TABLE).get("supercafe"),
|
||||
document.querySelector("#cont1"),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
|
||||
if (data.Estado == "Pedido") {
|
||||
new_tr.style.backgroundColor = "#FFFFFF";
|
||||
}
|
||||
if (data.Estado == "En preparación") {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
}
|
||||
if (data.Estado == "Listo") {
|
||||
new_tr.style.backgroundColor = "gold";
|
||||
}
|
||||
if (data.Estado == "Entregado") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
if (data.Estado == "Deuda") {
|
||||
new_tr.style.backgroundColor = "#f5d3ff";
|
||||
}
|
||||
<details style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black" open>
|
||||
<summary>Todas las comandas</summary>
|
||||
<div id="cont1"></div>
|
||||
</details>
|
||||
<br>
|
||||
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
|
||||
<summary>Deudas</summary>
|
||||
<div id="cont2"></div>
|
||||
</details>
|
||||
`;
|
||||
var config = [
|
||||
{
|
||||
key: "Persona",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Persona",
|
||||
},
|
||||
{
|
||||
key: "Estado",
|
||||
type: "comanda-status",
|
||||
default: "",
|
||||
label: "Estado",
|
||||
},
|
||||
{
|
||||
key: "Comanda",
|
||||
type: "comanda",
|
||||
default: "",
|
||||
label: "Comanda",
|
||||
},
|
||||
];
|
||||
if (!checkRole("supercafe:edit")) {
|
||||
config = [
|
||||
{
|
||||
key: "Persona",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Persona",
|
||||
},
|
||||
(data) => {
|
||||
if (data.Estado == "Deuda") {
|
||||
return true;
|
||||
}
|
||||
var key = data._key;
|
||||
if (old[key] == undefined) {
|
||||
old[key] = "";
|
||||
}
|
||||
if (old[key] != data.Estado) {
|
||||
if (tts && document.getElementById(tts_check).checked) {
|
||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
||||
JSON.parse(data.Comanda)["Selección"]
|
||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
||||
console.log("TTS: " + msg);
|
||||
let utterance = new SpeechSynthesisUtterance(msg);
|
||||
utterance.rate = 0.9;
|
||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
old[key] = data.Estado;
|
||||
}
|
||||
);
|
||||
|
||||
//Deudas
|
||||
TS_IndexElement(
|
||||
"supercafe",
|
||||
[
|
||||
{
|
||||
key: "Persona",
|
||||
type: "persona",
|
||||
default: "",
|
||||
label: "Persona",
|
||||
},
|
||||
{
|
||||
key: "Estado",
|
||||
type: "comanda-status",
|
||||
default: "",
|
||||
label: "Estado",
|
||||
},
|
||||
{
|
||||
key: "Comanda",
|
||||
type: "comanda",
|
||||
default: "",
|
||||
label: "Comanda",
|
||||
},
|
||||
],
|
||||
gun.get(TABLE).get("supercafe"),
|
||||
document.querySelector("#cont2"),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
|
||||
if (data.Estado == "Pedido") {
|
||||
new_tr.style.backgroundColor = "#FFFFFF";
|
||||
}
|
||||
if (data.Estado == "En preparación") {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
}
|
||||
if (data.Estado == "Listo") {
|
||||
new_tr.style.backgroundColor = "gold";
|
||||
}
|
||||
if (data.Estado == "Entregado") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
if (data.Estado == "Deuda") {
|
||||
new_tr.style.backgroundColor = "#f5d3ff";
|
||||
}
|
||||
{
|
||||
key: "Comanda",
|
||||
type: "comanda",
|
||||
default: "",
|
||||
label: "Comanda",
|
||||
},
|
||||
(data) => {
|
||||
if (data.Estado != "Deuda") {
|
||||
return true;
|
||||
}
|
||||
var key = data._key;
|
||||
if (old[key] == undefined) {
|
||||
old[key] = "";
|
||||
}
|
||||
if (old[key] != data.Estado) {
|
||||
if (tts && document.getElementById(tts_check).checked) {
|
||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
||||
JSON.parse(data.Comanda)["Selección"]
|
||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
||||
console.log("TTS: " + msg);
|
||||
let utterance = new SpeechSynthesisUtterance(msg);
|
||||
utterance.rate = 0.9;
|
||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
old[key] = data.Estado;
|
||||
];
|
||||
}
|
||||
//Todas las comandas
|
||||
var comandasTot = {};
|
||||
function calcPrecio() {
|
||||
var tot = 0;
|
||||
Object.values(comandasTot).forEach((precio) => {
|
||||
tot += precio;
|
||||
});
|
||||
document.getElementById(totalprecio).innerText = tot;
|
||||
return tot;
|
||||
}
|
||||
TS_IndexElement(
|
||||
"supercafe",
|
||||
config,
|
||||
"supercafe",
|
||||
document.querySelector("#cont1"),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
comandasTot[data._key] = SC_priceCalc(JSON.parse(data.Comanda))[0];
|
||||
calcPrecio();
|
||||
if (data.Estado == "Pedido") {
|
||||
new_tr.style.backgroundColor = "#FFFFFF";
|
||||
}
|
||||
);
|
||||
if (data.Estado == "En preparación") {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
}
|
||||
if (data.Estado == "Listo") {
|
||||
new_tr.style.backgroundColor = "gold";
|
||||
}
|
||||
if (data.Estado == "Entregado") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
if (data.Estado == "Deuda") {
|
||||
new_tr.style.backgroundColor = "#f5d3ff";
|
||||
}
|
||||
},
|
||||
(data) => {
|
||||
if (data.Estado == "Deuda") {
|
||||
return true;
|
||||
}
|
||||
var key = data._key;
|
||||
if (old[key] == undefined) {
|
||||
old[key] = "";
|
||||
}
|
||||
if (old[key] != data.Estado) {
|
||||
if (tts && document.getElementById(tts_check).checked) {
|
||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
||||
JSON.parse(data.Comanda)["Selección"]
|
||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
||||
let utterance = new SpeechSynthesisUtterance(msg);
|
||||
utterance.rate = 0.9;
|
||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
old[key] = data.Estado;
|
||||
}
|
||||
);
|
||||
|
||||
//Deudas
|
||||
TS_IndexElement(
|
||||
"supercafe",
|
||||
config,
|
||||
"supercafe",
|
||||
document.querySelector("#cont2"),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
comandasTot[data._key] = 0; // No mostrar comandas en deuda.
|
||||
calcPrecio();
|
||||
|
||||
if (data.Estado == "Pedido") {
|
||||
new_tr.style.backgroundColor = "#FFFFFF";
|
||||
}
|
||||
if (data.Estado == "En preparación") {
|
||||
new_tr.style.backgroundColor = "#FFCCCB";
|
||||
}
|
||||
if (data.Estado == "Listo") {
|
||||
new_tr.style.backgroundColor = "gold";
|
||||
}
|
||||
if (data.Estado == "Entregado") {
|
||||
new_tr.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
if (data.Estado == "Deuda") {
|
||||
new_tr.style.backgroundColor = "#f5d3ff";
|
||||
}
|
||||
},
|
||||
(data) => {
|
||||
if (data.Estado != "Deuda") {
|
||||
return true;
|
||||
}
|
||||
var key = data._key;
|
||||
if (old[key] == undefined) {
|
||||
old[key] = "";
|
||||
}
|
||||
if (old[key] != data.Estado) {
|
||||
if (tts && document.getElementById(tts_check).checked) {
|
||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
||||
JSON.parse(data.Comanda)["Selección"]
|
||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
||||
let utterance = new SpeechSynthesisUtterance(msg);
|
||||
utterance.rate = 0.9;
|
||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
old[key] = data.Estado;
|
||||
}
|
||||
);
|
||||
if (!checkRole("supercafe:edit")) {
|
||||
document.getElementById(btn_new).style.display = "none";
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash("supercafe," + safeuuid(""));
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ document.getElementById("reload").addEventListener("click", function () {
|
||||
});
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js").then((reg) => {
|
||||
navigator.serviceWorker.register("sw.js").then((reg) => {
|
||||
reg.addEventListener("updatefound", () => {
|
||||
// A wild service worker has appeared in reg.installing!
|
||||
newWorker = reg.installing;
|
||||
|
||||
43
src/sw.js
@@ -1,27 +1,26 @@
|
||||
var cacheName = 'telesec_2025-07-30_4';
|
||||
var CACHE = "telesec_%%VERSIONCO%%";
|
||||
importScripts(
|
||||
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
|
||||
);
|
||||
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(cacheName)
|
||||
.then(cache => cache.addAll(["index.html", "icon512_maskable.png", "icon512_rounded.png", "cola_cao.jpg", "manifest.json", "static/webrtc.js", "static/synchronous.js", "static/sea.js", "static/gun.js", "static/toastr.min.css", "static/store.js", "static/simplemde.min.css", "static/doublescroll.js", "static/rindexed.js", "static/yson.js", "static/toastr.min.js", "static/showdown.min.js", "static/load.js", "static/radix.js", "static/axe.js", "static/TeleSec.jpg", "static/path.js", "static/radisk.js", "static/open.js", "static/simplemde.min.js", "static/jquery.js", "static/euskaditech-css/README.md", "static/euskaditech-css/.gitignore", "static/euskaditech-css/simple.css", "static/euskaditech-css/.git", "static/euskaditech-css/logos/EuskadiTech/long nobg color.svg", "static/euskaditech-css/logos/EuskadiTech/nobg color.png", "static/euskaditech-css/logos/EuskadiTech/nobg color.svg", "static/euskaditech-css/logos/EuskadiTech/long nobg color.png", "static/ico/add.png", "static/ico/user_generic.png", "static/ico/keyboard_key_g.png", "static/ico/keyboard_key_p.png", "static/ico/snowflake.png", "static/ico/coffee_bean.png", "static/ico/arrow_up_red.png", "static/ico/milk (1).png", "static/ico/azucar-moreno.png", "static/ico/arrow_down_blue.png", "static/ico/camera2.png", "static/ico/fire.png", "static/ico/cookies.png", "static/ico/checkbox_unchecked.png", "static/ico/wheat.png", "static/ico/sacarina.jpg", "static/ico/arrow_left_green.png", "static/ico/tea_bag.png", "static/ico/cow.png", "static/ico/connect_ko.svg", "static/ico/milk.png", "static/ico/user.png", "static/ico/stevia.jpg", "static/ico/water_tap.png", "static/ico/thermometer2.png", "static/ico/statusok.png", "static/ico/lollipop.png", "static/ico/colacao.jpg", "static/ico/delete.png", "static/ico/cereales.png", "static/ico/checkbox.png", "static/ico/azucar-blanco.jpg", "static/ico/preferences.png", "static/ico/sizes.png", "static/ico/stevia-gotas.webp", "static/ico/connect_ok.svg", "static/ico/layered1/Azucar-Az. Moreno.png", "static/ico/layered1/Azucar-Stevia (Pastillas).png", "static/ico/layered1/Azucar-Sacarina.png", "static/ico/layered1/Selección-ColaCao.png", "static/ico/layered1/Temperatura-Templado.png", "static/ico/layered1/Tamaño-Pequeño.png", "static/ico/layered1/Leche-Sin lactosa.png", "static/ico/layered1/Cafeina-Sin.png", "static/ico/layered1/Leche-Vegetal.png", "static/ico/layered1/Leche-de Vaca.png", "static/ico/layered1/Selección-Infusion.png", "static/ico/layered1/Azucar-Sin.png", "static/ico/layered1/Selección-Leche.png", "static/ico/layered1/Temperatura-Frio.png", "static/ico/layered1/Background.png", "static/ico/layered1/Azucar-Edulcorante.png", "static/ico/layered1/Cafeina-Con.png", "static/ico/layered1/Selección-CaféLeche.png", "static/ico/layered1/Tamaño-Grande.png", "static/ico/layered1/Selección-CafeSolo.png", "static/ico/layered1/Leche-Agua.png", "static/ico/layered1/Temperatura-Caliente.png", "static/ico/layered1/Azucar-Stevia (Gotas).png", "static/ico/layered1/Azucar-Az. Blanco.png"]))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', function (event) {
|
||||
if (event.data.action === 'skipWaiting') {
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(function (response) {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
// workbox.routing.registerRoute(
|
||||
// new RegExp("/*"),
|
||||
// new workbox.strategies.StaleWhileRevalidate({
|
||||
// cacheName: CACHE,
|
||||
// })
|
||||
// );
|
||||
|
||||
// All but couchdb
|
||||
workbox.routing.registerRoute(
|
||||
({ url }) =>
|
||||
!url.pathname.startsWith("/_couchdb/") && url.origin === self.location.origin,
|
||||
new workbox.strategies.NetworkFirst({
|
||||
cacheName: CACHE,
|
||||
})
|
||||
);
|
||||