Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:06 +08:00
commit 5e3ca965d9
68 changed files with 11257 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# Odoo Instance Configuration
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
# Primary Model (use x_ prefix for Odoo Studio models)
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
# Optional: For static hosting (GitHub Pages, Cloudflare Pages, etc.)
# Set this to the full URL of your API server if frontend and backend are on different domains
PUBLIC_API_URL=

View File

@@ -0,0 +1,15 @@
node_modules/
.env
.env.local
dist/
build/
.svelte-kit/
.vercel/
.netlify/
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": false,
"moduleResolution": "bundler"
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "{{PROJECT_NAME}}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@vite-pwa/sveltekit": "^1.0.1",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"typescript": "^5.9.2",
"vite": "^7.1.7"
},
"dependencies": {
"vite-plugin-pwa": "^1.1.0"
}
}

View File

@@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true
}),
// Configure base path for GitHub Pages deployment
paths: {
base: process.argv.includes('dev') ? '' : process.env.PUBLIC_BASE_PATH || ''
}
}
};
export default config;

View File

@@ -0,0 +1,40 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
sveltekit(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
},
manifest: {
id: '/',
name: '{{PROJECT_NAME}}',
short_name: '{{PROJECT_NAME}}',
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#667eea',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]
}
})
]
});

View File

@@ -0,0 +1,48 @@
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run build
env:
PUBLIC_BASE_PATH: /${{ github.event.repository.name }}
ODOO_URL: ${{ secrets.ODOO_URL }}
ODOO_DB: ${{ secrets.ODOO_DB }}
ODOO_USERNAME: ${{ secrets.ODOO_USERNAME }}
ODOO_API_KEY: ${{ secrets.ODOO_API_KEY }}
ODOO_PRIMARY_MODEL: ${{ secrets.ODOO_PRIMARY_MODEL }}
- uses: actions/upload-pages-artifact@v2
with:
path: build
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View File

@@ -0,0 +1,6 @@
{
"buildCommand": "npm run build",
"outputDirectory": "build",
"framework": "sveltekit",
"regions": ["iad1"]
}

View File

