ShadCN Implementation

This commit is contained in:
sandy
2026-04-01 03:57:04 +02:00
parent d68d476482
commit 3546239396
37 changed files with 1073 additions and 320 deletions

1
.gitignore vendored
View File

@@ -13,7 +13,6 @@ venv/
env/ env/
# Django # Django
backend/db.sqlite3
backend/media/ backend/media/
backend/staticfiles/ backend/staticfiles/

View File

@@ -1,12 +1,84 @@
# Django Port Workspace # Django Port Workspace
This folder contains the rewrite target for the current FastAPI + React application. This folder contains the rewrite target for the current FastAPI + React application.
It is intended to be self-contained.
## What is here ## What is here
- [`backend`](/home/sandy/HUB-master/django-port/backend): Django project and domain apps - [`backend`](backend): Django project and domain apps
- [`frontend`](/home/sandy/HUB-master/django-port/frontend): SvelteKit frontend prepared for `shadcn-svelte` - [`frontend`](frontend): SvelteKit frontend with a local manual shadcn-style component layer
- [`docs/port-plan.md`](/home/sandy/HUB-master/django-port/docs/port-plan.md): migration strategy and scope - [`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 ## 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 Svelte or Django bootstrapping commands
- No heavy build, dev-server, or migration runs - 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
View 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

View File

@@ -14,14 +14,24 @@ This is the replacement backend for the legacy FastAPI app.
## Expected setup ## Expected setup
1. Install dependencies from [`pyproject.toml`](/home/sandy/HUB-master/django-port/backend/pyproject.toml). 1. Create a virtualenv in `backend/`.
2. Run `python manage.py makemigrations`. 2. Install dependencies from [`pyproject.toml`](pyproject.toml) with `pip install -e .`.
3. Run `python manage.py migrate`. 3. Copy [`backend/.env.example`](.env.example) to `.env` or export the same variables in your shell.
4. Run `python manage.py createsuperuser`. 4. Run `python manage.py migrate`.
5. Run `python manage.py import_legacy_data`. 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 ## Notes
- Auth uses Django sessions instead of custom JWT cookies. - Auth uses Django sessions instead of custom JWT cookies.
- The import command consolidates both legacy SQLite files into one Django schema. - 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. - 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.

View File

@@ -110,5 +110,6 @@ CORS_ALLOWED_ORIGINS = [
) )
] ]
LEGACY_CINCIN_DB = Path(os.getenv("LEGACY_CINCIN_DB", BASE_DIR.parent.parent / "cincin_phase1.sqlite")) LEGACY_DATA_DIR = BASE_DIR.parent / "data" / "legacy"
LEGACY_DALCORSO_DB = Path(os.getenv("LEGACY_DALCORSO_DB", BASE_DIR.parent.parent / "dalcorso.sqlite")) 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

Binary file not shown.

13
data/legacy/README.md Normal file
View 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`

Binary file not shown.

BIN
data/legacy/dalcorso.sqlite Normal file

Binary file not shown.

View File

@@ -21,7 +21,7 @@
1. Create Django migrations from the new model set. 1. Create Django migrations from the new model set.
2. Migrate the new database. 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. 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. 5. Move ETL scripts into Django management commands later instead of repo-root scripts.

View 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 businesss 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 users 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 users 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
View File

@@ -0,0 +1 @@
VITE_API_BASE=http://localhost:8000/api

View File

@@ -1,38 +1,50 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #f5f1e8; --background: #09090b;
--foreground: #201916; --foreground: #fafafa;
--card: #fffcf6; --card: #09090b;
--card-foreground: #201916; --card-foreground: #fafafa;
--popover: #fff9ef; --popover: #09090b;
--popover-foreground: #201916; --popover-foreground: #fafafa;
--primary: #16302b; --primary: #fafafa;
--primary-foreground: #f6efe4; --primary-foreground: #09090b;
--secondary: #d8c3a8; --secondary: #18181b;
--secondary-foreground: #2d241e; --secondary-foreground: #fafafa;
--muted: #ece2d3; --muted: #18181b;
--muted-foreground: #615246; --muted-foreground: #a1a1aa;
--accent: #ba6c46; --accent: #27272a;
--accent-foreground: #fff7ef; --accent-foreground: #fafafa;
--border: #cdbca5; --border: #27272a;
--input: #e7dbca; --input: #27272a;
--ring: #16302b; --ring: #d4d4d8;
} }
body { body {
background: background: var(--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);
color: var(--foreground); color: var(--foreground);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif; font-family: Arial, Helvetica, sans-serif;
min-height: 100vh; min-height: 100vh;
} }
.panel { * {
background: color-mix(in srgb, var(--card) 92%, white); border-color: var(--border);
border: 1px solid var(--border); box-sizing: border-box;
border-radius: 1rem; }
box-shadow: 0 18px 60px rgba(32, 25, 22, 0.08);
::selection {
background: #3f3f46;
color: var(--foreground);
}
button,
input,
select,
textarea {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
} }

