ShadCN Implementation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,7 +13,6 @@ venv/
|
||||
env/
|
||||
|
||||
# Django
|
||||
backend/db.sqlite3
|
||||
backend/media/
|
||||
backend/staticfiles/
|
||||
|
||||
|
||||
80
README.md
80
README.md
@@ -1,12 +1,84 @@
|
||||
# Django Port Workspace
|
||||
|
||||
This folder contains the rewrite target for the current FastAPI + React application.
|
||||
It is intended to be self-contained.
|
||||
|
||||
## What is here
|
||||
|
||||
- [`backend`](/home/sandy/HUB-master/django-port/backend): Django project and domain apps
|
||||
- [`frontend`](/home/sandy/HUB-master/django-port/frontend): SvelteKit frontend prepared for `shadcn-svelte`
|
||||
- [`docs/port-plan.md`](/home/sandy/HUB-master/django-port/docs/port-plan.md): migration strategy and scope
|
||||
- [`backend`](backend): Django project and domain apps
|
||||
- [`frontend`](frontend): SvelteKit frontend with a local manual shadcn-style component layer
|
||||
- [`data/legacy`](data/legacy): bundled legacy SQLite sources used by `import_legacy_data`
|
||||
- [`docs/port-plan.md`](docs/port-plan.md): migration strategy and scope
|
||||
|
||||
## Self-contained data
|
||||
|
||||
The folder already contains:
|
||||
|
||||
- the live Django database at [`backend/db.sqlite3`](backend/db.sqlite3)
|
||||
- the bundled legacy import sources at [`data/legacy/cincin_phase1.sqlite`](data/legacy/cincin_phase1.sqlite) and [`data/legacy/dalcorso.sqlite`](data/legacy/dalcorso.sqlite)
|
||||
|
||||
That means `django-port` no longer depends on the repo-root legacy databases for normal use or for re-running the importer.
|
||||
|
||||
## Fresh Machine Setup
|
||||
|
||||
`django-port` is now structured so it can be pushed as its own repository and used without the rest of the original workspace.
|
||||
|
||||
Backend setup:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e .
|
||||
cp .env.example .env
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Frontend setup:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Default local URLs:
|
||||
|
||||
- frontend: `http://localhost:5173`
|
||||
- backend API: `http://localhost:8000/api`
|
||||
|
||||
## Pushing As Its Own Repo
|
||||
|
||||
If this folder is moved into a new repository, include at minimum:
|
||||
|
||||
- `backend/`
|
||||
- `frontend/`
|
||||
- `data/legacy/`
|
||||
- `docs/`
|
||||
- `.gitignore`
|
||||
- `README.md`
|
||||
|
||||
If you want the seeded app data on the destination machine, commit and push:
|
||||
|
||||
- [`backend/db.sqlite3`](backend/db.sqlite3)
|
||||
- [`data/legacy/cincin_phase1.sqlite`](data/legacy/cincin_phase1.sqlite)
|
||||
- [`data/legacy/dalcorso.sqlite`](data/legacy/dalcorso.sqlite)
|
||||
|
||||
The repo does not need to include:
|
||||
|
||||
- any files outside `django-port`
|
||||
- a local virtualenv
|
||||
- `frontend/node_modules`
|
||||
- `frontend/.svelte-kit`
|
||||
|
||||
## Environment Files
|
||||
|
||||
Example env files are included at:
|
||||
|
||||
- [`backend/.env.example`](backend/.env.example)
|
||||
- [`frontend/.env.example`](frontend/.env.example)
|
||||
|
||||
## What is intentionally not done
|
||||
|
||||
@@ -14,4 +86,4 @@ This folder contains the rewrite target for the current FastAPI + React applicat
|
||||
- No Svelte or Django bootstrapping commands
|
||||
- No heavy build, dev-server, or migration runs
|
||||
|
||||
Everything committed here is code and structure only.
|
||||
Everything needed for the port itself now lives under this folder.
|
||||
|
||||
10
backend/.env.example
Normal file
10
backend/.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
DJANGO_SECRET_KEY=change-me
|
||||
DJANGO_DEBUG=1
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
DJANGO_TIME_ZONE=Europe/Budapest
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
DJANGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
|
||||
# Optional overrides. The defaults already point at ../data/legacy/.
|
||||
# LEGACY_CINCIN_DB=/absolute/path/to/cincin_phase1.sqlite
|
||||
# LEGACY_DALCORSO_DB=/absolute/path/to/dalcorso.sqlite
|
||||
@@ -14,14 +14,24 @@ This is the replacement backend for the legacy FastAPI app.
|
||||
|
||||
## Expected setup
|
||||
|
||||
1. Install dependencies from [`pyproject.toml`](/home/sandy/HUB-master/django-port/backend/pyproject.toml).
|
||||
2. Run `python manage.py makemigrations`.
|
||||
3. Run `python manage.py migrate`.
|
||||
4. Run `python manage.py createsuperuser`.
|
||||
5. Run `python manage.py import_legacy_data`.
|
||||
1. Create a virtualenv in `backend/`.
|
||||
2. Install dependencies from [`pyproject.toml`](pyproject.toml) with `pip install -e .`.
|
||||
3. Copy [`backend/.env.example`](.env.example) to `.env` or export the same variables in your shell.
|
||||
4. Run `python manage.py migrate`.
|
||||
5. Run `python manage.py createsuperuser` if you are starting from a blank DB.
|
||||
6. Run `python manage.py import_legacy_data` if you want to rebuild from the bundled legacy sources instead of using the committed `db.sqlite3`.
|
||||
|
||||
The import command now defaults to the bundled databases inside:
|
||||
|
||||
- [`../data/legacy/cincin_phase1.sqlite`](../data/legacy/cincin_phase1.sqlite)
|
||||
- [`../data/legacy/dalcorso.sqlite`](../data/legacy/dalcorso.sqlite)
|
||||
|
||||
You can still override them with `LEGACY_CINCIN_DB` and `LEGACY_DALCORSO_DB` if needed.
|
||||
|
||||
## Notes
|
||||
|
||||
- Auth uses Django sessions instead of custom JWT cookies.
|
||||
- The import command consolidates both legacy SQLite files into one Django schema.
|
||||
- Password hashes are not carried over directly; imported users get a forced reset placeholder.
|
||||
- The backend is intended to be runnable from the `django-port` subtree without depending on workspace-root data files.
|
||||
- The committed `db.sqlite3` is optional but supported. Keep it in the repo if you want seeded data to travel with the project.
|
||||
|
||||
@@ -110,5 +110,6 @@ CORS_ALLOWED_ORIGINS = [
|
||||
)
|
||||
]
|
||||
|
||||
LEGACY_CINCIN_DB = Path(os.getenv("LEGACY_CINCIN_DB", BASE_DIR.parent.parent / "cincin_phase1.sqlite"))
|
||||
LEGACY_DALCORSO_DB = Path(os.getenv("LEGACY_DALCORSO_DB", BASE_DIR.parent.parent / "dalcorso.sqlite"))
|
||||
LEGACY_DATA_DIR = BASE_DIR.parent / "data" / "legacy"
|
||||
LEGACY_CINCIN_DB = Path(os.getenv("LEGACY_CINCIN_DB", LEGACY_DATA_DIR / "cincin_phase1.sqlite"))
|
||||
LEGACY_DALCORSO_DB = Path(os.getenv("LEGACY_DALCORSO_DB", LEGACY_DATA_DIR / "dalcorso.sqlite"))
|
||||
|
||||
BIN
backend/db.sqlite3
Normal file
BIN
backend/db.sqlite3
Normal file
Binary file not shown.
13
data/legacy/README.md
Normal file
13
data/legacy/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Bundled Legacy Data
|
||||
|
||||
This directory contains the legacy SQLite databases used by the Django import command:
|
||||
|
||||
- `cincin_phase1.sqlite`
|
||||
- `dalcorso.sqlite`
|
||||
|
||||
The defaults in [`backend/config/settings.py`](../../backend/config/settings.py) point here so the `django-port` subtree can be used without depending on workspace-root copies of those files.
|
||||
|
||||
If needed, the importer can still be redirected with:
|
||||
|
||||
- `LEGACY_CINCIN_DB`
|
||||
- `LEGACY_DALCORSO_DB`
|
||||
BIN
data/legacy/cincin_phase1.sqlite
Normal file
BIN
data/legacy/cincin_phase1.sqlite
Normal file
Binary file not shown.
BIN
data/legacy/dalcorso.sqlite
Normal file
BIN
data/legacy/dalcorso.sqlite
Normal file
Binary file not shown.
@@ -21,7 +21,7 @@
|
||||
|
||||
1. Create Django migrations from the new model set.
|
||||
2. Migrate the new database.
|
||||
3. Run `import_legacy_data` to ingest `cincin_phase1.sqlite` and `dalcorso.sqlite`.
|
||||
3. Run `import_legacy_data` to ingest the bundled legacy SQLite files in `data/legacy/`.
|
||||
4. Rebuild auth credentials by forcing password resets for imported users.
|
||||
5. Move ETL scripts into Django management commands later instead of repo-root scripts.
|
||||
|
||||
|
||||
396
docs/session-handoff-2026-04-01-ui-reset.md
Normal file
396
docs/session-handoff-2026-04-01-ui-reset.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# Session Handoff
|
||||
|
||||
Date: April 1, 2026
|
||||
|
||||
Workspace root: [`/home/sandy/HUB-master`](/home/sandy/HUB-master)
|
||||
|
||||
Port root: [`/home/sandy/HUB-master/django-port`](/home/sandy/HUB-master/django-port)
|
||||
|
||||
This document supersedes the earlier handoff for the continuation work done after [`session-handoff-2026-04-01.md`](/home/sandy/HUB-master/django-port/docs/session-handoff-2026-04-01.md).
|
||||
|
||||
## 1. What Happened In This Continuation
|
||||
|
||||
The continuation work completed six major areas:
|
||||
|
||||
1. business access scoping was tightened across the backend API
|
||||
2. missing user and invoice update/delete flows were added
|
||||
3. a local `.gitignore` was added for the `django-port` subtree
|
||||
4. the temporary UI layer was replaced manually with a local `shadcn-svelte` style component system, then restyled again into a strict black-background, white-text, vanilla shadcn look
|
||||
5. the `django-port` subtree was made self-contained for data and repo portability
|
||||
6. startup and portability docs were tightened so the folder can be pushed as its own repo and started on another machine with only local env files and dependency installs
|
||||
|
||||
Important note:
|
||||
|
||||
- this UI work does **not** depend on the external `shadcn-svelte` CLI
|
||||
- the component layer now exists as local source files inside the repo
|
||||
|
||||
## 2. Backend Changes Since The Previous Handoff
|
||||
|
||||
Primary backend file still in play:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/apps/api/views.py`](/home/sandy/HUB-master/django-port/backend/apps/api/views.py)
|
||||
|
||||
### 2.1 Business Scoping Hardening
|
||||
|
||||
Scoping helpers were added or expanded so non-superusers now default to business-limited data more consistently:
|
||||
|
||||
- scoped vendors
|
||||
- scoped categories
|
||||
- scoped products
|
||||
- scoped invoices
|
||||
- vendor access checks
|
||||
- invoice payload access validation
|
||||
|
||||
Effects:
|
||||
|
||||
- dashboard overview is no longer global for non-superusers
|
||||
- vendors list now defaults to allowed businesses
|
||||
- vendor detail is denied outside allowed businesses
|
||||
- invoices list defaults to allowed businesses
|
||||
- invoice detail is resolved through scoped querysets
|
||||
- inventory now derives visibility from scoped products instead of the earlier brittle ad hoc filter
|
||||
- business summary inventory count is tied to that business’s vendor/category graph instead of the earlier global count
|
||||
|
||||
### 2.2 User And Invoice CRUD Completion
|
||||
|
||||
Invoice service logic was consolidated in:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/apps/operations/services.py`](/home/sandy/HUB-master/django-port/backend/apps/operations/services.py)
|
||||
|
||||
The service now has a shared persistence path for create and update:
|
||||
|
||||
- `create_invoice_from_payload`
|
||||
- `update_invoice_from_payload`
|
||||
|
||||
Added backend routes:
|
||||
|
||||
- `PUT /api/invoices/<invoice_id>/`
|
||||
- `DELETE /api/invoices/<invoice_id>/`
|
||||
- `GET /api/settings/users/<user_id>/`
|
||||
- `PUT /api/settings/users/<user_id>/`
|
||||
- `DELETE /api/settings/users/<user_id>/`
|
||||
|
||||
Routing file:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/apps/api/urls.py`](/home/sandy/HUB-master/django-port/backend/apps/api/urls.py)
|
||||
|
||||
### 2.3 Safety Notes
|
||||
|
||||
Current user deletion behavior:
|
||||
|
||||
- users cannot delete their own account through the new user detail endpoint
|
||||
|
||||
Current invoice write validation:
|
||||
|
||||
- business access is checked
|
||||
- vendor access is checked
|
||||
- category access is checked for non-superusers
|
||||
- product access is checked for non-superusers
|
||||
|
||||
## 3. Frontend/API Surface Added Since The Previous Handoff
|
||||
|
||||
API client file:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/api/client.ts`](/home/sandy/HUB-master/django-port/frontend/src/lib/api/client.ts)
|
||||
|
||||
Added frontend client methods:
|
||||
|
||||
- `updateInvoice`
|
||||
- `deleteInvoice`
|
||||
- `updateUser`
|
||||
- `deleteUser`
|
||||
|
||||
Frontend pages updated to use those flows:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/invoices/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/invoices/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/settings/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/settings/+page.svelte)
|
||||
|
||||
Behavior:
|
||||
|
||||
- invoice page now supports create, edit, and delete
|
||||
- settings page now supports create, edit, and delete for users
|
||||
|
||||
## 4. Gitignore Added
|
||||
|
||||
Added:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/.gitignore`](/home/sandy/HUB-master/django-port/.gitignore)
|
||||
|
||||
It ignores:
|
||||
|
||||
- `venv`
|
||||
- Python caches
|
||||
- Django static/media outputs
|
||||
- `frontend/node_modules`
|
||||
- Svelte/Vite build artifacts
|
||||
- common log and editor files
|
||||
|
||||
Important update from the latest continuation:
|
||||
|
||||
- the live Django database is **no longer ignored**
|
||||
- this was changed intentionally so `backend/db.sqlite3` can be committed if the user wants the shipped data included when pushing `django-port` to another repository
|
||||
|
||||
## 4.1 Self-Contained Data Packaging
|
||||
|
||||
The project no longer depends on workspace-root legacy SQLite files.
|
||||
|
||||
Bundled data now lives inside the port itself:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/db.sqlite3`](/home/sandy/HUB-master/django-port/backend/db.sqlite3)
|
||||
- [`/home/sandy/HUB-master/django-port/data/legacy/cincin_phase1.sqlite`](/home/sandy/HUB-master/django-port/data/legacy/cincin_phase1.sqlite)
|
||||
- [`/home/sandy/HUB-master/django-port/data/legacy/dalcorso.sqlite`](/home/sandy/HUB-master/django-port/data/legacy/dalcorso.sqlite)
|
||||
|
||||
Defaults in:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/config/settings.py`](/home/sandy/HUB-master/django-port/backend/config/settings.py)
|
||||
|
||||
now point `LEGACY_CINCIN_DB` and `LEGACY_DALCORSO_DB` at `django-port/data/legacy/` instead of the workspace root.
|
||||
|
||||
Documentation for this was added/updated in:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/README.md`](/home/sandy/HUB-master/django-port/README.md)
|
||||
- [`/home/sandy/HUB-master/django-port/backend/README.md`](/home/sandy/HUB-master/django-port/backend/README.md)
|
||||
- [`/home/sandy/HUB-master/django-port/docs/port-plan.md`](/home/sandy/HUB-master/django-port/docs/port-plan.md)
|
||||
- [`/home/sandy/HUB-master/django-port/data/legacy/README.md`](/home/sandy/HUB-master/django-port/data/legacy/README.md)
|
||||
|
||||
Additional portability setup in the latest pass:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/.env.example`](/home/sandy/HUB-master/django-port/backend/.env.example)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/.env.example`](/home/sandy/HUB-master/django-port/frontend/.env.example)
|
||||
|
||||
The root README now includes:
|
||||
|
||||
- fresh-machine backend setup
|
||||
- fresh-machine frontend setup
|
||||
- repo contents that should be pushed
|
||||
- explicit note that files outside `django-port` are no longer required
|
||||
|
||||
## 5. Current UI Architecture
|
||||
|
||||
This changed substantially.
|
||||
|
||||
The old situation was:
|
||||
|
||||
- temporary `button.svelte`
|
||||
- temporary `card.svelte`
|
||||
- warm custom palette
|
||||
- mixed per-page raw inputs/selects/textareas
|
||||
|
||||
The current situation is:
|
||||
|
||||
- a local manual `shadcn-svelte` style component layer lives under [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui)
|
||||
- global styling now uses a dark vanilla shadcn-like token set
|
||||
- screens have been migrated onto those primitives
|
||||
|
||||
### 5.1 Core UI Files
|
||||
|
||||
Utility helper:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/utils.ts`](/home/sandy/HUB-master/django-port/frontend/src/lib/utils.ts)
|
||||
|
||||
Core primitives:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/button.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/button.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-header.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-header.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-title.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-title.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-description.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-description.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-content.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-content.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-footer.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/card-footer.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/input.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/input.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/label.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/label.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/select.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/select.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/textarea.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/ui/textarea.svelte)
|
||||
|
||||
Global theme:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/app.css`](/home/sandy/HUB-master/django-port/frontend/src/app.css)
|
||||
|
||||
### 5.2 Visual Direction Now In Effect
|
||||
|
||||
The latest user correction explicitly required:
|
||||
|
||||
- white text
|
||||
- black backgrounds
|
||||
- purely vanilla shadcn look
|
||||
- no leftover warm/custom brand styling
|
||||
|
||||
Current global direction:
|
||||
|
||||
- background is near-black
|
||||
- foreground is near-white
|
||||
- cards are dark
|
||||
- primary action is light-on-dark inversion
|
||||
- muted surfaces are dark zinc-like blocks
|
||||
- typography uses a plain system sans stack rather than the earlier decorative direction
|
||||
|
||||
### 5.3 Shell Layout
|
||||
|
||||
Shell files:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/lib/components/app-shell/sidebar.svelte`](/home/sandy/HUB-master/django-port/frontend/src/lib/components/app-shell/sidebar.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/+layout.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/+layout.svelte)
|
||||
|
||||
Current shell behavior:
|
||||
|
||||
- dark app frame
|
||||
- left navigation in a sticky card
|
||||
- active route highlighted
|
||||
- main content area wrapped in a dark bordered panel
|
||||
|
||||
## 6. Pages Converted To The Manual Shadcn Layer
|
||||
|
||||
The following pages are now on the new local component layer and dark token system:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/login/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/login/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/dashboard/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/dashboard/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/invoices/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/invoices/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/vendors/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/vendors/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/inventory/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/inventory/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/events/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/events/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/schedule/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/schedule/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/settings/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/settings/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/devices/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/devices/+page.svelte)
|
||||
- [`/home/sandy/HUB-master/django-port/frontend/src/routes/app/business/[id]/+page.svelte`](/home/sandy/HUB-master/django-port/frontend/src/routes/app/business/[id]/+page.svelte)
|
||||
|
||||
## 7. Current Quality/Verification State
|
||||
|
||||
Backend verification used:
|
||||
|
||||
```bash
|
||||
python3 -m compileall django-port/backend
|
||||
```
|
||||
|
||||
This passed after the backend continuation work.
|
||||
|
||||
Frontend verification used:
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
This now passes with:
|
||||
|
||||
- `0 errors`
|
||||
- `0 warnings`
|
||||
|
||||
That is an improvement over the prior state where the frontend still had a set of accessibility warnings and one intermediate TypeScript regression during the CRUD/UI migration.
|
||||
|
||||
## 8. What Is Better Now
|
||||
|
||||
Compared with the prior handoff, the project is in a materially better state:
|
||||
|
||||
- backend scoping is more defensible
|
||||
- missing user/invoice CRUD is no longer missing
|
||||
- repo noise in `django-port` is reduced via `.gitignore`
|
||||
- the data needed to rerun the importer now lives under `django-port`
|
||||
- the live Django database can now be committed with the subtree for portability
|
||||
- UI is no longer based on temporary one-off primitives
|
||||
- the frontend no longer mixes multiple visual directions
|
||||
- the current visual direction now matches the user’s final explicit correction: black backgrounds, white text, vanilla shadcn feel
|
||||
|
||||
## 9. Remaining Gaps / Risks
|
||||
|
||||
Even after this continuation, there are still real gaps.
|
||||
|
||||
### 9.1 UI Is Manual, Not CLI-Generated
|
||||
|
||||
The component layer is local and manual.
|
||||
|
||||
That is acceptable for the user’s stated direction, but it means:
|
||||
|
||||
- future new components are still hand-authored unless someone later chooses to bring in the CLI workflow
|
||||
|
||||
### 9.2 Some Interaction Primitives Are Still Native
|
||||
|
||||
The visual system is consistent enough now, but some lower-level interaction pieces are still native rather than componentized, for example:
|
||||
|
||||
- plain checkbox controls
|
||||
- some inline destructive text buttons
|
||||
- some per-row action buttons inside lists
|
||||
|
||||
If the next session wants to push polish further, likely next UI candidates are:
|
||||
|
||||
- checkbox
|
||||
- badge
|
||||
- alert
|
||||
- table or data-list wrappers
|
||||
- reusable stat-card component
|
||||
|
||||
### 9.3 API Structure Is Still Large
|
||||
|
||||
This is unchanged from the earlier handoff:
|
||||
|
||||
- [`/home/sandy/HUB-master/django-port/backend/apps/api/views.py`](/home/sandy/HUB-master/django-port/backend/apps/api/views.py) is still too large
|
||||
|
||||
Correctness was prioritized over structural refactor.
|
||||
|
||||
### 9.4 No Automated Backend Test Coverage
|
||||
|
||||
Still missing:
|
||||
|
||||
- API tests
|
||||
- model/service tests
|
||||
- end-to-end tests
|
||||
|
||||
### 9.5 Repo Portability Depends On What Gets Committed
|
||||
|
||||
The folder is now prepared so it can stand alone, but another machine still only gets what is actually pushed.
|
||||
|
||||
To make the subtree truly self-contained in a new repository, the pushed repo should include at minimum:
|
||||
|
||||
- `backend/`
|
||||
- `frontend/`
|
||||
- `data/legacy/`
|
||||
- `docs/`
|
||||
- `.gitignore`
|
||||
- `README.md`
|
||||
- `backend/db.sqlite3` if the preloaded application data should ship too
|
||||
|
||||
It should **not** need:
|
||||
|
||||
- repo-root legacy databases outside `django-port`
|
||||
- repo-root frontend/backend folders outside `django-port`
|
||||
- local `venv`
|
||||
- local `node_modules`
|
||||
- `.svelte-kit`
|
||||
|
||||
Recommended startup files for another machine:
|
||||
|
||||
- `backend/.env` copied from `backend/.env.example`
|
||||
- `frontend/.env` copied from `frontend/.env.example`
|
||||
|
||||
Only the host/origin values should need adjustment if the deployment URLs differ from the local defaults.
|
||||
|
||||
## 10. Recommended Next Steps
|
||||
|
||||
If another Codex instance continues from here, recommended order is:
|
||||
|
||||
1. do manual browser QA on the dark UI across the main routes
|
||||
2. fix any remaining visual inconsistencies discovered in that manual pass
|
||||
3. decide whether `backend/db.sqlite3` should be committed in the destination repo as seeded app data
|
||||
4. add reusable primitives for checkbox/badge/alert if more UI polish is desired
|
||||
5. only after UI stabilizes, consider splitting `apps/api/views.py`
|
||||
6. add targeted API tests for business scoping and user/invoice CRUD
|
||||
|
||||
## 11. Immediate Startup Context For The Next Codex
|
||||
|
||||
The project is no longer at a bootstrap stage.
|
||||
|
||||
The current state should be understood as:
|
||||
|
||||
- Django backend is functioning
|
||||
- Svelte frontend is functioning
|
||||
- auth works
|
||||
- scoping is better than before
|
||||
- user/invoice CRUD exists
|
||||
- UI layer is now local manual shadcn-style and dark
|
||||
|
||||
So the next session should not redo scaffolding.
|
||||
|
||||
It should assume the task is now one of:
|
||||
|
||||
- polish
|
||||
- QA
|
||||
- test coverage
|
||||
- structural cleanup
|
||||
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE=http://localhost:8000/api
|
||||
@@ -1,38 +1,50 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #f5f1e8;
|
||||
--foreground: #201916;
|
||||
--card: #fffcf6;
|
||||
--card-foreground: #201916;
|
||||
--popover: #fff9ef;
|
||||
--popover-foreground: #201916;
|
||||
--primary: #16302b;
|
||||
--primary-foreground: #f6efe4;
|
||||
--secondary: #d8c3a8;
|
||||
--secondary-foreground: #2d241e;
|
||||
--muted: #ece2d3;
|
||||
--muted-foreground: #615246;
|
||||
--accent: #ba6c46;
|
||||
--accent-foreground: #fff7ef;
|
||||
--border: #cdbca5;
|
||||
--input: #e7dbca;
|
||||
--ring: #16302b;
|
||||
--background: #09090b;
|
||||
--foreground: #fafafa;
|
||||
--card: #09090b;
|
||||
--card-foreground: #fafafa;
|
||||
--popover: #09090b;
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #fafafa;
|
||||
--primary-foreground: #09090b;
|
||||
--secondary: #18181b;
|
||||
--secondary-foreground: #fafafa;
|
||||
--muted: #18181b;
|
||||
--muted-foreground: #a1a1aa;
|
||||
--accent: #27272a;
|
||||
--accent-foreground: #fafafa;
|
||||
--border: #27272a;
|
||||
--input: #27272a;
|
||||
--ring: #d4d4d8;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(186, 108, 70, 0.18), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(22, 48, 43, 0.18), transparent 35%),
|
||||
var(--background);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: color-mix(in srgb, var(--card) 92%, white);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 18px 60px rgba(32, 25, 22, 0.08);
|
||||
* {
|
||||
border-color: var(--border);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #3f3f46;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { api } from "$lib/api/client";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import { authUser, clearAuth, hasPermission } from "$lib/stores/auth";
|
||||
|
||||
const items = [
|
||||
@@ -28,14 +30,22 @@
|
||||
$: visibleItems = items.filter((item) => !$authUser || hasPermission(item.permission));
|
||||
</script>
|
||||
|
||||
<aside class="panel flex w-full max-w-64 flex-col gap-3 p-4">
|
||||
<Card className="flex w-full max-w-72 flex-col gap-4 bg-[var(--secondary)] p-4 lg:sticky lg:top-4 lg:h-[calc(100vh-2rem)]">
|
||||
<div class="border-b border-[var(--border)] pb-4">
|
||||
<p class="text-xs uppercase tracking-[0.25em] text-[var(--muted-foreground)]">Central Hub</p>
|
||||
<h1 class="mt-2 text-xl font-semibold">Hospitality Ops</h1>
|
||||
<p class="mt-1 text-sm text-[var(--muted-foreground)]">Django control surface</p>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-2">
|
||||
{#each visibleItems as item}
|
||||
<a class="rounded-xl px-3 py-2 text-sm text-[var(--foreground)] transition hover:bg-black/5" href={item.href}>
|
||||
<a
|
||||
class={`rounded-lg px-3 py-2 text-sm transition ${
|
||||
page.url.pathname === item.href
|
||||
? "bg-[var(--foreground)] text-[var(--background)]"
|
||||
: "text-[var(--muted-foreground)] hover:bg-[var(--accent)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
@@ -49,4 +59,4 @@
|
||||
<Button type="button" variant="ghost" on:click={signOut}>Sign out</Button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</Card>
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let href: string | undefined = undefined;
|
||||
export let type: "button" | "submit" | "reset" = "button";
|
||||
export let variant: "primary" | "secondary" | "ghost" | "destructive" = "primary";
|
||||
export let variant: ButtonVariant = "default";
|
||||
export let size: ButtonSize = "default";
|
||||
export let disabled = false;
|
||||
export let className = "";
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: MouseEvent }>();
|
||||
|
||||
const variants = {
|
||||
primary: "bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-95",
|
||||
secondary: "bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-95",
|
||||
ghost: "bg-transparent text-[var(--foreground)] hover:bg-black/5",
|
||||
destructive: "bg-red-700 text-white hover:bg-red-800"
|
||||
};
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors ring-offset-[var(--background)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-[var(--primary)] text-[var(--primary-foreground)] shadow-sm hover:opacity-95",
|
||||
secondary: "bg-[var(--secondary)] text-[var(--secondary-foreground)] shadow-sm hover:opacity-95",
|
||||
ghost: "hover:bg-[var(--accent)] text-[var(--foreground)]",
|
||||
destructive: "bg-red-700 text-white shadow-sm hover:bg-red-800",
|
||||
outline: "border border-[var(--border)] bg-[var(--background)] shadow-sm hover:bg-[var(--accent)]"
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
dispatch("click", event);
|
||||
@@ -22,7 +47,7 @@
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
class={`inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-medium transition ${variants[variant]}`}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{href}
|
||||
{...$$restProps}
|
||||
on:click={handleClick}
|
||||
@@ -31,7 +56,7 @@
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
class={`inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-medium transition disabled:opacity-50 ${variants[variant]}`}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...$$restProps}
|
||||
|
||||
9
frontend/src/lib/components/ui/card-content.svelte
Normal file
9
frontend/src/lib/components/ui/card-content.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<div class={cn("p-6 pt-0", className)}>
|
||||
<slot />
|
||||
</div>
|
||||
9
frontend/src/lib/components/ui/card-description.svelte
Normal file
9
frontend/src/lib/components/ui/card-description.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<p class={cn("text-sm text-[var(--muted-foreground)]", className)}>
|
||||
<slot />
|
||||
</p>
|
||||
9
frontend/src/lib/components/ui/card-footer.svelte
Normal file
9
frontend/src/lib/components/ui/card-footer.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<div class={cn("flex items-center p-6 pt-0", className)}>
|
||||
<slot />
|
||||
</div>
|
||||
9
frontend/src/lib/components/ui/card-header.svelte
Normal file
9
frontend/src/lib/components/ui/card-header.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-1.5 p-6", className)}>
|
||||
<slot />
|
||||
</div>
|
||||
9
frontend/src/lib/components/ui/card-title.svelte
Normal file
9
frontend/src/lib/components/ui/card-title.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<h3 class={cn("leading-none font-semibold tracking-tight", className)}>
|
||||
<slot />
|
||||
</h3>
|
||||
@@ -1,3 +1,14 @@
|
||||
<div class="panel p-5">
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"rounded-xl border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)] shadow-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
17
frontend/src/lib/components/ui/input.svelte
Normal file
17
frontend/src/lib/components/ui/input.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let type: string = "text";
|
||||
export let value: string | number = "";
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<input
|
||||
{type}
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-[var(--input)] bg-[var(--background)] px-3 py-2 text-sm ring-offset-[var(--background)] file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[var(--muted-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
/>
|
||||
10
frontend/src/lib/components/ui/label.svelte
Normal file
10
frontend/src/lib/components/ui/label.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let forId: string | undefined = undefined;
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<label for={forId} class={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)}>
|
||||
<slot />
|
||||
</label>
|
||||
17
frontend/src/lib/components/ui/select.svelte
Normal file
17
frontend/src/lib/components/ui/select.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let value: string | number | null = "";
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-[var(--input)] bg-[var(--background)] px-3 py-2 text-sm ring-offset-[var(--background)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
16
frontend/src/lib/components/ui/textarea.svelte
Normal file
16
frontend/src/lib/components/ui/textarea.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
export let value: string = "";
|
||||
export let className = "";
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex min-h-20 w-full rounded-md border border-[var(--input)] bg-[var(--background)] px-3 py-2 text-sm ring-offset-[var(--background)] placeholder:text-[var(--muted-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
</textarea>
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import Sidebar from "$lib/components/app-shell/sidebar.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import { authReady, bootstrapAuth } from "$lib/stores/auth";
|
||||
|
||||
let loading = true;
|
||||
@@ -24,21 +25,21 @@
|
||||
|
||||
{#if loading || !$authReady}
|
||||
<div class="mx-auto flex min-h-screen max-w-7xl items-center justify-center px-6 py-10">
|
||||
<div class="panel p-6 text-center">
|
||||
<Card className="p-6 text-center">
|
||||
<p class="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">Session</p>
|
||||
<p class="mt-2 text-lg font-medium">Checking authentication…</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="mx-auto flex min-h-screen max-w-7xl items-center justify-center px-6 py-10">
|
||||
<div class="panel p-6 text-center">
|
||||
<p class="text-sm text-red-700">{error}</p>
|
||||
</div>
|
||||
<Card className="p-6 text-center">
|
||||
<p class="text-sm text-red-300">{error}</p>
|
||||
</Card>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto flex min-h-screen max-w-7xl gap-6 px-4 py-4 lg:px-6">
|
||||
<div class="mx-auto flex min-h-screen max-w-[1600px] gap-6 px-4 py-4 lg:px-6">
|
||||
<Sidebar />
|
||||
<main class="min-w-0 flex-1">
|
||||
<main class="min-w-0 flex-1 rounded-2xl border border-[var(--border)] bg-[var(--secondary)]/40 p-4 lg:p-6">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { BusinessSummaryPayload } from "$lib/types/domain";
|
||||
|
||||
@@ -19,7 +23,7 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{:else if summary}
|
||||
<div>
|
||||
<p class="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">{summary.business.short_code}</p>
|
||||
@@ -27,21 +31,21 @@
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Revenue</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_revenue.toFixed(2)} {summary.business.currency}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Expenses</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_expenses.toFixed(2)} {summary.business.currency}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Outstanding invoices</p><p class="mt-2 text-3xl font-semibold">{summary.stats.outstanding_invoices}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Invoices</p><p class="mt-2 text-3xl font-semibold">{summary.stats.invoice_count}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">VAT tracked</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_vat.toFixed(2)} {summary.business.currency}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Inventory items</p><p class="mt-2 text-3xl font-semibold">{summary.stats.inventory_items}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Revenue</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_revenue.toFixed(2)} {summary.business.currency}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Expenses</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_expenses.toFixed(2)} {summary.business.currency}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Outstanding invoices</p><p class="mt-2 text-3xl font-semibold">{summary.stats.outstanding_invoices}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Invoices</p><p class="mt-2 text-3xl font-semibold">{summary.stats.invoice_count}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">VAT tracked</p><p class="mt-2 text-3xl font-semibold">{summary.stats.total_vat.toFixed(2)} {summary.business.currency}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Inventory items</p><p class="mt-2 text-3xl font-semibold">{summary.stats.inventory_items}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Recent revenue</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Latest imported revenue summary rows for this business.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Recent revenue</CardTitle>
|
||||
<CardDescription>Latest imported revenue summary rows for this business.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each summary.recent_revenue as row}
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
|
||||
<div>
|
||||
@@ -54,15 +58,15 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Recent invoices</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Most recent invoice activity linked to this business.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Recent invoices</CardTitle>
|
||||
<CardDescription>Most recent invoice activity linked to this business.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each summary.recent_invoices as invoice}
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
|
||||
<div>
|
||||
@@ -75,10 +79,10 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{:else}
|
||||
<Card>Loading business summary…</Card>
|
||||
<Card><CardContent className="p-5">Loading business summary…</CardContent></Card>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { DashboardBusinessSummary, DashboardOverview } from "$lib/types/domain";
|
||||
|
||||
@@ -30,27 +34,25 @@
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{:else if overview}
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Revenue</p><p class="mt-2 text-3xl font-semibold">{overview.total_revenue.toFixed(2)}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Expenses</p><p class="mt-2 text-3xl font-semibold">{overview.total_expenses.toFixed(2)}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Outstanding invoices</p><p class="mt-2 text-3xl font-semibold">{overview.outstanding_invoices}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">VAT tracked</p><p class="mt-2 text-3xl font-semibold">{overview.total_vat.toFixed(2)}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Vendors</p><p class="mt-2 text-3xl font-semibold">{overview.vendor_count}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Unread notifications</p><p class="mt-2 text-3xl font-semibold">{overview.unread_notifications}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Revenue</p><p class="mt-2 text-3xl font-semibold">{overview.total_revenue.toFixed(2)}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Expenses</p><p class="mt-2 text-3xl font-semibold">{overview.total_expenses.toFixed(2)}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Outstanding invoices</p><p class="mt-2 text-3xl font-semibold">{overview.outstanding_invoices}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">VAT tracked</p><p class="mt-2 text-3xl font-semibold">{overview.total_vat.toFixed(2)}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Vendors</p><p class="mt-2 text-3xl font-semibold">{overview.vendor_count}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Unread notifications</p><p class="mt-2 text-3xl font-semibold">{overview.unread_notifications}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">Business roll-up</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Per-business revenue, expenses, and invoice pressure.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Business roll-up</CardTitle>
|
||||
<CardDescription>Per-business revenue, expenses, and invoice pressure.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each businesses as business}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{business.business_name}</p>
|
||||
@@ -66,9 +68,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card>Loading dashboard…</Card>
|
||||
<Card><CardContent className="p-5">Loading dashboard…</CardContent></Card>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { DeviceItem, DeviceRegistrationTokenItem } from "$lib/types/domain";
|
||||
|
||||
@@ -112,28 +117,28 @@
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<Card>Loading devices…</Card>
|
||||
<Card><CardContent className="p-5">Loading devices…</CardContent></Card>
|
||||
{:else}
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Allowed devices</p><p class="mt-2 text-3xl font-semibold">{devices.length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Active devices</p><p class="mt-2 text-3xl font-semibold">{devices.filter((device) => device.is_active).length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Registration tokens</p><p class="mt-2 text-3xl font-semibold">{tokens.length}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Allowed devices</p><p class="mt-2 text-3xl font-semibold">{devices.length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Active devices</p><p class="mt-2 text-3xl font-semibold">{devices.filter((device) => device.is_active).length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Registration tokens</p><p class="mt-2 text-3xl font-semibold">{tokens.length}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<div class="space-y-6">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Whitelisted devices</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Imported device/IP allowlist plus new Django-managed entries.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Whitelisted devices</CardTitle>
|
||||
<CardDescription>Imported device/IP allowlist plus new Django-managed entries.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each devices as device}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{device.label || device.ip_address}</p>
|
||||
@@ -143,65 +148,69 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class={`text-sm ${device.is_active ? "text-green-700" : "text-red-700"}`}>{device.is_active ? "Active" : "Inactive"}</p>
|
||||
<button class="mt-2 text-sm text-red-700 hover:opacity-80" on:click={() => removeDevice(device.id)}>Delete</button>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{device.is_active ? "Active" : "Inactive"}</p>
|
||||
<button class="mt-2 text-sm text-red-300 hover:text-red-200" on:click={() => removeDevice(device.id)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Registration tokens</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Manual tokens for controlled device onboarding.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Registration tokens</CardTitle>
|
||||
<CardDescription>Manual tokens for controlled device onboarding.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each tokens as token}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<p class="font-medium">{token.label || "Untitled token"}</p>
|
||||
<p class="mt-1 break-all text-sm text-[var(--muted-foreground)]">{token.token}</p>
|
||||
<p class="mt-1 text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
||||
Expires {new Date(token.expires_at).toLocaleString()} {token.used_at ? `• used ${new Date(token.used_at).toLocaleString()}` : "• unused"}
|
||||
</p>
|
||||
<button class="mt-3 text-sm text-red-700 hover:opacity-80" on:click={() => removeToken(token.id)}>Delete</button>
|
||||
<button class="mt-3 text-sm text-red-300 hover:text-red-200" on:click={() => removeToken(token.id)}>Delete</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Add device</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Create a whitelist entry directly in the new backend.</p>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Add device</CardTitle>
|
||||
<CardDescription>Create a whitelist entry directly in the new backend.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form class="space-y-4" on:submit|preventDefault={saveDevice}>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={deviceForm.ip_address} placeholder="IP address" required />
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={deviceForm.label} placeholder="Label" />
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={deviceForm.user_agent} placeholder="User agent" />
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={deviceForm.ipv6_prefix} placeholder="IPv6 prefix" />
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={deviceForm.known_ips} placeholder="Known IPs, comma separated" />
|
||||
<Input bind:value={deviceForm.ip_address} placeholder="IP address" required />
|
||||
<Input bind:value={deviceForm.label} placeholder="Label" />
|
||||
<Input bind:value={deviceForm.user_agent} placeholder="User agent" />
|
||||
<Input bind:value={deviceForm.ipv6_prefix} placeholder="IPv6 prefix" />
|
||||
<Input bind:value={deviceForm.known_ips} placeholder="Known IPs, comma separated" />
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" bind:checked={deviceForm.is_active} />
|
||||
Active device
|
||||
</label>
|
||||
<Button type="submit" disabled={savingDevice}>{savingDevice ? "Saving…" : "Add device"}</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Create registration token</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Generate a fresh onboarding token with an expiry window.</p>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Create registration token</CardTitle>
|
||||
<CardDescription>Generate a fresh onboarding token with an expiry window.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form class="space-y-4" on:submit|preventDefault={saveToken}>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={tokenForm.label} placeholder="Label" />
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="number" min="1" bind:value={tokenForm.expires_in_days} />
|
||||
<Input bind:value={tokenForm.label} placeholder="Label" />
|
||||
<Input type="number" min="1" bind:value={tokenForm.expires_in_days} />
|
||||
<Button type="submit" disabled={savingToken}>{savingToken ? "Saving…" : "Create token"}</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import Textarea from "$lib/components/ui/textarea.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Business, EventItem } from "$lib/types/domain";
|
||||
|
||||
@@ -22,7 +29,7 @@
|
||||
end_datetime: "",
|
||||
all_day: false,
|
||||
location: "",
|
||||
color: "#ba6c46",
|
||||
color: "#09090b",
|
||||
recurrence_type: "none",
|
||||
recurrence_end_date: ""
|
||||
};
|
||||
@@ -70,7 +77,7 @@
|
||||
end_datetime: "",
|
||||
all_day: false,
|
||||
location: "",
|
||||
color: "#ba6c46",
|
||||
color: "#09090b",
|
||||
recurrence_type: "none",
|
||||
recurrence_end_date: ""
|
||||
};
|
||||
@@ -94,7 +101,7 @@
|
||||
end_datetime: event.end_datetime.slice(0, 16),
|
||||
all_day: event.all_day,
|
||||
location: event.location,
|
||||
color: event.color || "#ba6c46",
|
||||
color: event.color || "#09090b",
|
||||
recurrence_type: event.recurrence_type,
|
||||
recurrence_end_date: event.recurrence_end_date || ""
|
||||
};
|
||||
@@ -119,32 +126,33 @@
|
||||
<h2 class="mt-2 text-3xl font-semibold">Event planner</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedBusiness}>
|
||||
<Select bind:value={selectedBusiness}>
|
||||
<option value="">All businesses</option>
|
||||
{#each businesses as business}
|
||||
<option value={business.id}>{business.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
<Button type="button" on:click={loadData}>Filter</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Upcoming events</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Imported events plus newly created Django-side events.</p>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Upcoming events</CardTitle>
|
||||
<CardDescription>Imported events plus newly created Django-side events.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#if loading}
|
||||
<p>Loading events…</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each events as event}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{event.title}</p>
|
||||
@@ -154,49 +162,51 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)]" on:click={() => editEvent(event)}>Edit</button>
|
||||
<button class="text-sm text-red-700 hover:opacity-80" on:click={() => removeEvent(event.id)}>Delete</button>
|
||||
<div class="h-4 w-4 rounded-full border border-black/10" style={`background:${event.color || "#ba6c46"}`}></div>
|
||||
<button class="text-sm text-red-300 hover:text-red-200" on:click={() => removeEvent(event.id)}>Delete</button>
|
||||
<div class="h-4 w-4 rounded-full border border-white/10" style={`background:${event.color || "#09090b"}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Create event</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{editingEventId ? "Update an existing event." : "A lean replacement for the old calendar modal stack."}</p>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">{editingEventId ? "Edit event" : "Create event"}</CardTitle>
|
||||
<CardDescription>{editingEventId ? "Update an existing event." : "A lean replacement for the old calendar modal stack."}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form class="space-y-4" on:submit|preventDefault={saveEvent}>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.business_id}>
|
||||
<Select bind:value={form.business_id}>
|
||||
{#each businesses as business}
|
||||
<option value={business.id}>{business.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.title} placeholder="Event title" required />
|
||||
<textarea class="min-h-24 w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.description} placeholder="Description"></textarea>
|
||||
</Select>
|
||||
<Input bind:value={form.title} placeholder="Event title" required />
|
||||
<Textarea className="min-h-24" bind:value={form.description} placeholder="Description"></Textarea>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.event_type}>
|
||||
<Select bind:value={form.event_type}>
|
||||
<option value="other">Other</option>
|
||||
<option value="service">Service</option>
|
||||
<option value="private">Private</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
</select>
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.location} placeholder="Location" />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="datetime-local" bind:value={form.start_datetime} required />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="datetime-local" bind:value={form.end_datetime} required />
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.recurrence_type}>
|
||||
</Select>
|
||||
<Input bind:value={form.location} placeholder="Location" />
|
||||
<Input type="datetime-local" bind:value={form.start_datetime} required />
|
||||
<Input type="datetime-local" bind:value={form.end_datetime} required />
|
||||
<Select bind:value={form.recurrence_type}>
|
||||
<option value="none">No recurrence</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="date" bind:value={form.recurrence_end_date} />
|
||||
</Select>
|
||||
<Input type="date" bind:value={form.recurrence_end_date} />
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<input class="h-10 w-16 rounded-xl border border-[var(--input)] bg-white/70 px-2 py-1" type="color" bind:value={form.color} />
|
||||
<Input className="h-10 w-16 px-2 py-1" type="color" bind:value={form.color} />
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" bind:checked={form.all_day} />
|
||||
All day
|
||||
@@ -204,6 +214,7 @@
|
||||
</div>
|
||||
<Button type="submit" disabled={saving}>{saving ? "Saving…" : editingEventId ? "Update event" : "Create event"}</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Category, InventoryItem } from "$lib/types/domain";
|
||||
|
||||
@@ -42,34 +45,34 @@
|
||||
<h2 class="mt-2 text-3xl font-semibold">Inventory health</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={query} placeholder="Search product or GTIN" />
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedCategory}>
|
||||
<Input bind:value={query} placeholder="Search product or GTIN" />
|
||||
<Select bind:value={selectedCategory}>
|
||||
<option value="">All categories</option>
|
||||
{#each categories as category}
|
||||
<option value={category.id}>{category.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
<Button type="button" on:click={loadData}>Filter</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Tracked products</p><p class="mt-2 text-3xl font-semibold">{rows.length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Zero stock</p><p class="mt-2 text-3xl font-semibold">{rows.filter((row) => row.quantity_on_hand <= 0).length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Low stock</p><p class="mt-2 text-3xl font-semibold">{rows.filter((row) => row.quantity_on_hand > 0 && row.quantity_on_hand < 5).length}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Tracked products</p><p class="mt-2 text-3xl font-semibold">{rows.length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Zero stock</p><p class="mt-2 text-3xl font-semibold">{rows.filter((row) => row.quantity_on_hand <= 0).length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Low stock</p><p class="mt-2 text-3xl font-semibold">{rows.filter((row) => row.quantity_on_hand > 0 && row.quantity_on_hand < 5).length}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="space-y-3 p-5">
|
||||
{#if loading}
|
||||
<p>Loading inventory…</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each rows as row}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{row.product_name}</p>
|
||||
@@ -79,7 +82,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class={`text-2xl font-semibold ${row.quantity_on_hand <= 0 ? "text-red-700" : row.quantity_on_hand < 5 ? "text-amber-700" : ""}`}>
|
||||
<p class={`text-2xl font-semibold ${row.quantity_on_hand <= 0 ? "text-red-300" : row.quantity_on_hand < 5 ? "text-zinc-300" : ""}`}>
|
||||
{row.quantity_on_hand.toFixed(3)}
|
||||
</p>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{row.uom}</p>
|
||||
@@ -87,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { onMount } from "svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Label from "$lib/components/ui/label.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import Textarea from "$lib/components/ui/textarea.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Business, Category, Invoice, Product, Vendor } from "$lib/types/domain";
|
||||
|
||||
@@ -173,17 +177,12 @@
|
||||
<h2 class="mt-2 text-3xl font-semibold">Invoice tracker</h2>
|
||||
</div>
|
||||
<div class="w-full max-w-sm">
|
||||
<input
|
||||
class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2"
|
||||
bind:value={query}
|
||||
on:change={() => loadData(query)}
|
||||
placeholder="Search vendor, note, or invoice number"
|
||||
/>
|
||||
<Input bind:value={query} on:change={() => loadData(query)} placeholder="Search vendor, note, or invoice number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><div class="p-5"><p class="text-sm text-red-300">{error}</p></div></Card>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
@@ -203,7 +202,7 @@
|
||||
<div class="space-y-3">
|
||||
{#each invoices as invoice}
|
||||
<button
|
||||
class="w-full rounded-2xl border border-[var(--border)] bg-white/60 p-4 text-left transition hover:bg-white"
|
||||
class="w-full rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4 text-left transition hover:bg-[var(--accent)]"
|
||||
on:click={() => startInvoiceDraft(invoice)}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
@@ -239,9 +238,9 @@
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-xl bg-black/5 p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Business</p><p class="mt-1">{selectedInvoice.business_name ?? "Unassigned"}</p></div>
|
||||
<div class="rounded-xl bg-black/5 p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Status</p><p class="mt-1">{selectedInvoice.payment_status}</p></div>
|
||||
<div class="rounded-xl bg-black/5 p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Due</p><p class="mt-1">{selectedInvoice.due_date ?? "Not set"}</p></div>
|
||||
<div class="rounded-xl bg-[var(--muted)] p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Business</p><p class="mt-1">{selectedInvoice.business_name ?? "Unassigned"}</p></div>
|
||||
<div class="rounded-xl bg-[var(--muted)] p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Status</p><p class="mt-1">{selectedInvoice.payment_status}</p></div>
|
||||
<div class="rounded-xl bg-[var(--muted)] p-3"><p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Due</p><p class="mt-1">{selectedInvoice.due_date ?? "Not set"}</p></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
@@ -276,61 +275,61 @@
|
||||
<form class="space-y-4" on:submit|preventDefault={saveInvoice}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Business</label>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.business_id}>
|
||||
<Label forId="invoice-business" className="mb-2 block">Business</Label>
|
||||
<Select id="invoice-business" bind:value={form.business_id}>
|
||||
<option value="">Unassigned</option>
|
||||
{#each businesses as business}
|
||||
<option value={business.id}>{business.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Vendor</label>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.vendor_id} required>
|
||||
<Label forId="invoice-vendor" className="mb-2 block">Vendor</Label>
|
||||
<Select id="invoice-vendor" bind:value={form.vendor_id} required>
|
||||
<option value={0}>Select vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>{vendor.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Invoice number</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.invoice_number} />
|
||||
<Label forId="invoice-number" className="mb-2 block">Invoice number</Label>
|
||||
<Input id="invoice-number" bind:value={form.invoice_number} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Invoice date</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="date" bind:value={form.invoice_date} />
|
||||
<Label forId="invoice-date" className="mb-2 block">Invoice date</Label>
|
||||
<Input id="invoice-date" type="date" bind:value={form.invoice_date} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Due date</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="date" bind:value={form.due_date} />
|
||||
<Label forId="invoice-due-date" className="mb-2 block">Due date</Label>
|
||||
<Input id="invoice-due-date" type="date" bind:value={form.due_date} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Currency</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.currency} />
|
||||
<Label forId="invoice-currency" className="mb-2 block">Currency</Label>
|
||||
<Input id="invoice-currency" bind:value={form.currency} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Payment status</label>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.payment_status}>
|
||||
<Label forId="invoice-payment-status" className="mb-2 block">Payment status</Label>
|
||||
<Select id="invoice-payment-status" bind:value={form.payment_status}>
|
||||
<option value="unpaid">Unpaid</option>
|
||||
<option value="paid">Paid</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Goods received</label>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.goods_received_status}>
|
||||
<Label forId="invoice-goods-status" className="mb-2 block">Goods received</Label>
|
||||
<Select id="invoice-goods-status" bind:value={form.goods_received_status}>
|
||||
<option value="not_received">Not received</option>
|
||||
<option value="received">Received</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Categories</label>
|
||||
<p class="mb-2 text-sm font-medium">Categories</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each categories as category}
|
||||
<button
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.category_ids.includes(category.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-white/70"}`}
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.category_ids.includes(category.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-[var(--secondary)] text-[var(--muted-foreground)]"}`}
|
||||
type="button"
|
||||
on:click={() => toggleCategory(category.id)}
|
||||
>
|
||||
@@ -342,19 +341,19 @@
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="block text-sm font-medium">Line items</label>
|
||||
<p class="text-sm font-medium">Line items</p>
|
||||
<Button type="button" variant="secondary" on:click={addLine}>Add line</Button>
|
||||
</div>
|
||||
{#each form.line_items as line, index}
|
||||
<div class="grid gap-3 md:grid-cols-[1.5fr_0.7fr_0.8fr_auto]">
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={line.product_id}>
|
||||
<Select bind:value={line.product_id}>
|
||||
<option value={0}>Select product</option>
|
||||
{#each products as product}
|
||||
<option value={product.id}>{product.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="number" min="0" step="0.001" bind:value={line.quantity} />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="number" min="0" step="0.01" bind:value={line.unit_price} />
|
||||
</Select>
|
||||
<Input type="number" min="0" step="0.001" bind:value={line.quantity} />
|
||||
<Input type="number" min="0" step="0.01" bind:value={line.unit_price} />
|
||||
<div class="flex items-center justify-end text-sm text-[var(--muted-foreground)]">
|
||||
{(Number(line.quantity || 0) * Number(line.unit_price || 0)).toFixed(2)}
|
||||
</div>
|
||||
@@ -363,11 +362,11 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Notes</label>
|
||||
<textarea class="min-h-28 w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.notes}></textarea>
|
||||
<Label forId="invoice-notes" className="mb-2 block">Notes</Label>
|
||||
<Textarea id="invoice-notes" className="min-h-28" bind:value={form.notes}></Textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-2xl bg-black/5 p-4">
|
||||
<div class="flex items-center justify-between rounded-2xl bg-[var(--muted)] p-4">
|
||||
<div>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Draft subtotal</p>
|
||||
<p class="text-2xl font-semibold">{subtotal.toFixed(2)} {form.currency}</p>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Business, ScheduleOverview } from "$lib/types/domain";
|
||||
|
||||
@@ -38,54 +43,54 @@
|
||||
<h2 class="mt-2 text-3xl font-semibold">Schedule overview</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedBusiness}>
|
||||
<Select bind:value={selectedBusiness}>
|
||||
<option value="">All businesses</option>
|
||||
{#each businesses as business}
|
||||
<option value={business.id}>{business.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
<Button type="button" on:click={loadData}>Filter</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{:else if loading || !overview}
|
||||
<Card>Loading schedule…</Card>
|
||||
<Card><CardContent className="p-5">Loading schedule…</CardContent></Card>
|
||||
{:else}
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Roles</p><p class="mt-2 text-3xl font-semibold">{overview.roles.length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Templates</p><p class="mt-2 text-3xl font-semibold">{overview.templates.length}</p></Card>
|
||||
<Card><p class="text-sm text-[var(--muted-foreground)]">Assignments</p><p class="mt-2 text-3xl font-semibold">{overview.assignments.length}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Roles</p><p class="mt-2 text-3xl font-semibold">{overview.roles.length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Templates</p><p class="mt-2 text-3xl font-semibold">{overview.templates.length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-[var(--muted-foreground)]">Assignments</p><p class="mt-2 text-3xl font-semibold">{overview.assignments.length}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[0.7fr_1.2fr_1.1fr]">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Roles</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Shift role catalogue by business.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Roles</CardTitle>
|
||||
<CardDescription>Shift role catalogue by business.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each overview.roles as role}
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
|
||||
<div>
|
||||
<p class="font-medium">{role.name}</p>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{role.business_name}</p>
|
||||
</div>
|
||||
<div class="h-4 w-4 rounded-full border border-black/10" style={`background:${role.color || "#16302b"}`}></div>
|
||||
<div class="h-4 w-4 rounded-full border border-white/10" style={`background:${role.color || "#09090b"}`}></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Upcoming templates</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Recurring shift definitions and staffing targets.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Upcoming templates</CardTitle>
|
||||
<CardDescription>Recurring shift definitions and staffing targets.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each overview.templates as template}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-medium">{template.name}</p>
|
||||
@@ -102,15 +107,15 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold">Assignments</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Upcoming user allocations against templates.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Assignments</CardTitle>
|
||||
<CardDescription>Upcoming user allocations against templates.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{#each overview.assignments as assignment}
|
||||
<div class="flex items-start justify-between border-b border-[var(--border)] py-2 last:border-0">
|
||||
<div>
|
||||
@@ -124,7 +129,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Business, SettingsOverview, SettingsUser } from "$lib/types/domain";
|
||||
|
||||
@@ -132,7 +134,7 @@
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><div class="p-5"><p class="text-sm text-red-300">{error}</p></div></Card>
|
||||
{/if}
|
||||
|
||||
{#if loading || !overview}
|
||||
@@ -153,7 +155,7 @@
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{#each overview.users as user}
|
||||
<button class="w-full rounded-2xl border border-[var(--border)] bg-white/60 p-4 text-left transition hover:bg-white" on:click={() => startEditUser(user)}>
|
||||
<button class="w-full rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4 text-left transition hover:bg-[var(--accent)]" on:click={() => startEditUser(user)}>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{user.display_name || user.username}</p>
|
||||
@@ -162,7 +164,7 @@
|
||||
{user.role_name || "No role"} • {user.business_ids.length} businesses
|
||||
</p>
|
||||
</div>
|
||||
<p class={`text-sm ${user.is_active ? "text-green-700" : "text-red-700"}`}>{user.is_active ? "Active" : "Inactive"}</p>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{user.is_active ? "Active" : "Inactive"}</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -176,7 +178,7 @@
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{#each overview.roles as role}
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-white/60 p-4">
|
||||
<div class="rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium">{role.name}</p>
|
||||
@@ -208,17 +210,17 @@
|
||||
</div>
|
||||
<form class="space-y-4" on:submit|preventDefault={saveUser}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.username} placeholder="Username" required />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.password} type="password" placeholder={editingUserId ? "Leave blank to keep password" : "Password"} required={!editingUserId} />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.display_name} placeholder="Display name" />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.email} type="email" placeholder="Email" />
|
||||
<Input bind:value={form.username} placeholder="Username" required />
|
||||
<Input bind:value={form.password} type="password" placeholder={editingUserId ? "Leave blank to keep password" : "Password"} required={!editingUserId} />
|
||||
<Input bind:value={form.display_name} placeholder="Display name" />
|
||||
<Input bind:value={form.email} type="email" placeholder="Email" />
|
||||
</div>
|
||||
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.role_id}>
|
||||
<Select bind:value={form.role_id}>
|
||||
<option value="">No role</option>
|
||||
{#each overview.roles as role}
|
||||
<option value={role.id}>{role.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium">Business access</p>
|
||||
@@ -226,7 +228,7 @@
|
||||
{#each businesses as business}
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.business_ids.includes(business.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-white/70"}`}
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.business_ids.includes(business.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-[var(--secondary)] text-[var(--muted-foreground)]"}`}
|
||||
on:click={() => toggleBusiness(business.id)}
|
||||
>
|
||||
{business.name}
|
||||
|
||||
55
frontend/src/routes/app/vendors/+page.svelte
vendored
55
frontend/src/routes/app/vendors/+page.svelte
vendored
@@ -2,6 +2,13 @@
|
||||
import { onMount } from "svelte";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Select from "$lib/components/ui/select.svelte";
|
||||
import Textarea from "$lib/components/ui/textarea.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import type { Business, Category, Vendor } from "$lib/types/domain";
|
||||
|
||||
@@ -116,30 +123,31 @@
|
||||
<h2 class="mt-2 text-3xl font-semibold">Vendor management</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={query} placeholder="Search vendors" />
|
||||
<Input bind:value={query} placeholder="Search vendors" />
|
||||
<Button type="button" on:click={loadData}>Filter</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Card><p class="text-sm text-red-700">{error}</p></Card>
|
||||
<Card><CardContent className="p-5"><p class="text-sm text-red-300">{error}</p></CardContent></Card>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<Card>
|
||||
<div class="mb-4 grid gap-3 md:grid-cols-2">
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedBusiness}>
|
||||
<CardContent className="space-y-4 p-5">
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<Select bind:value={selectedBusiness}>
|
||||
<option value="">All businesses</option>
|
||||
{#each businesses as business}
|
||||
<option value={business.id}>{business.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedCategory}>
|
||||
</Select>
|
||||
<Select bind:value={selectedCategory}>
|
||||
<option value="">All categories</option>
|
||||
{#each categories as category}
|
||||
<option value={category.id}>{category.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
@@ -147,7 +155,7 @@
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each vendors as vendor}
|
||||
<button class="w-full rounded-2xl border border-[var(--border)] bg-white/60 p-4 text-left transition hover:bg-white" on:click={() => startEdit(vendor)}>
|
||||
<button class="w-full rounded-2xl border border-[var(--border)] bg-[var(--muted)] p-4 text-left transition hover:bg-[var(--accent)]" on:click={() => startEdit(vendor)}>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-medium">{vendor.name}</p>
|
||||
@@ -156,35 +164,37 @@
|
||||
{vendor.business_ids.length} businesses • {vendor.category_ids.length} categories
|
||||
</p>
|
||||
</div>
|
||||
<p class={`text-sm ${vendor.is_active ? "text-green-700" : "text-red-700"}`}>{vendor.is_active ? "Active" : "Inactive"}</p>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">{vendor.is_active ? "Active" : "Inactive"}</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<CardHeader className="flex-row items-start justify-between space-y-0">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">{editingVendorId ? "Edit vendor" : "Create vendor"}</h3>
|
||||
<p class="text-sm text-[var(--muted-foreground)]">Manage linkage to businesses and invoice categories.</p>
|
||||
<CardTitle className="text-xl">{editingVendorId ? "Edit vendor" : "Create vendor"}</CardTitle>
|
||||
<CardDescription>Manage linkage to businesses and invoice categories.</CardDescription>
|
||||
</div>
|
||||
{#if editingVendorId}
|
||||
<Button type="button" variant="ghost" on:click={resetForm}>Reset</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form class="space-y-4" on:submit|preventDefault={saveVendor}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.name} placeholder="Vendor name" required />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.vat_number} placeholder="VAT number" />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.registration_id} placeholder="Registration ID" />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.contact_email} placeholder="Email" />
|
||||
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2 md:col-span-2" bind:value={form.contact_phone} placeholder="Phone" />
|
||||
<Input bind:value={form.name} placeholder="Vendor name" required />
|
||||
<Input bind:value={form.vat_number} placeholder="VAT number" />
|
||||
<Input bind:value={form.registration_id} placeholder="Registration ID" />
|
||||
<Input bind:value={form.contact_email} placeholder="Email" />
|
||||
<Input className="md:col-span-2" bind:value={form.contact_phone} placeholder="Phone" />
|
||||
</div>
|
||||
<textarea class="min-h-20 w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.address} placeholder="Address"></textarea>
|
||||
<textarea class="min-h-24 w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.notes} placeholder="Notes"></textarea>
|
||||
<Textarea className="min-h-20" bind:value={form.address} placeholder="Address"></Textarea>
|
||||
<Textarea className="min-h-24" bind:value={form.notes} placeholder="Notes"></Textarea>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium">Businesses</p>
|
||||
@@ -192,7 +202,7 @@
|
||||
{#each businesses as business}
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.business_ids.includes(business.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-white/70"}`}
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.business_ids.includes(business.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-[var(--secondary)] text-[var(--muted-foreground)]"}`}
|
||||
on:click={() => (form.business_ids = toggleId(form.business_ids, business.id))}
|
||||
>
|
||||
{business.name}
|
||||
@@ -207,7 +217,7 @@
|
||||
{#each categories as category}
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.category_ids.includes(category.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-white/70"}`}
|
||||
class={`rounded-full border px-3 py-1 text-sm ${form.category_ids.includes(category.id) ? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]" : "border-[var(--border)] bg-[var(--secondary)] text-[var(--muted-foreground)]"}`}
|
||||
on:click={() => (form.category_ids = toggleId(form.category_ids, category.id))}
|
||||
>
|
||||
{category.name}
|
||||
@@ -225,6 +235,7 @@
|
||||
{saving ? "Saving…" : editingVendorId ? "Update vendor" : "Create vendor"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import Button from "$lib/components/ui/button.svelte";
|
||||
import Card from "$lib/components/ui/card.svelte";
|
||||
import CardContent from "$lib/components/ui/card-content.svelte";
|
||||
import CardDescription from "$lib/components/ui/card-description.svelte";
|
||||
import CardHeader from "$lib/components/ui/card-header.svelte";
|
||||
import CardTitle from "$lib/components/ui/card-title.svelte";
|
||||
import Input from "$lib/components/ui/input.svelte";
|
||||
import Label from "$lib/components/ui/label.svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import { authUser, authReady, bootstrapAuth } from "$lib/stores/auth";
|
||||
|
||||
@@ -43,41 +49,70 @@
|
||||
|
||||
{#if checkingSession}
|
||||
<div class="mx-auto flex min-h-screen max-w-6xl items-center justify-center px-6 py-10">
|
||||
<div class="panel p-6 text-center">
|
||||
<Card className="w-full max-w-md bg-[var(--secondary)] p-6 text-center">
|
||||
<p class="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">Session</p>
|
||||
<p class="mt-2 text-lg font-medium">Checking authentication…</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto flex min-h-screen max-w-6xl items-center px-6 py-10">
|
||||
<div class="grid w-full gap-8 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<div class="space-y-6">
|
||||
<div class="mx-auto flex min-h-screen max-w-7xl items-center px-6 py-10">
|
||||
<div class="grid w-full gap-10 lg:grid-cols-[1.3fr_0.7fr]">
|
||||
<div class="flex flex-col justify-center space-y-8">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm uppercase tracking-[0.35em] text-[var(--muted-foreground)]">Django + Svelte port</p>
|
||||
<h1 class="max-w-2xl text-5xl font-semibold leading-tight">
|
||||
Replace the brittle FastAPI tangle with a session-based operations platform.
|
||||
<h1 class="max-w-3xl text-5xl font-semibold leading-tight lg:text-6xl">
|
||||
Hospitality operations, rebuilt as a clean black-and-white control surface.
|
||||
</h1>
|
||||
<p class="max-w-xl text-lg text-[var(--muted-foreground)]">
|
||||
The new frontend is organized around stable domains, smaller route modules, and a backend that uses Django ORM and admin instead of runtime schema patching.
|
||||
<p class="max-w-2xl text-lg text-[var(--muted-foreground)]">
|
||||
Session auth, Django ORM, scoped business access, and domain-focused screens without the legacy FastAPI sprawl.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<form class="space-y-4" on:submit|preventDefault={submit}>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Username</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={username} />
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<Card className="bg-[var(--secondary)]">
|
||||
<CardContent className="p-5">
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Backend</p>
|
||||
<p class="mt-2 text-lg font-semibold">Django</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--secondary)]">
|
||||
<CardContent className="p-5">
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Frontend</p>
|
||||
<p class="mt-2 text-lg font-semibold">SvelteKit</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--secondary)]">
|
||||
<CardContent className="p-5">
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">UI</p>
|
||||
<p class="mt-2 text-lg font-semibold">Vanilla shadcn style</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">Password</label>
|
||||
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="password" bind:value={password} />
|
||||
</div>
|
||||
|
||||
<Card className="border-[var(--foreground)]/10 bg-[var(--secondary)]">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Sign in</CardTitle>
|
||||
<CardDescription>Use the Django session auth flow for the new operations app.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form class="space-y-4" on:submit|preventDefault={submit}>
|
||||
<div class="space-y-2">
|
||||
<Label forId="login-username">Username</Label>
|
||||
<Input id="login-username" bind:value={username} autocomplete="username" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label forId="login-password">Password</Label>
|
||||
<Input id="login-password" type="password" bind:value={password} autocomplete="current-password" />
|
||||
</div>
|
||||
{#if error}
|
||||
<p class="text-sm text-red-700">{error}</p>
|
||||
<p class="rounded-md border border-red-900 bg-red-950/40 px-3 py-2 text-sm text-red-300">{error}</p>
|
||||
{/if}
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Button type="submit" disabled={submitting} className="w-full">
|
||||
{submitting ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user