styling fix and readme files reorg
This commit is contained in:
parent
31eda793ab
commit
11f23eb643
173
.github/copilot-instructions.md
vendored
173
.github/copilot-instructions.md
vendored
@ -7,40 +7,142 @@ This is a full-stack grocery list management app with **role-based access contro
|
|||||||
- **Frontend**: React 19 + TypeScript + Vite (port 3000/5173)
|
- **Frontend**: React 19 + TypeScript + Vite (port 3000/5173)
|
||||||
- **Deployment**: Docker Compose with separate dev/prod configurations
|
- **Deployment**: Docker Compose with separate dev/prod configurations
|
||||||
|
|
||||||
|
## Mobile-First Design Principles
|
||||||
|
|
||||||
|
**CRITICAL**: All UI components MUST be designed for both mobile and desktop from the start.
|
||||||
|
|
||||||
|
**Responsive Design Requirements**:
|
||||||
|
- Use relative units (`rem`, `em`, `%`, `vh/vw`) over fixed pixels where possible
|
||||||
|
- Implement mobile breakpoints: `480px`, `768px`, `1024px`
|
||||||
|
- Test layouts at: 320px (small phone), 375px (phone), 768px (tablet), 1024px+ (desktop)
|
||||||
|
- Avoid horizontal scrolling on mobile devices
|
||||||
|
- Touch targets minimum 44x44px for mobile usability
|
||||||
|
- Use `max-width` with `margin: 0 auto` for content containers
|
||||||
|
- Stack elements vertically on mobile, use flexbox/grid for larger screens
|
||||||
|
- Hide/collapse navigation into hamburger menus on mobile
|
||||||
|
- Ensure modals/dropdowns work well on small screens
|
||||||
|
|
||||||
|
**Common Patterns**:
|
||||||
|
```css
|
||||||
|
/* Mobile-first approach */
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Key Design Patterns
|
### Key Design Patterns
|
||||||
|
|
||||||
**Three-tier RBAC system** (`viewer`, `editor`, `admin`):
|
**Dual RBAC System** - Two separate role hierarchies:
|
||||||
- `viewer`: Read-only access to grocery lists
|
|
||||||
- `editor`: Can add items and mark as bought
|
**1. System Roles** (users.role column):
|
||||||
- `admin`: Full user management via admin panel
|
- `system_admin`: Access to Admin Panel for system-wide management (stores, users)
|
||||||
- Roles defined in [backend/models/user.model.js](backend/models/user.model.js) and mirrored in [frontend/src/constants/roles.js](frontend/src/constants/roles.js)
|
- `user`: Regular system user (default for new registrations)
|
||||||
|
- Defined in [backend/models/user.model.js](backend/models/user.model.js)
|
||||||
|
- Used for Admin Panel access control
|
||||||
|
|
||||||
|
**2. Household Roles** (household_members.role column):
|
||||||
|
- `admin`: Can manage household members, change roles, delete household
|
||||||
|
- `user`: Can add/edit items, mark as bought (standard member permissions)
|
||||||
|
- Defined per household membership
|
||||||
|
- Used for household-level permissions (item management, member management)
|
||||||
|
|
||||||
|
**Important**: Always distinguish between system role and household role:
|
||||||
|
- **System role**: From `AuthContext` or `req.user.role` - controls Admin Panel access
|
||||||
|
- **Household role**: From `activeHousehold.role` or `household_members.role` - controls household operations
|
||||||
|
|
||||||
**Middleware chain pattern** for protected routes:
|
**Middleware chain pattern** for protected routes:
|
||||||
```javascript
|
```javascript
|
||||||
router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem);
|
// System-level protection
|
||||||
|
router.get("/stores", auth, requireRole("system_admin"), controller.getAllStores);
|
||||||
|
|
||||||
|
// Household-level checks done in controller
|
||||||
|
router.post("/lists/:householdId/items", auth, controller.addItem);
|
||||||
```
|
```
|
||||||
- `auth` middleware extracts JWT from `Authorization: Bearer <token>` header
|
- `auth` middleware extracts JWT from `Authorization: Bearer <token>` header
|
||||||
- `requireRole` checks if user's role matches allowed roles
|
- `requireRole` checks system role only
|
||||||
- See [backend/routes/list.routes.js](backend/routes/list.routes.js) for examples
|
- Household role checks happen in controllers using `household.model.js` methods
|
||||||
|
|
||||||
**Frontend route protection**:
|
**Frontend route protection**:
|
||||||
- `<PrivateRoute>`: Requires authentication, redirects to `/login` if no token
|
- `<PrivateRoute>`: Requires authentication, redirects to `/login` if no token
|
||||||
- `<RoleGuard allowed={[ROLES.ADMIN]}>`: Requires specific role(s), redirects to `/` if unauthorized
|
- `<RoleGuard allowed={[ROLES.SYSTEM_ADMIN]}>`: Requires system_admin role for Admin Panel
|
||||||
|
- Household permissions: Check `activeHousehold.role` in components (not route-level)
|
||||||
- Example in [frontend/src/App.jsx](frontend/src/App.jsx)
|
- Example in [frontend/src/App.jsx](frontend/src/App.jsx)
|
||||||
|
|
||||||
|
**Multi-Household Architecture**:
|
||||||
|
- Users can belong to multiple households
|
||||||
|
- Each household has its own grocery lists, stores, and item classifications
|
||||||
|
- `HouseholdContext` manages active household selection
|
||||||
|
- All list operations are scoped to the active household
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
**PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables.
|
**PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables.
|
||||||
|
|
||||||
**Tables** (inferred from models, no formal migrations):
|
**Core Tables**:
|
||||||
- **users**: `id`, `username`, `password` (bcrypt hashed), `name`, `role`
|
|
||||||
- **grocery_list**: `id`, `item_name`, `quantity`, `bought`, `added_by`
|
**users** - System users
|
||||||
- **grocery_history**: Junction table tracking which users added which items
|
- `id` (PK), `username`, `password` (bcrypt), `name`, `display_name`
|
||||||
|
- `role`: `system_admin` | `user` (default: `viewer` - legacy)
|
||||||
|
- System-level authentication and authorization
|
||||||
|
|
||||||
|
**households** - Household entities
|
||||||
|
- `id` (PK), `name`, `invite_code`, `created_by`, `created_at`
|
||||||
|
- Each household is independent with own lists and members
|
||||||
|
|
||||||
|
**household_members** - Junction table (users ↔ households)
|
||||||
|
- `id` (PK), `household_id` (FK), `user_id` (FK), `role`, `joined_at`
|
||||||
|
- `role`: `admin` | `user` (household-level permissions)
|
||||||
|
- One user can belong to multiple households with different roles
|
||||||
|
|
||||||
|
**items** - Master item catalog
|
||||||
|
- `id` (PK), `name`, `default_image`, `default_image_mime_type`, `usage_count`
|
||||||
|
- Shared across all households, case-insensitive unique names
|
||||||
|
|
||||||
|
**stores** - Store definitions (system-wide)
|
||||||
|
- `id` (PK), `name`, `default_zones` (JSONB array)
|
||||||
|
- Managed by system_admin in Admin Panel
|
||||||
|
|
||||||
|
**household_stores** - Stores available to each household
|
||||||
|
- `id` (PK), `household_id` (FK), `store_id` (FK), `is_default`
|
||||||
|
- Links households to stores they use
|
||||||
|
|
||||||
|
**household_lists** - Grocery list items per household
|
||||||
|
- `id` (PK), `household_id` (FK), `store_id` (FK), `item_id` (FK)
|
||||||
|
- `quantity`, `bought`, `custom_image`, `custom_image_mime_type`
|
||||||
|
- `added_by`, `modified_on`
|
||||||
|
- Scoped to household + store combination
|
||||||
|
|
||||||
|
**household_list_history** - Tracks quantity contributions
|
||||||
|
- `id` (PK), `household_list_id` (FK), `quantity`, `added_by`, `added_on`
|
||||||
|
- Multi-contributor tracking (who added how much)
|
||||||
|
|
||||||
|
**household_item_classifications** - Item classifications per household/store
|
||||||
|
- `id` (PK), `household_id`, `store_id`, `item_id`
|
||||||
|
- `item_type`, `item_group`, `zone`, `confidence`, `source`
|
||||||
|
- Household-specific overrides of global classifications
|
||||||
|
|
||||||
|
**item_classification** - Global item classifications
|
||||||
|
- `id` (PK), `item_type`, `item_group`, `zone`, `confidence`, `source`
|
||||||
|
- System-wide defaults for item categorization
|
||||||
|
|
||||||
|
**Legacy Tables** (deprecated, may still exist):
|
||||||
|
- `grocery_list`, `grocery_history` - Old single-household implementation
|
||||||
|
|
||||||
**Important patterns**:
|
**Important patterns**:
|
||||||
- No migration system - schema changes are manual SQL
|
- No formal migration system - schema changes are manual SQL
|
||||||
- Items use case-insensitive matching (`ILIKE`) to prevent duplicates
|
- Items use case-insensitive matching (`ILIKE`) to prevent duplicates
|
||||||
- JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.js](backend/models/list.model.js))
|
- JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.v2.js](backend/models/list.model.v2.js))
|
||||||
|
- All list operations require `household_id` parameter for scoping
|
||||||
|
- Image storage: `bytea` columns for images with separate MIME type columns
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
@ -137,11 +239,16 @@ See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow
|
|||||||
|
|
||||||
## Authentication Flow
|
## Authentication Flow
|
||||||
|
|
||||||
1. User logs in → backend returns `{token, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
|
1. User logs in → backend returns `{token, userId, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
|
||||||
|
- `role` is the **system role** (`system_admin` or `user`)
|
||||||
2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx))
|
2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx))
|
||||||
3. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
|
3. `HouseholdContext` loads user's households and sets active household
|
||||||
4. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
|
- Active household includes `household.role` (the **household role**)
|
||||||
5. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login
|
4. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
|
||||||
|
5. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
|
||||||
|
- Sets `req.user = { id, role, username }` with **system role**
|
||||||
|
6. Controllers check household membership/role using [backend/models/household.model.js](backend/models/household.model.js)
|
||||||
|
7. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login
|
||||||
|
|
||||||
## Critical Conventions
|
## Critical Conventions
|
||||||
|
|
||||||
@ -167,16 +274,36 @@ See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow
|
|||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
**Add a new protected route**:
|
**Add a new protected route**:
|
||||||
1. Backend: Add route with `auth` + `requireRole(...)` middleware
|
1. Backend: Add route with `auth` middleware (+ `requireRole(...)` if system role check needed)
|
||||||
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` and/or `<RoleGuard>`
|
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` (and `<RoleGuard>` for Admin Panel)
|
||||||
|
|
||||||
**Access user info in backend controller**:
|
**Access user info in backend controller**:
|
||||||
```javascript
|
```javascript
|
||||||
const { id, role } = req.user; // Set by auth middleware
|
const { id, role } = req.user; // Set by auth middleware (system role)
|
||||||
|
const userId = req.user.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check household permissions in backend controller**:
|
||||||
|
```javascript
|
||||||
|
const householdRole = await household.getUserRole(householdId, userId);
|
||||||
|
if (!householdRole) return res.status(403).json({ message: "Not a member of this household" });
|
||||||
|
if (householdRole !== 'admin') return res.status(403).json({ message: "Household admin required" });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check household permissions in frontend**:
|
||||||
|
```javascript
|
||||||
|
const { activeHousehold } = useContext(HouseholdContext);
|
||||||
|
const householdRole = activeHousehold?.role; // 'admin' or 'user'
|
||||||
|
|
||||||
|
// Allow all members except viewers (no viewer role in households)
|
||||||
|
const canManageItems = householdRole && householdRole !== 'viewer'; // Usually just check if role exists
|
||||||
|
|
||||||
|
// Admin-only actions
|
||||||
|
const canManageMembers = householdRole === 'admin';
|
||||||
```
|
```
|
||||||
|
|
||||||
**Query grocery items with contributors**:
|
**Query grocery items with contributors**:
|
||||||
Use the JOIN pattern in [backend/models/list.model.js](backend/models/list.model.js) - aggregates user names via `grocery_history` table.
|
Use the JOIN pattern in [backend/models/list.model.v2.js](backend/models/list.model.v2.js) - aggregates user names via `household_list_history` table.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@ -26,21 +26,20 @@ exports.getHouseholdStores = async (req, res) => {
|
|||||||
exports.addStoreToHousehold = async (req, res) => {
|
exports.addStoreToHousehold = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { storeId, isDefault } = req.body;
|
const { storeId, isDefault } = req.body;
|
||||||
|
// console.log("Adding store to household:", { householdId: req.params.householdId, storeId, isDefault });
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
return res.status(400).json({ error: "Store ID is required" });
|
return res.status(400).json({ error: "Store ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if store exists
|
|
||||||
const store = await storeModel.getStoreById(storeId);
|
const store = await storeModel.getStoreById(storeId);
|
||||||
if (!store) {
|
if (!store) return res.status(404).json({ error: "Store not found" });
|
||||||
return res.status(404).json({ error: "Store not found" });
|
const foundStores = await storeModel.getHouseholdStores(req.params.householdId);
|
||||||
}
|
// if (foundStores.length == 0) isDefault = 'true';
|
||||||
|
|
||||||
await storeModel.addStoreToHousehold(
|
await storeModel.addStoreToHousehold(
|
||||||
req.params.householdId,
|
req.params.householdId,
|
||||||
storeId,
|
storeId,
|
||||||
isDefault || false
|
foundStores.length == 0 ? true : isDefault || false
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
|
|||||||
@ -94,7 +94,6 @@ exports.refreshInviteCode = async (householdId) => {
|
|||||||
|
|
||||||
// Join household via invite code
|
// Join household via invite code
|
||||||
exports.joinHousehold = async (inviteCode, userId) => {
|
exports.joinHousehold = async (inviteCode, userId) => {
|
||||||
// Find household by invite code
|
|
||||||
const householdResult = await pool.query(
|
const householdResult = await pool.query(
|
||||||
`SELECT id, name FROM households
|
`SELECT id, name FROM households
|
||||||
WHERE invite_code = $1
|
WHERE invite_code = $1
|
||||||
@ -102,22 +101,19 @@ exports.joinHousehold = async (inviteCode, userId) => {
|
|||||||
[inviteCode]
|
[inviteCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (householdResult.rows.length === 0) {
|
|
||||||
return null; // Invalid or expired code
|
if (householdResult.rows.length === 0) return null;
|
||||||
}
|
|
||||||
|
|
||||||
const household = householdResult.rows[0];
|
const household = householdResult.rows[0];
|
||||||
|
|
||||||
// Check if already member
|
|
||||||
const existingMember = await pool.query(
|
const existingMember = await pool.query(
|
||||||
`SELECT id FROM household_members
|
`SELECT id FROM household_members
|
||||||
WHERE household_id = $1 AND user_id = $2`,
|
WHERE household_id = $1 AND user_id = $2`,
|
||||||
[household.id, userId]
|
[household.id, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingMember.rows.length > 0) {
|
if (existingMember.rows.length > 0) return { ...household, alreadyMember: true };
|
||||||
return { ...household, alreadyMember: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add as user role
|
// Add as user role
|
||||||
await pool.query(
|
await pool.query(
|
||||||
|
|||||||
74
docs/README.md
Normal file
74
docs/README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Documentation Index
|
||||||
|
|
||||||
|
This directory contains all project documentation organized by category.
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
### `/architecture` - System Design & Structure
|
||||||
|
- **[component-structure.md](architecture/component-structure.md)** - Frontend component organization and patterns
|
||||||
|
- **[multi-household-architecture-plan.md](architecture/multi-household-architecture-plan.md)** - Multi-household system architecture design
|
||||||
|
|
||||||
|
### `/features` - Feature Implementation Details
|
||||||
|
- **[classification-implementation.md](features/classification-implementation.md)** - Item classification system (zones, types, groups)
|
||||||
|
- **[image-storage-implementation.md](features/image-storage-implementation.md)** - Image storage and handling (bytea, MIME types)
|
||||||
|
|
||||||
|
### `/guides` - How-To & Reference Guides
|
||||||
|
- **[api-documentation.md](guides/api-documentation.md)** - REST API endpoints and usage
|
||||||
|
- **[frontend-readme.md](guides/frontend-readme.md)** - Frontend development guide
|
||||||
|
- **[MOBILE_RESPONSIVE_AUDIT.md](guides/MOBILE_RESPONSIVE_AUDIT.md)** - Mobile-first design guidelines and audit checklist
|
||||||
|
- **[setup-checklist.md](guides/setup-checklist.md)** - Development environment setup steps
|
||||||
|
|
||||||
|
### `/migration` - Database Migrations & Updates
|
||||||
|
- **[MIGRATION_GUIDE.md](migration/MIGRATION_GUIDE.md)** - Multi-household migration instructions (also in `backend/migrations/`)
|
||||||
|
- **[POST_MIGRATION_UPDATES.md](migration/POST_MIGRATION_UPDATES.md)** - Required updates after migration
|
||||||
|
|
||||||
|
### `/archive` - Completed Implementation Records
|
||||||
|
Historical documentation of completed features. Useful for reference but not actively maintained.
|
||||||
|
|
||||||
|
- **[ACCOUNT_MANAGEMENT_IMPLEMENTATION.md](archive/ACCOUNT_MANAGEMENT_IMPLEMENTATION.md)** - Phase 4: Display name and password change
|
||||||
|
- **[code-cleanup-guide.md](archive/code-cleanup-guide.md)** - Code cleanup checklist (completed)
|
||||||
|
- **[HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md](archive/HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md)** - Household management UI implementation
|
||||||
|
- **[IMPLEMENTATION_STATUS.md](archive/IMPLEMENTATION_STATUS.md)** - Multi-household migration sprint status
|
||||||
|
- **[settings-dark-mode.md](archive/settings-dark-mode.md)** - Dark mode implementation notes
|
||||||
|
- **[TEST_SUITE_README.md](archive/TEST_SUITE_README.md)** - Testing infrastructure documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Root-Level Documentation
|
||||||
|
|
||||||
|
These files remain at the project root for easy access:
|
||||||
|
|
||||||
|
- **[../README.md](../README.md)** - Project overview and quick start
|
||||||
|
- **[../.github/copilot-instructions.md](../.github/copilot-instructions.md)** - AI assistant instructions (architecture, RBAC, conventions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Quick Reference
|
||||||
|
|
||||||
|
**Setting up the project?** → Start with [setup-checklist.md](guides/setup-checklist.md)
|
||||||
|
|
||||||
|
**Understanding the API?** → See [api-documentation.md](guides/api-documentation.md)
|
||||||
|
|
||||||
|
**Working on mobile UI?** → Check [MOBILE_RESPONSIVE_AUDIT.md](guides/MOBILE_RESPONSIVE_AUDIT.md)
|
||||||
|
|
||||||
|
**Need architecture context?** → Read [../.github/copilot-instructions.md](../.github/copilot-instructions.md)
|
||||||
|
|
||||||
|
**Running migrations?** → Follow [MIGRATION_GUIDE.md](migration/MIGRATION_GUIDE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Contributing to Documentation
|
||||||
|
|
||||||
|
When adding new documentation:
|
||||||
|
|
||||||
|
1. **Guides** (`/guides`) - General how-to, setup, reference
|
||||||
|
2. **Features** (`/features`) - Specific feature implementation details
|
||||||
|
3. **Architecture** (`/architecture`) - System design, patterns, structure
|
||||||
|
4. **Migration** (`/migration`) - Database migrations and upgrade guides
|
||||||
|
5. **Archive** (`/archive`) - Completed implementation records (for reference only)
|
||||||
|
|
||||||
|
Keep documentation:
|
||||||
|
- ✅ Up-to-date with code changes
|
||||||
|
- ✅ Concise and scannable
|
||||||
|
- ✅ Linked to relevant files (use relative paths)
|
||||||
|
- ✅ Organized by category
|
||||||
43
docs/archive/TEST_SUITE_README.md
Normal file
43
docs/archive/TEST_SUITE_README.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# API Test Suite
|
||||||
|
|
||||||
|
The test suite has been reorganized into separate files for better maintainability:
|
||||||
|
|
||||||
|
## New Modular Structure (✅ Complete)
|
||||||
|
- **api-tests.html** - Main HTML file
|
||||||
|
- **test-config.js** - Global state management
|
||||||
|
- **test-definitions.js** - All 62 test cases across 8 categories
|
||||||
|
- **test-runner.js** - Test execution logic
|
||||||
|
- **test-ui.js** - UI manipulation functions
|
||||||
|
- **test-styles.css** - All CSS styles
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
1. Start the dev server: `docker-compose -f docker-compose.dev.yml up`
|
||||||
|
2. Navigate to: `http://localhost:5000/test/api-tests.html`
|
||||||
|
3. Configure credentials (default: admin/admin123)
|
||||||
|
4. Click "▶ Run All Tests"
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- ✅ 62 comprehensive tests
|
||||||
|
- ✅ Collapsible test cards (collapsed by default)
|
||||||
|
- ✅ Expected field validation with visual indicators
|
||||||
|
- ✅ Color-coded HTTP status badges
|
||||||
|
- ✅ Auto-expansion on test run
|
||||||
|
- ✅ Expand/Collapse all buttons
|
||||||
|
- ✅ Real-time pass/fail/error states
|
||||||
|
- ✅ Summary dashboard
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
```
|
||||||
|
backend/public/
|
||||||
|
├── api-tests.html # Main entry point (use this)
|
||||||
|
├── test-config.js # State management (19 lines)
|
||||||
|
├── test-definitions.js # Test cases (450+ lines)
|
||||||
|
├── test-runner.js # Test execution (160+ lines)
|
||||||
|
├── test-ui.js # UI functions (90+ lines)
|
||||||
|
└── test-styles.css # All styles (310+ lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Old File
|
||||||
|
- **api-test.html** - Original monolithic version (kept for reference)
|
||||||
|
|
||||||
|
Total: ~1030 lines split into 6 clean, modular files
|
||||||
283
docs/guides/MOBILE_RESPONSIVE_AUDIT.md
Normal file
283
docs/guides/MOBILE_RESPONSIVE_AUDIT.md
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
# Mobile Responsive Design Audit & Recommendations
|
||||||
|
|
||||||
|
## ✅ Already Mobile-Friendly
|
||||||
|
|
||||||
|
### Components
|
||||||
|
1. **Navbar** - Just updated with hamburger menu, dropdowns, sticky positioning
|
||||||
|
2. **AdminPanel** - Has responsive breakpoints (768px, 480px)
|
||||||
|
3. **Manage page** - Has responsive breakpoints (768px, 480px)
|
||||||
|
4. **ManageHousehold** - Has 768px breakpoint
|
||||||
|
5. **Settings** - Has 768px breakpoint
|
||||||
|
6. **StoreManagement** - Has 768px breakpoint
|
||||||
|
7. **GroceryList** - Has 480px breakpoint
|
||||||
|
|
||||||
|
## ✅ Recently Completed (2026-01-26)
|
||||||
|
|
||||||
|
### **All Modals** - Mobile optimization COMPLETE ✓
|
||||||
|
**Files updated with responsive styles:**
|
||||||
|
- ✅ `frontend/src/styles/AddImageModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/ImageUploadModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/ItemClassificationModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/SimilarItemModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/components/EditItemModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/components/ConfirmAddExistingModal.css` - Added 768px & 480px breakpoints
|
||||||
|
- ✅ `frontend/src/styles/ImageModal.css` - Enhanced with 480px breakpoint
|
||||||
|
- ✅ `frontend/src/styles/components/AddItemWithDetailsModal.css` - Enhanced with 768px breakpoint
|
||||||
|
- ✅ `frontend/src/styles/ConfirmBuyModal.css` - Already excellent (480px & 360px breakpoints)
|
||||||
|
|
||||||
|
**Mobile improvements implemented:**
|
||||||
|
- Modal width: 95% at 768px, 100% at 480px
|
||||||
|
- All buttons: Full-width stacking on mobile with 44px minimum height
|
||||||
|
- Input fields: 16px font-size to prevent iOS zoom
|
||||||
|
- Image previews: Responsive sizing (180-200px on mobile)
|
||||||
|
- Touch targets: 44x44px minimum for all interactive elements
|
||||||
|
- Overflow: Auto scrolling for tall modals (max-height: 90vh)
|
||||||
|
- Spacing: Reduced padding on small screens
|
||||||
|
|
||||||
|
## ⚠️ Needs Improvement
|
||||||
|
|
||||||
|
### High Priority
|
||||||
|
|
||||||
|
#### 1. **HouseholdSwitcher** - Dropdown might overflow on mobile
|
||||||
|
**File:** `frontend/src/styles/components/HouseholdSwitcher.css`
|
||||||
|
|
||||||
|
**Current:** No mobile breakpoints
|
||||||
|
**Needs:**
|
||||||
|
```css
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.household-switcher-dropdown {
|
||||||
|
max-width: 90vw;
|
||||||
|
right: auto;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **StoreTabs** - Horizontal scrolling tabs on mobile
|
||||||
|
**File:** `frontend/src/styles/components/StoreTabs.css`
|
||||||
|
|
||||||
|
**Needs:**
|
||||||
|
```css
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.store-tabs {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-tab {
|
||||||
|
min-width: 100px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Login/Register Pages** - Need better mobile padding
|
||||||
|
**Files:**
|
||||||
|
- `frontend/src/styles/pages/Login.css`
|
||||||
|
- `frontend/src/styles/pages/Register.css`
|
||||||
|
|
||||||
|
**Needs:**
|
||||||
|
```css
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.card {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
|
||||||
|
#### 4. **GroceryList Item Cards** - Could be more touch-friendly
|
||||||
|
**File:** `frontend/src/styles/pages/GroceryList.css`
|
||||||
|
|
||||||
|
**Current:** Has 480px breakpoint
|
||||||
|
**Enhancement needed:**
|
||||||
|
- Increase touch target sizes for mobile
|
||||||
|
- Better spacing between items on small screens
|
||||||
|
- Optimize image display on mobile
|
||||||
|
|
||||||
|
#### 5. **AddItemForm** - Input width and spacing
|
||||||
|
**File:** `frontend/src/styles/components/AddItemForm.css`
|
||||||
|
|
||||||
|
**Has 480px breakpoint** but verify:
|
||||||
|
- Input font-size is 16px+ (prevents iOS zoom)
|
||||||
|
- Buttons are full-width on mobile
|
||||||
|
- Adequate spacing between form elements
|
||||||
|
|
||||||
|
#### 6. **CreateJoinHousehold Modal**
|
||||||
|
**File:** `frontend/src/styles/components/manage/CreateJoinHousehold.css`
|
||||||
|
|
||||||
|
**Has 600px breakpoint** - Review for:
|
||||||
|
- Full-screen on very small devices
|
||||||
|
- Button sizing and spacing
|
||||||
|
- Tab navigation usability
|
||||||
|
|
||||||
|
### Low Priority
|
||||||
|
|
||||||
|
#### 7. **SuggestionList** - Touch interactions
|
||||||
|
**File:** `frontend/src/styles/components/SuggestionList.css`
|
||||||
|
|
||||||
|
**Needs:** Mobile-specific styles for:
|
||||||
|
- Larger tap targets
|
||||||
|
- Better scrolling behavior
|
||||||
|
- Touch feedback
|
||||||
|
|
||||||
|
#### 8. **ClassificationSection** - Zone selection on mobile
|
||||||
|
**File:** `frontend/src/styles/components/ClassificationSection.css`
|
||||||
|
|
||||||
|
**Needs:**
|
||||||
|
- Ensure zone buttons are touch-friendly
|
||||||
|
- Stack vertically if needed on small screens
|
||||||
|
|
||||||
|
#### 9. **ImageUploadSection**
|
||||||
|
**File:** `frontend/src/styles/components/ImageUploadSection.css`
|
||||||
|
|
||||||
|
**Needs:**
|
||||||
|
- Camera access optimization for mobile
|
||||||
|
- Preview image sizing
|
||||||
|
- Upload button sizing
|
||||||
|
|
||||||
|
## 🎯 General Recommendations
|
||||||
|
|
||||||
|
### 1. **Global Styles**
|
||||||
|
Update `frontend/src/index.css`:
|
||||||
|
```css
|
||||||
|
/* Prevent zoom on input focus (iOS) */
|
||||||
|
input, select, textarea {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better touch scrolling */
|
||||||
|
* {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure body doesn't overflow horizontally */
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Container Max-Widths**
|
||||||
|
Standardize across the app:
|
||||||
|
- Small components: `max-width: 600px`
|
||||||
|
- Medium pages: `max-width: 800px`
|
||||||
|
- Wide layouts: `max-width: 1200px`
|
||||||
|
- Always pair with `margin: 0 auto` and `padding: 1rem`
|
||||||
|
|
||||||
|
### 3. **Button Sizing**
|
||||||
|
Mobile-friendly buttons:
|
||||||
|
```css
|
||||||
|
.btn-primary, .btn-secondary {
|
||||||
|
min-height: 44px; /* Apple's recommended minimum */
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn-primary, .btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Form Layouts**
|
||||||
|
Stack form fields on mobile:
|
||||||
|
```css
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Image Handling**
|
||||||
|
Responsive images:
|
||||||
|
```css
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. **Typography**
|
||||||
|
Adjust for mobile readability:
|
||||||
|
```css
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
h1 { font-size: 1.75rem; }
|
||||||
|
h2 { font-size: 1.5rem; }
|
||||||
|
h3 { font-size: 1.25rem; }
|
||||||
|
body { font-size: 16px; } /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Testing Checklist
|
||||||
|
|
||||||
|
Test on these viewports:
|
||||||
|
- [ ] 320px (iPhone SE)
|
||||||
|
- [ ] 375px (iPhone 12/13 Pro)
|
||||||
|
- [ ] 390px (iPhone 14 Pro)
|
||||||
|
- [ ] 414px (iPhone Pro Max)
|
||||||
|
- [ ] 768px (iPad Portrait)
|
||||||
|
- [ ] 1024px (iPad Landscape)
|
||||||
|
- [ ] 1280px+ (Desktop)
|
||||||
|
|
||||||
|
Test these interactions:
|
||||||
|
- [ ] Navigation menu (hamburger)
|
||||||
|
- [ ] Dropdowns (household, user menu)
|
||||||
|
- [ ] All modals
|
||||||
|
- [ ] Form inputs (no zoom on focus)
|
||||||
|
- [ ] Touch gestures (swipe, long-press)
|
||||||
|
- [ ] Scrolling (no horizontal overflow)
|
||||||
|
- [ ] Image upload/viewing
|
||||||
|
- [ ] Tab navigation
|
||||||
|
|
||||||
|
## 🔄 Future Considerations
|
||||||
|
|
||||||
|
1. **Progressive Web App (PWA)**
|
||||||
|
- Add manifest.json
|
||||||
|
- Service worker for offline support
|
||||||
|
- Install prompt
|
||||||
|
|
||||||
|
2. **Touch Gestures**
|
||||||
|
- Swipe to delete items
|
||||||
|
- Pull to refresh lists
|
||||||
|
- Long-press for context menu
|
||||||
|
|
||||||
|
3. **Keyboard Handling**
|
||||||
|
- iOS keyboard overlap handling
|
||||||
|
- Android keyboard behavior
|
||||||
|
- Input focus management
|
||||||
|
|
||||||
|
4. **Performance**
|
||||||
|
- Lazy load images
|
||||||
|
- Virtual scrolling for long lists
|
||||||
|
- Code splitting by route
|
||||||
|
|
||||||
|
## 📝 How to Maintain Mobile-First Design
|
||||||
|
|
||||||
|
I've updated `.github/copilot-instructions.md` with mobile-first design principles. This will be included in all future conversations automatically.
|
||||||
|
|
||||||
|
**To ensure I remember in new conversations:**
|
||||||
|
1. ✅ Mobile-first guidelines are now in copilot-instructions.md (automatically loaded)
|
||||||
|
2. Start conversations with: "Remember to keep mobile/desktop responsiveness in mind"
|
||||||
|
3. Review this audit document before making UI changes
|
||||||
|
4. Run mobile testing after any CSS/layout changes
|
||||||
|
|
||||||
|
**Quick reminder phrases:**
|
||||||
|
- "Make this mobile-friendly"
|
||||||
|
- "Add responsive breakpoints"
|
||||||
|
- "Test on mobile viewports"
|
||||||
|
- "Ensure touch-friendly targets"
|
||||||
243
docs/migration/MIGRATION_GUIDE.md
Normal file
243
docs/migration/MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# Multi-Household Architecture Migration Guide
|
||||||
|
|
||||||
|
## Pre-Migration Checklist
|
||||||
|
|
||||||
|
- [ ] **Backup Database**
|
||||||
|
```bash
|
||||||
|
pg_dump -U your_user -d grocery_list > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Test on Staging First**
|
||||||
|
- Copy production database to staging environment
|
||||||
|
- Run migration on staging
|
||||||
|
- Verify all data migrated correctly
|
||||||
|
- Test application functionality
|
||||||
|
|
||||||
|
- [ ] **Review Migration Script**
|
||||||
|
- Read through `multi_household_architecture.sql`
|
||||||
|
- Understand each step
|
||||||
|
- Note verification queries
|
||||||
|
|
||||||
|
- [ ] **Announce Maintenance Window**
|
||||||
|
- Notify users of downtime
|
||||||
|
- Schedule during low-usage period
|
||||||
|
- Estimate 15-30 minutes for migration
|
||||||
|
|
||||||
|
## Running the Migration
|
||||||
|
|
||||||
|
### 1. Connect to Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql -U your_user -d grocery_list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run Migration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
\i backend/migrations/multi_household_architecture.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
1. ✅ Create 8 new tables
|
||||||
|
2. ✅ Create default "Main Household"
|
||||||
|
3. ✅ Create default "Costco" store
|
||||||
|
4. ✅ Migrate all users to household members
|
||||||
|
5. ✅ Extract items to master catalog
|
||||||
|
6. ✅ Migrate grocery_list → household_lists
|
||||||
|
7. ✅ Migrate classifications
|
||||||
|
8. ✅ Migrate history records
|
||||||
|
9. ✅ Update user system roles
|
||||||
|
|
||||||
|
### 3. Verify Migration
|
||||||
|
|
||||||
|
Run these queries inside psql:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check household created
|
||||||
|
SELECT * FROM households;
|
||||||
|
|
||||||
|
-- Check all users migrated
|
||||||
|
SELECT u.username, u.role as system_role, hm.role as household_role
|
||||||
|
FROM users u
|
||||||
|
JOIN household_members hm ON u.id = hm.user_id
|
||||||
|
ORDER BY u.id;
|
||||||
|
|
||||||
|
-- Check item counts match
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(DISTINCT item_name) FROM grocery_list) as old_unique_items,
|
||||||
|
(SELECT COUNT(*) FROM items) as new_items;
|
||||||
|
|
||||||
|
-- Check list counts
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM grocery_list) as old_lists,
|
||||||
|
(SELECT COUNT(*) FROM household_lists) as new_lists;
|
||||||
|
|
||||||
|
-- Check classification counts
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM item_classification) as old_classifications,
|
||||||
|
(SELECT COUNT(*) FROM household_item_classifications) as new_classifications;
|
||||||
|
|
||||||
|
-- Check history counts
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM grocery_history) as old_history,
|
||||||
|
(SELECT COUNT(*) FROM household_list_history) as new_history;
|
||||||
|
|
||||||
|
-- Verify no data loss - check if all old items have corresponding new records
|
||||||
|
SELECT gl.item_name
|
||||||
|
FROM grocery_list gl
|
||||||
|
LEFT JOIN items i ON LOWER(i.name) = LOWER(TRIM(gl.item_name))
|
||||||
|
LEFT JOIN household_lists hl ON hl.item_id = i.id
|
||||||
|
WHERE hl.id IS NULL;
|
||||||
|
-- Should return 0 rows
|
||||||
|
|
||||||
|
-- Check invite code
|
||||||
|
SELECT name, invite_code FROM households;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Application
|
||||||
|
|
||||||
|
- [ ] Users can log in
|
||||||
|
- [ ] Can view "Main Household" list
|
||||||
|
- [ ] Can add items
|
||||||
|
- [ ] Can mark items as bought
|
||||||
|
- [ ] History shows correctly
|
||||||
|
- [ ] Classifications preserved
|
||||||
|
- [ ] Images display correctly
|
||||||
|
|
||||||
|
## Post-Migration Cleanup
|
||||||
|
|
||||||
|
**Only after verifying everything works correctly:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Drop old tables (CAREFUL - THIS IS IRREVERSIBLE)
|
||||||
|
DROP TABLE IF EXISTS grocery_history CASCADE;
|
||||||
|
DROP TABLE IF EXISTS item_classification CASCADE;
|
||||||
|
DROP TABLE IF EXISTS grocery_list CASCADE;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
### If Migration Fails
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Inside psql during migration
|
||||||
|
ROLLBACK;
|
||||||
|
|
||||||
|
-- Then restore from backup
|
||||||
|
\q
|
||||||
|
psql -U your_user -d grocery_list < backup_YYYYMMDD_HHMMSS.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### If Issues Found After Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Drop the database and restore
|
||||||
|
dropdb grocery_list
|
||||||
|
createdb grocery_list
|
||||||
|
psql -U your_user -d grocery_list < backup_YYYYMMDD_HHMMSS.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: Duplicate items in items table
|
||||||
|
**Cause**: Case-insensitive matching not working
|
||||||
|
**Solution**: Check item names for leading/trailing spaces
|
||||||
|
|
||||||
|
### Issue: Foreign key constraint errors
|
||||||
|
**Cause**: User or item references not found
|
||||||
|
**Solution**: Verify all users and items exist before migrating lists
|
||||||
|
|
||||||
|
### Issue: History not showing
|
||||||
|
**Cause**: household_list_id references incorrect
|
||||||
|
**Solution**: Check JOIN conditions in history migration
|
||||||
|
|
||||||
|
### Issue: Images not displaying
|
||||||
|
**Cause**: BYTEA encoding issues
|
||||||
|
**Solution**: Verify image_mime_type correctly migrated
|
||||||
|
|
||||||
|
## Migration Timeline
|
||||||
|
|
||||||
|
- **T-0**: Begin maintenance window
|
||||||
|
- **T+2min**: Backup complete
|
||||||
|
- **T+3min**: Start migration script
|
||||||
|
- **T+8min**: Migration complete (for ~1000 items)
|
||||||
|
- **T+10min**: Run verification queries
|
||||||
|
- **T+15min**: Test application functionality
|
||||||
|
- **T+20min**: If successful, announce completion
|
||||||
|
- **T+30min**: End maintenance window
|
||||||
|
|
||||||
|
## Data Integrity Checks
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Ensure all users belong to at least one household
|
||||||
|
SELECT u.id, u.username
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN household_members hm ON u.id = hm.user_id
|
||||||
|
WHERE hm.id IS NULL;
|
||||||
|
-- Should return 0 rows
|
||||||
|
|
||||||
|
-- Ensure all household lists have valid items
|
||||||
|
SELECT hl.id
|
||||||
|
FROM household_lists hl
|
||||||
|
LEFT JOIN items i ON hl.item_id = i.id
|
||||||
|
WHERE i.id IS NULL;
|
||||||
|
-- Should return 0 rows
|
||||||
|
|
||||||
|
-- Ensure all history has valid list references
|
||||||
|
SELECT hlh.id
|
||||||
|
FROM household_list_history hlh
|
||||||
|
LEFT JOIN household_lists hl ON hlh.household_list_id = hl.id
|
||||||
|
WHERE hl.id IS NULL;
|
||||||
|
-- Should return 0 rows
|
||||||
|
|
||||||
|
-- Check for orphaned classifications
|
||||||
|
SELECT hic.id
|
||||||
|
FROM household_item_classifications hic
|
||||||
|
LEFT JOIN household_lists hl ON hic.item_id = hl.item_id
|
||||||
|
AND hic.household_id = hl.household_id
|
||||||
|
AND hic.store_id = hl.store_id
|
||||||
|
WHERE hl.id IS NULL;
|
||||||
|
-- Should return 0 rows (or classifications for removed items, which is ok)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
✅ All tables created successfully
|
||||||
|
✅ All users migrated to "Main Household"
|
||||||
|
✅ Item count matches (unique items from old → new)
|
||||||
|
✅ List count matches (all grocery_list items → household_lists)
|
||||||
|
✅ Classification count matches
|
||||||
|
✅ History count matches
|
||||||
|
✅ No NULL foreign keys
|
||||||
|
✅ Application loads without errors
|
||||||
|
✅ Users can perform all CRUD operations
|
||||||
|
✅ Images display correctly
|
||||||
|
✅ Bought items still marked as bought
|
||||||
|
✅ Recently bought still shows correctly
|
||||||
|
|
||||||
|
## Next Steps After Migration
|
||||||
|
|
||||||
|
1. ✅ Update backend models (Sprint 2)
|
||||||
|
2. ✅ Update API routes
|
||||||
|
3. ✅ Update controllers
|
||||||
|
4. ✅ Test all endpoints
|
||||||
|
5. ✅ Update frontend contexts
|
||||||
|
6. ✅ Update UI components
|
||||||
|
7. ✅ Enable multi-household features
|
||||||
|
|
||||||
|
## Support & Troubleshooting
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
1. Check PostgreSQL logs: `/var/log/postgresql/`
|
||||||
|
2. Check application logs
|
||||||
|
3. Restore from backup if needed
|
||||||
|
4. Review migration script for errors
|
||||||
|
|
||||||
|
## Monitoring Post-Migration
|
||||||
|
|
||||||
|
For the first 24 hours after migration:
|
||||||
|
- Monitor error logs
|
||||||
|
- Watch for performance issues
|
||||||
|
- Verify user activity normal
|
||||||
|
- Check for any data inconsistencies
|
||||||
|
- Be ready to rollback if critical issues found
|
||||||
@ -15,7 +15,7 @@ export const getHouseholdStores = (householdId) =>
|
|||||||
* Add a store to a household
|
* Add a store to a household
|
||||||
*/
|
*/
|
||||||
export const addStoreToHousehold = (householdId, storeId, isDefault = false) =>
|
export const addStoreToHousehold = (householdId, storeId, isDefault = false) =>
|
||||||
api.post(`/stores/household/${householdId}`, { store_id: storeId, is_default: isDefault });
|
api.post(`/stores/household/${householdId}`, { storeId: storeId, isDefault: isDefault });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a store from a household
|
* Remove a store from a household
|
||||||
|
|||||||
@ -1,35 +1,84 @@
|
|||||||
import "../../styles/components/Navbar.css";
|
import "../../styles/components/Navbar.css";
|
||||||
|
|
||||||
import { useContext } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { AuthContext } from "../../context/AuthContext";
|
import { AuthContext } from "../../context/AuthContext";
|
||||||
import HouseholdSwitcher from "../household/HouseholdSwitcher";
|
import HouseholdSwitcher from "../household/HouseholdSwitcher";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const { role, logout, username } = useContext(AuthContext);
|
const { role, logout, username } = useContext(AuthContext);
|
||||||
|
const [showNavMenu, setShowNavMenu] = useState(false);
|
||||||
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||||
|
|
||||||
|
const closeMenus = () => {
|
||||||
|
setShowNavMenu(false);
|
||||||
|
setShowUserMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar">
|
<nav className="navbar">
|
||||||
<div className="navbar-links">
|
{/* Left: Navigation Menu */}
|
||||||
<Link to="/">Home</Link>
|
<div className="navbar-section navbar-left">
|
||||||
<Link to="/manage">Manage</Link>
|
<button
|
||||||
<Link to="/settings">Settings</Link>
|
className="navbar-menu-btn"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNavMenu(!showNavMenu);
|
||||||
|
setShowUserMenu(false);
|
||||||
|
}}
|
||||||
|
aria-label="Navigation menu"
|
||||||
|
>
|
||||||
|
<span className="hamburger-icon">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{role === "system_admin" && <Link to="/admin">Admin</Link>}
|
{showNavMenu && (
|
||||||
|
<>
|
||||||
|
<div className="menu-overlay" onClick={closeMenus}></div>
|
||||||
|
<div className="navbar-dropdown nav-dropdown">
|
||||||
|
<Link to="/" onClick={closeMenus}>Home</Link>
|
||||||
|
<Link to="/manage" onClick={closeMenus}>Manage</Link>
|
||||||
|
<Link to="/settings" onClick={closeMenus}>Settings</Link>
|
||||||
|
{role === "system_admin" && <Link to="/admin" onClick={closeMenus}>Admin</Link>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Household Switcher */}
|
||||||
|
<div className="navbar-section navbar-center">
|
||||||
<HouseholdSwitcher />
|
<HouseholdSwitcher />
|
||||||
|
|
||||||
<div className="navbar-idcard">
|
|
||||||
<div className="navbar-idinfo">
|
|
||||||
<span className="navbar-username">{username}</span>
|
|
||||||
<span className="navbar-role">{role}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="navbar-logout" onClick={logout}>
|
{/* Right: User Menu */}
|
||||||
|
<div className="navbar-section navbar-right">
|
||||||
|
<button
|
||||||
|
className="navbar-user-btn"
|
||||||
|
onClick={() => {
|
||||||
|
setShowUserMenu(!showUserMenu);
|
||||||
|
setShowNavMenu(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{username}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showUserMenu && (
|
||||||
|
<>
|
||||||
|
<div className="menu-overlay" onClick={closeMenus}></div>
|
||||||
|
<div className="navbar-dropdown user-dropdown">
|
||||||
|
<div className="user-dropdown-info">
|
||||||
|
<span className="user-dropdown-username">{username}</span>
|
||||||
|
<span className="user-dropdown-role">{role}</span>
|
||||||
|
</div>
|
||||||
|
<button className="user-dropdown-logout" onClick={() => { logout(); closeMenus(); }}>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -11,7 +11,7 @@ import "../../styles/components/manage/ManageStores.css";
|
|||||||
|
|
||||||
export default function ManageStores() {
|
export default function ManageStores() {
|
||||||
const { activeHousehold } = useContext(HouseholdContext);
|
const { activeHousehold } = useContext(HouseholdContext);
|
||||||
const { stores: householdStores, loadStores } = useContext(StoreContext);
|
const { stores: householdStores, refreshStores } = useContext(StoreContext);
|
||||||
const [allStores, setAllStores] = useState([]);
|
const [allStores, setAllStores] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showAddStore, setShowAddStore] = useState(false);
|
const [showAddStore, setShowAddStore] = useState(false);
|
||||||
@ -36,8 +36,9 @@ export default function ManageStores() {
|
|||||||
|
|
||||||
const handleAddStore = async (storeId) => {
|
const handleAddStore = async (storeId) => {
|
||||||
try {
|
try {
|
||||||
|
console.log("Adding store with ID:", storeId);
|
||||||
await addStoreToHousehold(activeHousehold.id, storeId, false);
|
await addStoreToHousehold(activeHousehold.id, storeId, false);
|
||||||
await loadStores();
|
await refreshStores();
|
||||||
setShowAddStore(false);
|
setShowAddStore(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add store:", error);
|
console.error("Failed to add store:", error);
|
||||||
@ -50,7 +51,7 @@ export default function ManageStores() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await removeStoreFromHousehold(activeHousehold.id, storeId);
|
await removeStoreFromHousehold(activeHousehold.id, storeId);
|
||||||
await loadStores();
|
await refreshStores();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to remove store:", error);
|
console.error("Failed to remove store:", error);
|
||||||
alert("Failed to remove store");
|
alert("Failed to remove store");
|
||||||
@ -60,7 +61,7 @@ export default function ManageStores() {
|
|||||||
const handleSetDefault = async (storeId) => {
|
const handleSetDefault = async (storeId) => {
|
||||||
try {
|
try {
|
||||||
await setDefaultStore(activeHousehold.id, storeId);
|
await setDefaultStore(activeHousehold.id, storeId);
|
||||||
await loadStores();
|
await refreshStores();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to set default store:", error);
|
console.error("Failed to set default store:", error);
|
||||||
alert("Failed to set default store");
|
alert("Failed to set default store");
|
||||||
|
|||||||
@ -81,3 +81,38 @@
|
|||||||
.add-image-remove:hover {
|
.add-image-remove:hover {
|
||||||
background: rgba(255, 0, 0, 1);
|
background: rgba(255, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.add-image-preview-container {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-image-preview {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-image-option-btn {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.add-image-preview {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-image-option-btn {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-image-remove {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -64,3 +64,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.image-modal-overlay {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal-content {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 95vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal-img {
|
||||||
|
max-height: 50vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -201,3 +201,67 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.image-upload-modal {
|
||||||
|
width: 95%;
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-modal h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-option-btn {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image-preview {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-cancel,
|
||||||
|
.image-upload-skip,
|
||||||
|
.image-upload-confirm {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.image-upload-modal {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-modal h2 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-subtitle {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image-preview {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-remove-image {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -100,3 +100,52 @@
|
|||||||
.classification-modal-btn-confirm:hover {
|
.classification-modal-btn-confirm:hover {
|
||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.classification-modal-content {
|
||||||
|
width: 95%;
|
||||||
|
padding: 1.25rem;
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-subtitle {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-select {
|
||||||
|
padding: 0.7em;
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.classification-modal-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-modal-field label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,3 +22,23 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.similar-item-suggested,
|
||||||
|
.similar-item-original {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.similar-modal-actions .btn {
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.similar-item-suggested,
|
||||||
|
.similar-item-original {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -195,13 +195,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
/* Mobile responsiveness */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 768px) {
|
||||||
|
.add-item-details-overlay {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.add-item-details-modal {
|
.add-item-details-modal {
|
||||||
padding: 1.2em;
|
width: 95%;
|
||||||
|
max-width: 95%;
|
||||||
|
padding: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-item-details-title {
|
.add-item-details-title {
|
||||||
font-size: 1.2em;
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-select {
|
||||||
|
padding: 0.7em;
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-image-options {
|
||||||
|
gap: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-image-btn {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.add-item-details-modal {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-title {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-subtitle {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-details-section-title {
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-item-details-image-options {
|
.add-item-details-image-options {
|
||||||
@ -210,9 +258,10 @@
|
|||||||
|
|
||||||
.add-item-details-image-btn {
|
.add-item-details-image-btn {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-item-details-actions {
|
.add-item-details-field label {
|
||||||
flex-direction: column;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,3 +39,30 @@
|
|||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.confirm-add-existing-qty-info {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-row {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-value {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-total .qty-label,
|
||||||
|
.qty-total .qty-value {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.qty-row {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: var(--spacing-xxs) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -187,3 +187,83 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.edit-modal-overlay {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-content {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 95%;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-input,
|
||||||
|
.edit-modal-select {
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-quantity-input {
|
||||||
|
width: 70px;
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-inline-field {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-inline-field label {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-inline-field .edit-modal-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.edit-modal-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-quantity-control {
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
.household-switcher-toggle {
|
.household-switcher-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
@ -15,6 +16,7 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.household-switcher-toggle:hover {
|
.household-switcher-toggle:hover {
|
||||||
@ -29,11 +31,18 @@
|
|||||||
|
|
||||||
.household-name {
|
.household-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-icon {
|
.dropdown-icon {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-icon.open {
|
.dropdown-icon.open {
|
||||||
@ -53,7 +62,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.5rem);
|
top: calc(100% + 0.5rem);
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 220px;
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@ -1,58 +1,232 @@
|
|||||||
|
/* Navbar - Sticky at top */
|
||||||
.navbar {
|
.navbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
background: #343a40;
|
background: #343a40;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.6em 1em;
|
padding: 0.75rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 1rem;
|
||||||
margin-bottom: 1em;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-links a {
|
/* Navbar Sections */
|
||||||
|
.navbar-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-left {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-center {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger Menu Button */
|
||||||
|
.navbar-menu-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-icon span {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu-btn:hover .hamburger-icon span {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Button */
|
||||||
|
.navbar-user-btn {
|
||||||
|
background: #495057;
|
||||||
color: white;
|
color: white;
|
||||||
margin-right: 1em;
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Overlay */
|
||||||
|
.menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Base Styles */
|
||||||
|
.navbar-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1001;
|
||||||
|
min-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Dropdown */
|
||||||
|
.nav-dropdown {
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown a {
|
||||||
|
color: #343a40;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 1.1em;
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-links a:hover {
|
.nav-dropdown a:last-child {
|
||||||
text-decoration: underline;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-logout {
|
.nav-dropdown a:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Dropdown */
|
||||||
|
.user-dropdown {
|
||||||
|
right: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-info {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-username {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-role {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-logout {
|
||||||
|
width: 100%;
|
||||||
background: #dc3545;
|
background: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.4em 0.8em;
|
padding: 0.75rem 1.25rem;
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100px;
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-idcard {
|
.user-dropdown-logout:hover {
|
||||||
display: flex;
|
background: #c82333;
|
||||||
align-items: center;
|
|
||||||
align-content: center;
|
|
||||||
margin-right: 1em;
|
|
||||||
padding: 0.3em 0.6em;
|
|
||||||
background: #495057;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-idinfo {
|
/* Household Switcher - Centered with max width */
|
||||||
display: flex;
|
.navbar-center > * {
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
line-height: 1.1;
|
max-width: 24ch; /* 24 characters max width */
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-username {
|
/* Mobile Responsive */
|
||||||
font-size: 0.95em;
|
@media (max-width: 768px) {
|
||||||
font-weight: bold;
|
.navbar {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-role {
|
.navbar-center {
|
||||||
font-size: 0.75em;
|
max-width: 60%;
|
||||||
opacity: 0.8;
|
}
|
||||||
|
|
||||||
|
.navbar-user-btn {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.navbar {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-center {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user-btn {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-icon {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-icon span {
|
||||||
|
height: 2.5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,13 @@
|
|||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
border-bottom: 2px solid var(--color-border-light);
|
border-bottom: 2px solid var(--color-border-light);
|
||||||
|
touch-action: pan-x; /* Lock Y-axis, allow only horizontal scrolling */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari/Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab {
|
.settings-tab {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user