@@ -0,0 +1,243 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this {{PROJECT_NAME}} codebase.
## Project Overview
**{{PROJECT_NAME}}** - An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management, built with SvelteKit frontend and Odoo Studio backend.
## Development Commands
### Development
```bash
npm run dev # Start dev server at http://localhost:5173
```
### Building
```bash
npm run build # Production build (outputs to /build)
npm run preview # Preview production build locally
```
### Type Checking
```bash
npm run check # Run svelte-check for type errors
npm run check:watch # Watch mode for type checking
```
## Environment Setup
Required environment variables in `.env`:
```env
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
```
**Important**: The app uses API keys (not passwords) for Odoo authentication. These are server-side only and never exposed to the client.
## Architecture
### Three-Layer Data Flow
```
Frontend Component (Svelte)
Cache Store (Svelte Store)
Odoo API Client (src/lib/odoo.js)
Server Route (src/routes/api/odoo/+server.js)
Odoo JSON-RPC Backend
```
Data also persists to localStorage for offline access.
### Key Architectural Patterns
#### 1. **Smart Caching Layer** (`src/lib/stores/cache.js`)
The centerpiece of the frontend architecture. This Svelte store provides:
- **Immediate data availability**: Shows cached data from localStorage instantly on page load
- **Background sync**: Automatically syncs with Odoo every 3 minutes if cache is stale (>5 minutes old)
- **Incremental fetching**: Only fetches records with `id > lastRecordId` to minimize API calls
- **Optimistic updates**: UI updates immediately, syncs to server in background
**Key functions**:
- `initialize()` - Call in `onMount()`, loads cache and starts background sync
- `sync()` - Incremental sync with Odoo
- `forceRefresh()` - Clears cache and does full sync
- `createRecord()`, `updateRecord()`, `deleteRecord()` - CRUD operations with optimistic updates
#### 2. **Server-Side API Proxy** (`src/routes/api/odoo/+server.js`)
SvelteKit server route that acts as a JSON-RPC proxy to Odoo. This pattern:
- Keeps credentials server-side (never exposed to client)
- Caches Odoo UID to reduce authentication calls
- Provides a simple `{ action, data }` interface for the frontend
**Supported actions**: `create`, `search`, `search_model`, `update`, `delete`
#### 3. **Odoo Field Formatting**
Odoo uses specific formats for relational fields:
- **Many2One (single relation)**: Send as integer ID, receive as `[id, "display_name"]` tuple
- **Many2Many (multiple relations)**: Send as `[[6, 0, [id1, id2, ...]]]`, receive as array of tuples
Helper functions in `src/lib/odoo.js`:
- `formatMany2one(id)` - Converts ID to integer or `false`
- `formatMany2many(ids)` - Wraps IDs in Odoo command format `[[6, 0, [...]]]`
#### 4. **Offline-First Strategy**
Two-phase data loading:
1. **Immediate Load**: Show cached data from localStorage
2. **Background Sync**: Fetch new data if cache is stale
```javascript
// Phase 1: Load from cache immediately
const cachedData = loadFromStorage();
updateUI(cachedData);
// Phase 2: Background sync if stale
if (cachedData.isStale) {
syncInBackground();
}
```
### SvelteKit Configuration
- **Rendering**: `ssr: false`, `csr: true`, `prerender: true` in `src/routes/+layout.js`
- **Adapter**: `adapter-static` configured for static output to `/build` directory
- **Base path**: Configurable via `PUBLIC_BASE_PATH` env var (for GitHub Pages deployment)
### PWA Features
Configured in `vite.config.js`:
- **Service Worker**: Auto-generated with Workbox, caches all static assets
- **Auto-update**: New versions automatically activate
- **Manifest**: Configured for standalone mode, installable on mobile
- **Icons**: 192x192 and 512x512 PNG icons in `/static`
## Odoo Model Structure
### Main Model: `x_{{MODEL_NAME}}`
| Field | Type | Purpose |
|-------|------|---------|
| `x_name` | Char | Record name |
| Add your custom fields with x_studio_ prefix |
## Important Development Notes
### Working with the Cache
When modifying record-related features:
1. **Always call `{{MODEL_NAME}}Cache.sync()` after mutations** (create/update/delete)
2. **Initialize the cache in page components**: `onMount(() => {{MODEL_NAME}}Cache.initialize())`
3. **Clean up on unmount**: `onDestroy(() => {{MODEL_NAME}}Cache.destroy())`
4. **Cache invalidation**: Increment cache version in `cache.js` if store schema changes
### Odoo API Patterns
When adding new Odoo operations:
1. **Frontend**: Add method to `src/lib/odoo.js` (calls `/api/odoo` endpoint)
2. **Backend**: Add new action case in `src/routes/api/odoo/+server.js`
3. **Use `execute()` helper** for model operations (wraps authentication)
Example:
```javascript
// Frontend (odoo.js)
async customOperation(id) {
return this.callApi('custom', { id });
}
// Backend (+server.js)
case 'custom':
const result = await execute('x_{{MODEL_NAME}}', 'write', [[data.id], { custom_field: true }]);
return json({ success: true, result });
```
### PWA Manifest Updates
When changing app name, icons, or theme:
1. Update `vite.config.js` manifest section
2. Update `/static/manifest.json`
3. Replace `/static/icon-192.png` and `/static/icon-512.png`
4. Run `npm run build` to regenerate service worker
### Deployment
**Vercel** (primary):
- Automatically deploys from `main` branch
- Set environment variables in Vercel dashboard
- Outputs to `/build` directory (configured in `vercel.json`)
**GitHub Pages**:
- Set `PUBLIC_BASE_PATH=/repo-name` in GitHub Actions secrets
- Configure GitHub Pages source as "GitHub Actions"
- Workflow auto-deploys on push to `main`
## File Structure Reference
```
src/
├── lib/
│ ├── stores/
│ │ └── cache.js # Core caching & sync logic
│ ├── odoo.js # Frontend API client
│ ├── db.js # IndexedDB manager
│ └── utils.js # Utility functions
├── routes/
│ ├── +layout.js # Root layout config (SSR/CSR settings)
│ ├── +layout.svelte # Root layout component
│ ├── +page.svelte # Add record form
│ ├── list/+page.svelte # List all records
│ └── api/odoo/+server.js # Odoo JSON-RPC proxy endpoint
└── app.html # HTML template with PWA meta tags
```
## Common Gotchas
1. **Odoo field naming**: All custom fields use `x_studio_` prefix (Odoo Studio convention)
2. **Partner name format**: Odoo returns `[id, "name"]` tuples, must extract display name for UI
3. **localStorage limits**: Browser typically allows 5-10MB, sufficient for hundreds of records
4. **Service worker caching**: After PWA updates, users may need to close all tabs and reopen
5. **Base path in production**: GitHub Pages deployments require `PUBLIC_BASE_PATH` env var set
6. **API authentication**: Use API keys, not passwords. Generate in Odoo user settings.
## Adding New Features
### Add a New Field
1. Add field in Odoo Studio with `x_studio_` prefix
2. Update `fields` array in `src/lib/stores/cache.js`
3. Add form input in `src/routes/+page.svelte`
4. Display field in `src/routes/list/+page.svelte`
### Add a New Page
1. Create `src/routes/new-page/+page.svelte`
2. Add navigation link in layout or other pages
3. Import and use cache store if accessing data
### Add Partner/Relation Field
1. Add Many2one or Many2many field in Odoo
2. Use `odooClient.formatMany2one()` or `formatMany2many()` when saving
3. Use `odooClient.fetchPartners()` to load options
4. Display resolved names in UI
---
**Generated with Odoo PWA Generator**