View File

@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/state";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.svelte";
import { authUser, clearAuth, hasPermission } from "$lib/stores/auth"; import { authUser, clearAuth, hasPermission } from "$lib/stores/auth";
const items = [ const items = [
@@ -28,14 +30,22 @@
$: visibleItems = items.filter((item) => !$authUser || hasPermission(item.permission)); $: visibleItems = items.filter((item) => !$authUser || hasPermission(item.permission));
</script> </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"> <div class="border-b border-[var(--border)] pb-4">
<p class="text-xs uppercase tracking-[0.25em] text-[var(--muted-foreground)]">Central Hub</p> <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> <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> </div>
<nav class="flex flex-col gap-2"> <nav class="flex flex-col gap-2">
{#each visibleItems as item} {#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} {item.label}
</a> </a>
{/each} {/each}
@@ -49,4 +59,4 @@
<Button type="button" variant="ghost" on:click={signOut}>Sign out</Button> <Button type="button" variant="ghost" on:click={signOut}>Sign out</Button>
</div> </div>
</div> </div>
</aside> </Card>

View File

@@ -1,19 +1,44 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; 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 href: string | undefined = undefined;
export let type: "button" | "submit" | "reset" = "button"; 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 disabled = false;
export let className = "";
const dispatch = createEventDispatcher<{ click: MouseEvent }>(); const dispatch = createEventDispatcher<{ click: MouseEvent }>();
const variants = { const buttonVariants = cva(
primary: "bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-95", "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",
secondary: "bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-95", {
ghost: "bg-transparent text-[var(--foreground)] hover:bg-black/5", variants: {
destructive: "bg-red-700 text-white hover:bg-red-800" 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) { function handleClick(event: MouseEvent) {
dispatch("click", event); dispatch("click", event);
@@ -22,7 +47,7 @@
{#if href} {#if href}
<a <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} {href}
{...$$restProps} {...$$restProps}
on:click={handleClick} on:click={handleClick}
@@ -31,7 +56,7 @@
</a> </a>
{:else} {:else}
<button <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} {type}
{disabled} {disabled}
{...$$restProps} {...$$restProps}

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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 /> <slot />
</div> </div>

View 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}
/>

View 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>

View 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>

View 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>

View 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));
}

View File

@@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import Sidebar from "$lib/components/app-shell/sidebar.svelte"; import Sidebar from "$lib/components/app-shell/sidebar.svelte";
import Card from "$lib/components/ui/card.svelte";
import { authReady, bootstrapAuth } from "$lib/stores/auth"; import { authReady, bootstrapAuth } from "$lib/stores/auth";
let loading = true; let loading = true;
@@ -24,21 +25,21 @@
{#if loading || !$authReady} {#if loading || !$authReady}
<div class="mx-auto flex min-h-screen max-w-7xl items-center justify-center px-6 py-10"> <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="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">Session</p>
<p class="mt-2 text-lg font-medium">Checking authentication…</p> <p class="mt-2 text-lg font-medium">Checking authentication…</p>
</div> </Card>
</div> </div>
{:else if error} {:else if error}
<div class="mx-auto flex min-h-screen max-w-7xl items-center justify-center px-6 py-10"> <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 text-red-700">{error}</p> <p class="text-sm text-red-300">{error}</p>
</div> </Card>
</div> </div>
{:else} {: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 /> <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 /> <slot />
</main> </main>
</div> </div>

View File

@@ -2,6 +2,10 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from "$app/state"; import { page } from "$app/state";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { BusinessSummaryPayload } from "$lib/types/domain"; import type { BusinessSummaryPayload } from "$lib/types/domain";
@@ -19,7 +23,7 @@
<div class="space-y-6"> <div class="space-y-6">
{#if error} {#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} {:else if summary}
<div> <div>
<p class="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">{summary.business.short_code}</p> <p class="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">{summary.business.short_code}</p>
@@ -27,21 +31,21 @@
</div> </div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <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><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><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><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><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><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><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><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><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><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><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)]">Inventory items</p><p class="mt-2 text-3xl font-semibold">{summary.stats.inventory_items}</p></CardContent></Card>
</div> </div>
<div class="grid gap-6 xl:grid-cols-[1fr_1fr]"> <div class="grid gap-6 xl:grid-cols-[1fr_1fr]">
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Recent revenue</h3> <CardTitle className="text-xl">Recent revenue</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Latest imported revenue summary rows for this business.</p> <CardDescription>Latest imported revenue summary rows for this business.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each summary.recent_revenue as row} {#each summary.recent_revenue as row}
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0"> <div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
<div> <div>
@@ -54,15 +58,15 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Recent invoices</h3> <CardTitle className="text-xl">Recent invoices</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Most recent invoice activity linked to this business.</p> <CardDescription>Most recent invoice activity linked to this business.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each summary.recent_invoices as invoice} {#each summary.recent_invoices as invoice}
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0"> <div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
<div> <div>
@@ -75,10 +79,10 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
</div> </div>
{:else} {:else}
<Card>Loading business summary…</Card> <Card><CardContent className="p-5">Loading business summary…</CardContent></Card>
{/if} {/if}
</div> </div>

View File

@@ -2,6 +2,10 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { DashboardBusinessSummary, DashboardOverview } from "$lib/types/domain"; import type { DashboardBusinessSummary, DashboardOverview } from "$lib/types/domain";
@@ -30,27 +34,25 @@
</div> </div>
{#if error} {#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} {:else if overview}
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <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><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><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><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><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><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><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><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><p class="text-sm text-[var(--muted-foreground)]">Vendors</p><p class="mt-2 text-3xl font-semibold">{overview.vendor_count}</p></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><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)]">Unread notifications</p><p class="mt-2 text-3xl font-semibold">{overview.unread_notifications}</p></CardContent></Card>
</div> </div>
<Card> <Card>
<div class="mb-4 flex items-center justify-between"> <CardHeader>
<div> <CardTitle className="text-xl">Business roll-up</CardTitle>
<h3 class="text-xl font-semibold">Business roll-up</h3> <CardDescription>Per-business revenue, expenses, and invoice pressure.</CardDescription>
<p class="text-sm text-[var(--muted-foreground)]">Per-business revenue, expenses, and invoice pressure.</p> </CardHeader>
</div> <CardContent className="space-y-3">
</div>
<div class="space-y-3">
{#each businesses as business} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{business.business_name}</p> <p class="font-medium">{business.business_name}</p>
@@ -66,9 +68,9 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
{:else} {:else}
<Card>Loading dashboard…</Card> <Card><CardContent className="p-5">Loading dashboard…</CardContent></Card>
{/if} {/if}
</div> </div>

View File

@@ -2,6 +2,11 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { DeviceItem, DeviceRegistrationTokenItem } from "$lib/types/domain"; import type { DeviceItem, DeviceRegistrationTokenItem } from "$lib/types/domain";
@@ -112,28 +117,28 @@
</div> </div>
{#if error} {#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}
{#if loading} {#if loading}
<Card>Loading devices…</Card> <Card><CardContent className="p-5">Loading devices…</CardContent></Card>
{:else} {:else}
<div class="grid gap-4 md:grid-cols-3"> <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><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><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><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><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)]">Registration tokens</p><p class="mt-2 text-3xl font-semibold">{tokens.length}</p></CardContent></Card>
</div> </div>
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]"> <div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<div class="space-y-6"> <div class="space-y-6">
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Whitelisted devices</h3> <CardTitle className="text-xl">Whitelisted devices</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Imported device/IP allowlist plus new Django-managed entries.</p> <CardDescription>Imported device/IP allowlist plus new Django-managed entries.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each devices as device} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{device.label || device.ip_address}</p> <p class="font-medium">{device.label || device.ip_address}</p>
@@ -143,65 +148,69 @@
</p> </p>
</div> </div>
<div class="text-right"> <div class="text-right">
<p class={`text-sm ${device.is_active ? "text-green-700" : "text-red-700"}`}>{device.is_active ? "Active" : "Inactive"}</p> <p class="text-sm text-[var(--muted-foreground)]">{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> <button class="mt-2 text-sm text-red-300 hover:text-red-200" on:click={() => removeDevice(device.id)}>Delete</button>
</div> </div>
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Registration tokens</h3> <CardTitle className="text-xl">Registration tokens</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Manual tokens for controlled device onboarding.</p> <CardDescription>Manual tokens for controlled device onboarding.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each tokens as token} {#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="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 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)]"> <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"} Expires {new Date(token.expires_at).toLocaleString()} {token.used_at ? ` used ${new Date(token.used_at).toLocaleString()}` : "• unused"}
</p> </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> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
</div> </div>
<div class="space-y-6"> <div class="space-y-6">
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Add device</h3> <CardTitle className="text-xl">Add device</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Create a whitelist entry directly in the new backend.</p> <CardDescription>Create a whitelist entry directly in the new backend.</CardDescription>
</div> </CardHeader>
<CardContent>
<form class="space-y-4" on:submit|preventDefault={saveDevice}> <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 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 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 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 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.known_ips} placeholder="Known IPs, comma separated" />
<label class="flex items-center gap-2 text-sm"> <label class="flex items-center gap-2 text-sm">
<input type="checkbox" bind:checked={deviceForm.is_active} /> <input type="checkbox" bind:checked={deviceForm.is_active} />
Active device Active device
</label> </label>
<Button type="submit" disabled={savingDevice}>{savingDevice ? "Saving…" : "Add device"}</Button> <Button type="submit" disabled={savingDevice}>{savingDevice ? "Saving…" : "Add device"}</Button>
</form> </form>
</CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Create registration token</h3> <CardTitle className="text-xl">Create registration token</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Generate a fresh onboarding token with an expiry window.</p> <CardDescription>Generate a fresh onboarding token with an expiry window.</CardDescription>
</div> </CardHeader>
<CardContent>
<form class="space-y-4" on:submit|preventDefault={saveToken}> <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 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 type="number" min="1" bind:value={tokenForm.expires_in_days} />
<Button type="submit" disabled={savingToken}>{savingToken ? "Saving…" : "Create token"}</Button> <Button type="submit" disabled={savingToken}>{savingToken ? "Saving…" : "Create token"}</Button>
</form> </form>
</CardContent>
</Card> </Card>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,13 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { Business, EventItem } from "$lib/types/domain"; import type { Business, EventItem } from "$lib/types/domain";
@@ -22,7 +29,7 @@
end_datetime: "", end_datetime: "",
all_day: false, all_day: false,
location: "", location: "",
color: "#ba6c46", color: "#09090b",
recurrence_type: "none", recurrence_type: "none",
recurrence_end_date: "" recurrence_end_date: ""
}; };
@@ -70,7 +77,7 @@
end_datetime: "", end_datetime: "",
all_day: false, all_day: false,
location: "", location: "",
color: "#ba6c46", color: "#09090b",
recurrence_type: "none", recurrence_type: "none",
recurrence_end_date: "" recurrence_end_date: ""
}; };
@@ -94,7 +101,7 @@
end_datetime: event.end_datetime.slice(0, 16), end_datetime: event.end_datetime.slice(0, 16),
all_day: event.all_day, all_day: event.all_day,
location: event.location, location: event.location,
color: event.color || "#ba6c46", color: event.color || "#09090b",
recurrence_type: event.recurrence_type, recurrence_type: event.recurrence_type,
recurrence_end_date: event.recurrence_end_date || "" recurrence_end_date: event.recurrence_end_date || ""
}; };
@@ -119,32 +126,33 @@
<h2 class="mt-2 text-3xl font-semibold">Event planner</h2> <h2 class="mt-2 text-3xl font-semibold">Event planner</h2>
</div> </div>
<div class="flex gap-2"> <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> <option value="">All businesses</option>
{#each businesses as business} {#each businesses as business}
<option value={business.id}>{business.name}</option> <option value={business.id}>{business.name}</option>
{/each} {/each}
</select> </Select>
<Button type="button" on:click={loadData}>Filter</Button> <Button type="button" on:click={loadData}>Filter</Button>
</div> </div>
</div> </div>
{#if error} {#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}
<div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]"> <div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Upcoming events</h3> <CardTitle className="text-xl">Upcoming events</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Imported events plus newly created Django-side events.</p> <CardDescription>Imported events plus newly created Django-side events.</CardDescription>
</div> </CardHeader>
<CardContent className="space-y-3">
{#if loading} {#if loading}
<p>Loading events…</p> <p>Loading events…</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each events as event} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{event.title}</p> <p class="font-medium">{event.title}</p>
@@ -154,49 +162,51 @@
</div> </div>
<div class="flex items-center gap-2"> <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-[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> <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-black/10" style={`background:${event.color || "#ba6c46"}`}></div> <div class="h-4 w-4 rounded-full border border-white/10" style={`background:${event.color || "#09090b"}`}></div>
</div> </div>
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/if}
</CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Create event</h3> <CardTitle className="text-xl">{editingEventId ? "Edit event" : "Create event"}</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">{editingEventId ? "Update an existing event." : "A lean replacement for the old calendar modal stack."}</p> <CardDescription>{editingEventId ? "Update an existing event." : "A lean replacement for the old calendar modal stack."}</CardDescription>
</div> </CardHeader>
<CardContent>
<form class="space-y-4" on:submit|preventDefault={saveEvent}> <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} {#each businesses as business}
<option value={business.id}>{business.name}</option> <option value={business.id}>{business.name}</option>
{/each} {/each}
</select> </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 /> <Input 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> <Textarea className="min-h-24" bind:value={form.description} placeholder="Description"></Textarea>
<div class="grid gap-4 md:grid-cols-2"> <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="other">Other</option>
<option value="service">Service</option> <option value="service">Service</option>
<option value="private">Private</option> <option value="private">Private</option>
<option value="maintenance">Maintenance</option> <option value="maintenance">Maintenance</option>
</select> </Select>
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.location} placeholder="Location" /> <Input 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 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 /> <Input 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 bind:value={form.recurrence_type}>
<option value="none">No recurrence</option> <option value="none">No recurrence</option>
<option value="daily">Daily</option> <option value="daily">Daily</option>
<option value="weekly">Weekly</option> <option value="weekly">Weekly</option>
<option value="monthly">Monthly</option> <option value="monthly">Monthly</option>
</select> </Select>
<input class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" type="date" bind:value={form.recurrence_end_date} /> <Input type="date" bind:value={form.recurrence_end_date} />
</div> </div>
<div class="flex items-center gap-3"> <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"> <label class="flex items-center gap-2 text-sm">
<input type="checkbox" bind:checked={form.all_day} /> <input type="checkbox" bind:checked={form.all_day} />
All day All day
@@ -204,6 +214,7 @@
</div> </div>
<Button type="submit" disabled={saving}>{saving ? "Saving…" : editingEventId ? "Update event" : "Create event"}</Button> <Button type="submit" disabled={saving}>{saving ? "Saving…" : editingEventId ? "Update event" : "Create event"}</Button>
</form> </form>
</CardContent>
</Card> </Card>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,9 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { Category, InventoryItem } from "$lib/types/domain"; import type { Category, InventoryItem } from "$lib/types/domain";
@@ -42,34 +45,34 @@
<h2 class="mt-2 text-3xl font-semibold">Inventory health</h2> <h2 class="mt-2 text-3xl font-semibold">Inventory health</h2>
</div> </div>
<div class="flex gap-2"> <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" /> <Input 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}> <Select bind:value={selectedCategory}>
<option value="">All categories</option> <option value="">All categories</option>
{#each categories as category} {#each categories as category}
<option value={category.id}>{category.name}</option> <option value={category.id}>{category.name}</option>
{/each} {/each}
</select> </Select>
<Button type="button" on:click={loadData}>Filter</Button> <Button type="button" on:click={loadData}>Filter</Button>
</div> </div>
</div> </div>
{#if error} {#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}
<div class="grid gap-4 md:grid-cols-3"> <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><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><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><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><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)]">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> </div>
<Card> <Card>
<CardContent className="space-y-3 p-5">
{#if loading} {#if loading}
<p>Loading inventory…</p> <p>Loading inventory…</p>
{:else} {:else}
<div class="space-y-3">
{#each rows as row} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{row.product_name}</p> <p class="font-medium">{row.product_name}</p>
@@ -79,7 +82,7 @@
</p> </p>
</div> </div>
<div class="text-right"> <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)} {row.quantity_on_hand.toFixed(3)}
</p> </p>
<p class="text-sm text-[var(--muted-foreground)]">{row.uom}</p> <p class="text-sm text-[var(--muted-foreground)]">{row.uom}</p>
@@ -87,7 +90,7 @@
</div> </div>
</div> </div>
{/each} {/each}
</div>
{/if} {/if}
</CardContent>
</Card> </Card>
</div> </div>