View File

@@ -0,0 +1,225 @@
# {{PROJECT_NAME}}
An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management with Odoo Studio backend integration.
## ✨ Features
- 📱 **Progressive Web App** - Installable on mobile and desktop
- 🔌 **Offline-first** - Works without internet connection
- 🔄 **Auto-sync** - Background synchronization with Odoo
- 💾 **Smart caching** - localStorage + IndexedDB for optimal performance
- 🎨 **Responsive UI** - Works on all devices
- ⚡ **Fast** - Instant UI updates with optimistic rendering
## 🚀 Quick Start
### Prerequisites
- Node.js 18+ installed
- Odoo Studio account with API access
- Custom Odoo model created: `x_{{MODEL_NAME}}`
### Installation
1. Clone this repository:
```bash
git clone <repository-url>
cd {{PROJECT_NAME}}
```
2. Install dependencies:
```bash
npm install
```
3. Configure environment:
```bash
cp .env.example .env
```
4. Edit `.env` with your Odoo credentials:
```env
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database-name
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
```
5. Start development server:
```bash
npm run dev
```
6. Open [http://localhost:5173](http://localhost:5173)
## 🔧 Odoo Studio Setup
### Create Custom Model
1. Log into your Odoo instance
2. Navigate to **Studio**
3. Create a new model: `x_{{MODEL_NAME}}`
4. Add these recommended fields:
- `x_name` (Char) - Required - Record name
- `x_studio_description` (Text) - Description
- `x_studio_date` (Date) - Date field
- `x_studio_value` (Float) - Numeric value
- Add more custom fields as needed with `x_studio_` prefix
### Get API Credentials
1. Go to **Settings** → **Users & Companies** → **Users**
2. Select your user
3. Click **Generate API Key**
4. Copy the key and add to `.env` file
## 📦 Build & Deployment
### Build for Production
```bash
npm run build
```
### Deploy to Vercel
```bash
npm install -g vercel
vercel
```
Set environment variables in Vercel dashboard:
- `ODOO_URL`
- `ODOO_DB`
- `ODOO_USERNAME`
- `ODOO_API_KEY`
- `ODOO_PRIMARY_MODEL`
### Deploy to GitHub Pages
1. Add secrets to GitHub repository:
- `ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`, `ODOO_PRIMARY_MODEL`
2. Enable GitHub Pages in repository settings:
- Source: GitHub Actions
3. Push to main branch - auto-deploys via GitHub Actions
### Deploy to Cloudflare Pages
```bash
npm run build
wrangler pages publish build
```
## 🏗️ Architecture
This app follows a three-layer architecture:
```
Frontend Component
Cache Store (Svelte Store)
Odoo API Client (Frontend)
Server Route (/api/odoo)
Odoo JSON-RPC
```
### Key Features:
- **Smart Caching**: Loads from cache instantly, syncs in background
- **Incremental Sync**: Only fetches new records since last sync
- **Offline Queue**: Saves changes locally when offline, syncs when online
- **PWA**: Service worker caches all assets for offline use
See `CLAUDE.md` for detailed architecture documentation.
## 🛠️ Development
### Available Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview production build
- `npm run check` - Type check
### Project Structure
```
{{PROJECT_NAME}}/
├── src/
│ ├── lib/
│ │ ├── odoo.js # Odoo API client
│ │ ├── db.js # IndexedDB manager
│ │ ├── utils.js # Utility functions
│ │ └── stores/
│ │ └── cache.js # Smart cache store
│ ├── routes/
│ │ ├── +layout.svelte # Root layout
│ │ ├── +page.svelte # Add record form
│ │ ├── list/+page.svelte # List all records
│ │ └── api/odoo/+server.js # Server API proxy
│ └── app.html # HTML template
├── static/
│ ├── manifest.json # PWA manifest
│ ├── icon-192.png # App icon 192x192
│ └── icon-512.png # App icon 512x512
├── .env # Environment variables (gitignored)
├── .env.example # Environment template
├── svelte.config.js # SvelteKit configuration
├── vite.config.js # Vite + PWA configuration
└── package.json # Dependencies
```
## 📝 Adding Fields
To add custom fields to your model:
1. Add field in Odoo Studio (use `x_studio_` prefix)
2. Update `src/lib/stores/cache.js` - add field to `fields` array
3. Update `src/routes/+page.svelte` - add form input
4. Update `src/routes/list/+page.svelte` - display field
## 🐛 Troubleshooting
### "Authentication failed"
- Verify `ODOO_API_KEY` is correct
- Check `ODOO_USERNAME` matches your Odoo user
- Ensure API access is enabled in Odoo
### "Model not found"
- Verify model name uses `x_` prefix: `x_{{MODEL_NAME}}`
- Check model exists in Odoo Studio
- Ensure model is published
### Offline mode not working
- Check service worker is registered (DevTools → Application)
- Verify PWA manifest is loaded
- Clear browser cache and reload
### Data not syncing
- Check browser console for errors
- Verify internet connection
- Check Odoo API is accessible
## 📚 Learn More
- [SvelteKit Documentation](https://kit.svelte.dev/)
- [Odoo API Documentation](https://www.odoo.com/documentation/)
- [PWA Guide](https://web.dev/progressive-web-apps/)
## 📄 License
MIT
## 🤝 Contributing
Contributions welcome! Please open an issue or PR.
---
**Generated with [Odoo PWA Generator](https://github.com/jamshid/odoo-pwa-generator)** 🚀

View File

@@ -0,0 +1,134 @@
import { json } from '@sveltejs/kit';
import {
ODOO_URL,
ODOO_DB,
ODOO_USERNAME,
ODOO_API_KEY
} from '$env/static/private';
/** @type {number|null} */
let cachedUid = null;
/**
* Make JSON-RPC call to Odoo
* @param {string} service
* @param {string} method
* @param {any[]} args
*/
async function callOdoo(service, method, args) {
const response = await fetch(`${ODOO_URL}/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
service: service,
method: method,
args: args
},
id: Math.floor(Math.random() * 1000000)
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.data?.message || data.error.message || 'Odoo API Error');
}
return data.result;
}
/**
* Authenticate with Odoo and get UID
*/
async function authenticate() {
if (cachedUid) return cachedUid;
const authMethod = ODOO_API_KEY;
const uid = await callOdoo('common', 'login', [ODOO_DB, ODOO_USERNAME, authMethod]);
if (!uid) {
throw new Error('Authentication failed');
}
cachedUid = uid;
return uid;
}
/**
* Execute a method on Odoo model
* @param {string} model
* @param {string} method
* @param {any[]} args
* @param {Record<string, any>} kwargs
*/
async function execute(model, method, args = [], kwargs = {}) {
const uid = await authenticate();
const authMethod = ODOO_API_KEY;
return await callOdoo('object', 'execute_kw', [
ODOO_DB,
uid,
authMethod,
model,
method,
args,
kwargs
]);
}
/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
try {
const { action, data } = await request.json();
switch (action) {
case 'create': {
const { model, fields } = data;
const id = await execute(model, 'create', [fields]);
return json({ success: true, id });
}
case 'search': {
const { model, domain = [], fields = [] } = data;
const results = await execute(model, 'search_read', [domain], { fields });
return json({ success: true, results });
}
// Search any model (used by frontend to load res.partner list, etc.)
case 'search_model': {
const { model, domain = [], fields = [] } = data;
const results = await execute(model, 'search_read', [domain], { fields });
return json({ success: true, results });
}
case 'update': {
const { model, id, values } = data;
const result = await execute(model, 'write', [[id], values]);
return json({ success: true, result });
}
case 'delete': {
const { model, id } = data;
const result = await execute(model, 'unlink', [[id]]);
return json({ success: true, result });
}
default:
return json({ success: false, error: 'Invalid action' }, { status: 400 });
}
} catch (error) {
console.error('Odoo API Error:', error);
return json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,311 @@
<script>
import { {{MODEL_NAME}}Cache, cacheStatus } from '$lib/stores/cache';
import { filterRecords } from '$lib/utils';
import { onMount, onDestroy } from 'svelte';
let searchTerm = $state('');
let records = $derived(${{MODEL_NAME}}Cache.records);
let filteredRecords = $derived(filterRecords(records, searchTerm, ['x_name']));
let status = $derived($cacheStatus);
onMount(async () => {
await {{MODEL_NAME}}Cache.initialize();
});
onDestroy(() => {
{{MODEL_NAME}}Cache.destroy();
});
async function handleDelete(id) {
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
return;
}
try {
await {{MODEL_NAME}}Cache.deleteRecord(id);
} catch (error) {
alert(`Failed to delete: ${error.message}`);
}
}
function handleRefresh() {
{{MODEL_NAME}}Cache.forceRefresh();
}
</script>
<svelte:head>
<title>All {{MODEL_DISPLAY_NAME}}s - {{PROJECT_NAME}}</title>
</svelte:head>
<div class="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<nav>
<a href="/">Add {{MODEL_DISPLAY_NAME}}</a>
<a href="/list" class="active">View All</a>
</nav>
<div class="list-container">
<div class="list-header">
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
<div class="header-actions">
<input
type="search"
placeholder="Search..."
bind:value={searchTerm}
class="search-input"
/>
<button class="refresh-btn" onclick={handleRefresh} disabled={status.isSyncing}>
{status.isSyncing ? '⏳' : '🔄'}
</button>
</div>
</div>
{#if status.isLoading}
<div class="loading">Loading...</div>
{:else if filteredRecords.length === 0}
<div class="empty">
{searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!'}
</div>
{:else}
<div class="record-list">
{#each filteredRecords as record (record.id)}
<div class="record-card">
<div class="record-content">
<h3>{record.x_name}</h3>
<!-- Add more fields to display -->
<p class="record-meta">ID: {record.id}</p>
</div>
<div class="record-actions">
<button class="delete-btn" onclick={() => handleDelete(record.id)}>
🗑️
</button>
</div>
</div>
{/each}
</div>
{/if}
{#if status.lastSync > 0}
<div class="sync-info">
Last synced: {new Date(status.lastSync).toLocaleString()}
{#if status.isStale}
<span class="stale-badge">Stale</span>
{/if}
</div>
{/if}
</div>
</div>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 16px;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
h2 {
margin: 0;
color: #333;
font-size: 1.5em;
}
nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: #667eea;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
nav a.active {
background: #667eea;
color: white;
}
.list-container {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
min-width: 200px;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
.refresh-btn {
padding: 10px 15px;
background: #f0f0f0;
color: #667eea;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
transform: rotate(360deg);
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading,
.empty {
text-align: center;
padding: 40px;
color: #666;
font-size: 16px;
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
transition: all 0.3s;
}
.record-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.record-content {
flex: 1;
}
.record-content h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 1.1em;
}
.record-meta {
margin: 0;
color: #666;
font-size: 0.9em;
}
.record-actions {
display: flex;
gap: 8px;
}
.delete-btn {
padding: 8px 12px;
background: #fff5f5;
color: #e53e3e;
border: 1px solid #feb2b2;
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.delete-btn:hover {
background: #fed7d7;
}
.sync-info {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
text-align: center;
color: #666;
font-size: 0.9em;
}
.stale-badge {
display: inline-block;
margin-left: 10px;
padding: 2px 8px;
background: #fef5e7;
color: #f39c12;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
@media (max-width: 600px) {
.list-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
flex-direction: column;
align-items: stretch;
}
.search-input {
min-width: auto;
}
.record-card {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.record-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -0,0 +1,4 @@
// Disable SSR for this app (client-side only with Odoo backend)
export const ssr = false;
export const csr = true;
export const prerender = true;

View File

@@ -0,0 +1,24 @@
<script>
let { children } = $props();
</script>
<svelte:head>
<title>{{PROJECT_NAME}} - {{MODEL_DISPLAY_NAME}} Manager</title>
<meta name="description" content="Offline-first {{MODEL_DISPLAY_NAME}} management app with Odoo integration" />
</svelte:head>
{@render children?.()}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
:global(*) {
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,271 @@
<script>
import { {{MODEL_NAME}}Cache } from '$lib/stores/cache';
import { onMount, onDestroy } from 'svelte';
let name = $state('');
let loading = $state(false);
let message = $state('');
let isOffline = $state(!navigator.onLine);
// Listen for online/offline events
if (typeof window !== 'undefined') {
window.addEventListener('online', () => { isOffline = false; });
window.addEventListener('offline', () => { isOffline = true; });
}
onMount(async () => {
await {{MODEL_NAME}}Cache.initialize();
});
onDestroy(() => {
{{MODEL_NAME}}Cache.destroy();
});
async function handleSubmit() {
if (!name.trim()) {
message = '⚠️ Please fill in the name field';
return;
}
loading = true;
message = '';
try {
const payload = {
x_name: name
// Add more fields as needed
};
await {{MODEL_NAME}}Cache.createRecord(payload);
if (navigator.onLine) {
message = '✅ {{MODEL_DISPLAY_NAME}} added successfully!';
} else {
message = '✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.';
}
// Reset form
name = '';
} catch (error) {
message = `❌ Error: ${error.message}`;
} finally {
loading = false;
}
}
function handleRefresh() {
{{MODEL_NAME}}Cache.forceRefresh();
}
</script>
<svelte:head>
<title>Add {{MODEL_DISPLAY_NAME}} - {{PROJECT_NAME}}</title>
</svelte:head>
<div class="container">
<h1>📋 {{PROJECT_NAME}}</h1>
<!-- Offline Indicator -->
{#if isOffline}
<div class="offline-banner">
📡 Offline Mode - Data will be synced when you're back online
</div>
{/if}
<nav>
<a href="/" class="active">Add {{MODEL_DISPLAY_NAME}}</a>
<a href="/list">View All</a>
</nav>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
bind:value={name}
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
required
/>
</div>
<!-- Add more form fields based on your Odoo model -->
{#if message}
<div class="message" class:error={message.includes('❌')}>{message}</div>
{/if}
<div class="button-group">
<button type="submit" disabled={loading}>
{loading ? '⏳ Adding...' : ' Add {{MODEL_DISPLAY_NAME}}'}
</button>
{#if !isOffline}
<button type="button" class="refresh-btn" onclick={handleRefresh}>
🔄 Refresh Data
</button>
{/if}
</div>
</form>
</div>
<style>
.container {
max-width: 600px;
margin: 0 auto;
padding: 16px;
}
@media (max-width: 480px) {
.container {
padding: 12px;
}
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
nav {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
border-radius: 10px;
padding: 5px;
}
nav a {
flex: 1;
text-align: center;
padding: 12px;
text-decoration: none;
color: #667eea;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
nav a.active {
background: #667eea;
color: white;
}
.offline-banner {
background: #e3f2fd;
color: #1565c0;
padding: 12px 20px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
border: 2px solid #64b5f6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
form {
background: white;
padding: 24px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
@media (max-width: 480px) {
form {
padding: 16px;
border-radius: 12px;
}
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
input,
select,
textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 150px;
padding: 15px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
button:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn {
background: #f0f0f0;
color: #667eea;
flex: 0 1 auto;
}
.refresh-btn:hover:not(:disabled) {
background: #e0e0e0;
color: #5568d3;
}
.message {
padding: 12px;
border-radius: 8px;
margin-bottom: 15px;
background: #d4edda;
color: #155724;
text-align: center;
}
.message.error {
background: #f8d7da;
color: #721c24;
}
</style>

View File

@@ -0,0 +1,24 @@
{
"name": "{{PROJECT_NAME}}",
"short_name": "{{PROJECT_NAME}}",
"description": "{{MODEL_DISPLAY_NAME}} management app with Odoo integration",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#667eea",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}