View File

@@ -2,6 +2,10 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Card from "$lib/components/ui/card.svelte"; import Card from "$lib/components/ui/card.svelte";
import Button from "$lib/components/ui/button.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 { api } from "$lib/api/client";
import type { Business, Category, Invoice, Product, Vendor } from "$lib/types/domain"; 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> <h2 class="mt-2 text-3xl font-semibold">Invoice tracker</h2>
</div> </div>
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<input <Input bind:value={query} on:change={() => loadData(query)} placeholder="Search vendor, note, or invoice number" />
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"
/>
</div> </div>
</div> </div>
{#if error} {#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}
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]"> <div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
@@ -203,7 +202,7 @@
<div class="space-y-3"> <div class="space-y-3">
{#each invoices as invoice} {#each invoices as invoice}
<button <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)} on:click={() => startInvoiceDraft(invoice)}
> >
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
@@ -239,9 +238,9 @@
</div> </div>
<div class="grid gap-3 md:grid-cols-3"> <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-[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-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-[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-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)]">Due</p><p class="mt-1">{selectedInvoice.due_date ?? "Not set"}</p></div>
</div> </div>
<div class="mt-4 space-y-2"> <div class="mt-4 space-y-2">
@@ -276,61 +275,61 @@
<form class="space-y-4" on:submit|preventDefault={saveInvoice}> <form class="space-y-4" on:submit|preventDefault={saveInvoice}>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<div> <div>
<label class="mb-2 block text-sm font-medium">Business</label> <Label forId="invoice-business" className="mb-2 block">Business</Label>
<select class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.business_id}> <Select id="invoice-business" bind:value={form.business_id}>
<option value="">Unassigned</option> <option value="">Unassigned</option>
{#each businesses as business} {#each businesses as business}
<option value={business.id}>{business.name}</option> <option value={business.id}>{business.name}</option>
{/each} {/each}
</select> </Select>
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Vendor</label> <Label forId="invoice-vendor" className="mb-2 block">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> <Select id="invoice-vendor" bind:value={form.vendor_id} required>
<option value={0}>Select vendor</option> <option value={0}>Select vendor</option>
{#each vendors as vendor} {#each vendors as vendor}
<option value={vendor.id}>{vendor.name}</option> <option value={vendor.id}>{vendor.name}</option>
{/each} {/each}
</select> </Select>
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Invoice number</label> <Label forId="invoice-number" className="mb-2 block">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} /> <Input id="invoice-number" bind:value={form.invoice_number} />
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Invoice date</label> <Label forId="invoice-date" className="mb-2 block">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} /> <Input id="invoice-date" type="date" bind:value={form.invoice_date} />
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Due date</label> <Label forId="invoice-due-date" className="mb-2 block">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} /> <Input id="invoice-due-date" type="date" bind:value={form.due_date} />
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Currency</label> <Label forId="invoice-currency" className="mb-2 block">Currency</Label>
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={form.currency} /> <Input id="invoice-currency" bind:value={form.currency} />
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Payment status</label> <Label forId="invoice-payment-status" className="mb-2 block">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}> <Select id="invoice-payment-status" bind:value={form.payment_status}>
<option value="unpaid">Unpaid</option> <option value="unpaid">Unpaid</option>
<option value="paid">Paid</option> <option value="paid">Paid</option>
</select> </Select>
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Goods received</label> <Label forId="invoice-goods-status" className="mb-2 block">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}> <Select id="invoice-goods-status" bind:value={form.goods_received_status}>
<option value="not_received">Not received</option> <option value="not_received">Not received</option>
<option value="received">Received</option> <option value="received">Received</option>
</select> </Select>
</div> </div>
</div> </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"> <div class="flex flex-wrap gap-2">
{#each categories as category} {#each categories as category}
<button <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" type="button"
on:click={() => toggleCategory(category.id)} on:click={() => toggleCategory(category.id)}
> >
@@ -342,19 +341,19 @@
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center justify-between"> <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> <Button type="button" variant="secondary" on:click={addLine}>Add line</Button>
</div> </div>
{#each form.line_items as line, index} {#each form.line_items as line, index}
<div class="grid gap-3 md:grid-cols-[1.5fr_0.7fr_0.8fr_auto]"> <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> <option value={0}>Select product</option>
{#each products as product} {#each products as product}
<option value={product.id}>{product.name}</option> <option value={product.id}>{product.name}</option>
{/each} {/each}
</select> </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 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} /> <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)]"> <div class="flex items-center justify-end text-sm text-[var(--muted-foreground)]">
{(Number(line.quantity || 0) * Number(line.unit_price || 0)).toFixed(2)} {(Number(line.quantity || 0) * Number(line.unit_price || 0)).toFixed(2)}
</div> </div>
@@ -363,11 +362,11 @@
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-medium">Notes</label> <Label forId="invoice-notes" className="mb-2 block">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> <Textarea id="invoice-notes" className="min-h-28" bind:value={form.notes}></Textarea>
</div> </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> <div>
<p class="text-sm text-[var(--muted-foreground)]">Draft subtotal</p> <p class="text-sm text-[var(--muted-foreground)]">Draft subtotal</p>
<p class="text-2xl font-semibold">{subtotal.toFixed(2)} {form.currency}</p> <p class="text-2xl font-semibold">{subtotal.toFixed(2)} {form.currency}</p>

View File

@@ -2,6 +2,11 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { Business, ScheduleOverview } from "$lib/types/domain"; import type { Business, ScheduleOverview } from "$lib/types/domain";
@@ -38,54 +43,54 @@
<h2 class="mt-2 text-3xl font-semibold">Schedule overview</h2> <h2 class="mt-2 text-3xl font-semibold">Schedule overview</h2>
</div> </div>
<div class="flex gap-2"> <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> <option value="">All businesses</option>
{#each businesses as business} {#each businesses as business}
<option value={business.id}>{business.name}</option> <option value={business.id}>{business.name}</option>
{/each} {/each}
</select> </Select>
<Button type="button" on:click={loadData}>Filter</Button> <Button type="button" on:click={loadData}>Filter</Button>
</div> </div>
</div> </div>
{#if error} {#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} {:else if loading || !overview}
<Card>Loading schedule…</Card> <Card><CardContent className="p-5">Loading schedule…</CardContent></Card>
{:else} {:else}
<div class="grid gap-4 md:grid-cols-3"> <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><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><p class="text-sm text-[var(--muted-foreground)]">Templates</p><p class="mt-2 text-3xl font-semibold">{overview.templates.length}</p></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><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)]">Assignments</p><p class="mt-2 text-3xl font-semibold">{overview.assignments.length}</p></CardContent></Card>
</div> </div>
<div class="grid gap-6 xl:grid-cols-[0.7fr_1.2fr_1.1fr]"> <div class="grid gap-6 xl:grid-cols-[0.7fr_1.2fr_1.1fr]">
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Roles</h3> <CardTitle className="text-xl">Roles</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Shift role catalogue by business.</p> <CardDescription>Shift role catalogue by business.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each overview.roles as role} {#each overview.roles as role}
<div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0"> <div class="flex items-center justify-between border-b border-[var(--border)] py-2 last:border-0">
<div> <div>
<p class="font-medium">{role.name}</p> <p class="font-medium">{role.name}</p>
<p class="text-sm text-[var(--muted-foreground)]">{role.business_name}</p> <p class="text-sm text-[var(--muted-foreground)]">{role.business_name}</p>
</div> </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> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Upcoming templates</h3> <CardTitle className="text-xl">Upcoming templates</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Recurring shift definitions and staffing targets.</p> <CardDescription>Recurring shift definitions and staffing targets.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each overview.templates as template} {#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 class="flex items-start justify-between gap-3">
<div> <div>
<p class="font-medium">{template.name}</p> <p class="font-medium">{template.name}</p>
@@ -102,15 +107,15 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4"> <CardHeader>
<h3 class="text-xl font-semibold">Assignments</h3> <CardTitle className="text-xl">Assignments</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Upcoming user allocations against templates.</p> <CardDescription>Upcoming user allocations against templates.</CardDescription>
</div> </CardHeader>
<div class="space-y-3"> <CardContent className="space-y-3">
{#each overview.assignments as assignment} {#each overview.assignments as assignment}
<div class="flex items-start justify-between border-b border-[var(--border)] py-2 last:border-0"> <div class="flex items-start justify-between border-b border-[var(--border)] py-2 last:border-0">
<div> <div>
@@ -124,7 +129,7 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </CardContent>
</Card> </Card>
</div> </div>
{/if} {/if}

View File

@@ -2,6 +2,8 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { Business, SettingsOverview, SettingsUser } from "$lib/types/domain"; import type { Business, SettingsOverview, SettingsUser } from "$lib/types/domain";
@@ -132,7 +134,7 @@
</div> </div>
{#if error} {#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}
{#if loading || !overview} {#if loading || !overview}
@@ -153,7 +155,7 @@
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
{#each overview.users as user} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{user.display_name || user.username}</p> <p class="font-medium">{user.display_name || user.username}</p>
@@ -162,7 +164,7 @@
{user.role_name || "No role"} • {user.business_ids.length} businesses {user.role_name || "No role"} • {user.business_ids.length} businesses
</p> </p>
</div> </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> </div>
</button> </button>
{/each} {/each}
@@ -176,7 +178,7 @@
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
{#each overview.roles as role} {#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 class="flex items-start justify-between gap-4">
<div> <div>
<p class="font-medium">{role.name}</p> <p class="font-medium">{role.name}</p>
@@ -208,17 +210,17 @@
</div> </div>
<form class="space-y-4" on:submit|preventDefault={saveUser}> <form class="space-y-4" on:submit|preventDefault={saveUser}>
<div class="grid gap-4 md:grid-cols-2"> <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 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 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 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.email} type="email" placeholder="Email" />
</div> </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> <option value="">No role</option>
{#each overview.roles as role} {#each overview.roles as role}
<option value={role.id}>{role.name}</option> <option value={role.id}>{role.name}</option>
{/each} {/each}
</select> </Select>
<div> <div>
<p class="mb-2 text-sm font-medium">Business access</p> <p class="mb-2 text-sm font-medium">Business access</p>
@@ -226,7 +228,7 @@
{#each businesses as business} {#each businesses as business}
<button <button
type="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)} on:click={() => toggleBusiness(business.id)}
> >
{business.name} {business.name}

View File

@@ -2,6 +2,13 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import type { Business, Category, Vendor } from "$lib/types/domain"; import type { Business, Category, Vendor } from "$lib/types/domain";
@@ -116,30 +123,31 @@
<h2 class="mt-2 text-3xl font-semibold">Vendor management</h2> <h2 class="mt-2 text-3xl font-semibold">Vendor management</h2>
</div> </div>
<div class="flex gap-2"> <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> <Button type="button" on:click={loadData}>Filter</Button>
</div> </div>
</div> </div>
{#if error} {#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}
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]"> <div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<Card> <Card>
<div class="mb-4 grid gap-3 md:grid-cols-2"> <CardContent className="space-y-4 p-5">
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedBusiness}> <div class="grid gap-3 md:grid-cols-2">
<Select bind:value={selectedBusiness}>
<option value="">All businesses</option> <option value="">All businesses</option>
{#each businesses as business} {#each businesses as business}
<option value={business.id}>{business.name}</option> <option value={business.id}>{business.name}</option>
{/each} {/each}
</select> </Select>
<select class="rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={selectedCategory}> <Select bind:value={selectedCategory}>
<option value="">All categories</option> <option value="">All categories</option>
{#each categories as category} {#each categories as category}
<option value={category.id}>{category.name}</option> <option value={category.id}>{category.name}</option>
{/each} {/each}
</select> </Select>
</div> </div>
{#if loading} {#if loading}
@@ -147,7 +155,7 @@
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each vendors as vendor} {#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 class="flex items-start justify-between gap-3">
<div> <div>
<p class="font-medium">{vendor.name}</p> <p class="font-medium">{vendor.name}</p>
@@ -156,35 +164,37 @@
{vendor.business_ids.length} businesses • {vendor.category_ids.length} categories {vendor.business_ids.length} businesses • {vendor.category_ids.length} categories
</p> </p>
</div> </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> </div>
</button> </button>
{/each} {/each}
</div> </div>
{/if} {/if}
</CardContent>
</Card> </Card>
<Card> <Card>
<div class="mb-4 flex items-center justify-between"> <CardHeader className="flex-row items-start justify-between space-y-0">
<div> <div>
<h3 class="text-xl font-semibold">{editingVendorId ? "Edit vendor" : "Create vendor"}</h3> <CardTitle className="text-xl">{editingVendorId ? "Edit vendor" : "Create vendor"}</CardTitle>
<p class="text-sm text-[var(--muted-foreground)]">Manage linkage to businesses and invoice categories.</p> <CardDescription>Manage linkage to businesses and invoice categories.</CardDescription>
</div> </div>
{#if editingVendorId} {#if editingVendorId}
<Button type="button" variant="ghost" on:click={resetForm}>Reset</Button> <Button type="button" variant="ghost" on:click={resetForm}>Reset</Button>
{/if} {/if}
</div> </CardHeader>
<CardContent>
<form class="space-y-4" on:submit|preventDefault={saveVendor}> <form class="space-y-4" on:submit|preventDefault={saveVendor}>
<div class="grid gap-4 md:grid-cols-2"> <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 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 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 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 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 className="md:col-span-2" bind:value={form.contact_phone} placeholder="Phone" />
</div> </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 className="min-h-20" 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-24" bind:value={form.notes} placeholder="Notes"></Textarea>
<div> <div>
<p class="mb-2 text-sm font-medium">Businesses</p> <p class="mb-2 text-sm font-medium">Businesses</p>
@@ -192,7 +202,7 @@
{#each businesses as business} {#each businesses as business}
<button <button
type="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))} on:click={() => (form.business_ids = toggleId(form.business_ids, business.id))}
> >
{business.name} {business.name}
@@ -207,7 +217,7 @@
{#each categories as category} {#each categories as category}
<button <button
type="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))} on:click={() => (form.category_ids = toggleId(form.category_ids, category.id))}
> >
{category.name} {category.name}
@@ -225,6 +235,7 @@
{saving ? "Saving…" : editingVendorId ? "Update vendor" : "Create vendor"} {saving ? "Saving…" : editingVendorId ? "Update vendor" : "Create vendor"}
</Button> </Button>
</form> </form>
</CardContent>
</Card> </Card>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,12 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import Button from "$lib/components/ui/button.svelte"; import Button from "$lib/components/ui/button.svelte";
import Card from "$lib/components/ui/card.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 { api } from "$lib/api/client";
import { authUser, authReady, bootstrapAuth } from "$lib/stores/auth"; import { authUser, authReady, bootstrapAuth } from "$lib/stores/auth";
@@ -43,41 +49,70 @@
{#if checkingSession} {#if checkingSession}
<div class="mx-auto flex min-h-screen max-w-6xl items-center justify-center px-6 py-10"> <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="text-sm uppercase tracking-[0.3em] text-[var(--muted-foreground)]">Session</p>
<p class="mt-2 text-lg font-medium">Checking authentication…</p> <p class="mt-2 text-lg font-medium">Checking authentication…</p>
</div> </Card>
</div> </div>
{:else} {:else}
<div class="mx-auto flex min-h-screen max-w-6xl items-center px-6 py-10"> <div class="mx-auto flex min-h-screen max-w-7xl items-center px-6 py-10">
<div class="grid w-full gap-8 lg:grid-cols-[1.2fr_0.8fr]"> <div class="grid w-full gap-10 lg:grid-cols-[1.3fr_0.7fr]">
<div class="space-y-6"> <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> <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"> <h1 class="max-w-3xl text-5xl font-semibold leading-tight lg:text-6xl">
Replace the brittle FastAPI tangle with a session-based operations platform. Hospitality operations, rebuilt as a clean black-and-white control surface.
</h1> </h1>
<p class="max-w-xl text-lg text-[var(--muted-foreground)]"> <p class="max-w-2xl 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. Session auth, Django ORM, scoped business access, and domain-focused screens without the legacy FastAPI sprawl.
</p> </p>
</div> </div>
<Card> <div class="grid gap-4 sm:grid-cols-3">
<form class="space-y-4" on:submit|preventDefault={submit}> <Card className="bg-[var(--secondary)]">
<div> <CardContent className="p-5">
<label class="mb-2 block text-sm font-medium">Username</label> <p class="text-xs uppercase tracking-[0.2em] text-[var(--muted-foreground)]">Backend</p>
<input class="w-full rounded-xl border border-[var(--input)] bg-white/70 px-3 py-2" bind:value={username} /> <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>
<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} /> <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> </div>
{#if error} {#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} {/if}
<Button type="submit" disabled={submitting}> <Button type="submit" disabled={submitting} className="w-full">
{submitting ? "Signing in..." : "Sign in"} {submitting ? "Signing in..." : "Sign in"}
</Button> </Button>
</form> </form>
</CardContent>
</Card> </Card>
</div> </div>
</div> </div>