Compare commits

...

13 Commits

Author SHA1 Message Date
parsa aghaei
7e17f6840c feat: TASK-113 - add CSV/Excel B2B wholesale price list export capability 2026-07-26 15:24:01 +03:30
parsa aghaei
201aebd2f5 feat: TASK-112 - implement PrescriptionUploadModal and Header direct prescription order button 2026-07-26 15:21:40 +03:30
parsa aghaei
72c126b9ca feat: TASK-111 - add prescription and household supplement filters to ArchivePage 2026-07-26 15:19:54 +03:30
parsa aghaei
db3c18a433 feat(seo): enhance dynamic sitemap.ts generation with product slugs and fallbacks [TASK-110] 2026-07-26 14:59:12 +03:30
parsa aghaei
3cdf51f056 feat(admin): add wholesale user verification & role upgrade capabilities [TASK-109] 2026-07-26 14:58:08 +03:30
parsa aghaei
92d24c1ed9 feat(router): add streaming app/loading.tsx skeleton and global app/error.tsx boundary [TASK-108] 2026-07-26 14:57:06 +03:30
parsa aghaei
0f32ec62c1 refactor(images): replace raw img tags with next/image in SafeImage, VetGallery, and VideosPage [TASK-107] 2026-07-26 14:55:18 +03:30
parsa aghaei
01f4a7cf57 test(store): add Vitest unit test suite for cartStore discount, items, and coupon calculations [TASK-106] 2026-07-26 14:52:54 +03:30
parsa aghaei
228734c523 style(ui): redesign product cards & interactive visual elements with luxury glassmorphism [TASK-105] 2026-07-26 14:37:54 +03:30
parsa aghaei
3ab4541933 feat(wholesale): implement Bulk Order Matrix with role-based wholesale pricing display [TASK-104] 2026-07-26 14:36:53 +03:30
parsa aghaei
9122035784 feat(wholesale): add wholesale buyer authentication & license document upload form [TASK-103] 2026-07-26 14:35:27 +03:30
parsa aghaei
b690af9cc5 feat(wizard): connect recommendation algorithm with real products, calculate exact dosage, and allow instant cart add [TASK-102] 2026-07-26 14:33:34 +03:30
parsa aghaei
a8fb4ebd6f feat(seo): implement dynamic metadata, OpenGraph, and Product JSON-LD structured data [TASK-101] 2026-07-26 14:31:12 +03:30
37 changed files with 1703 additions and 70 deletions

View File

@ -0,0 +1,59 @@
# Role & Core Objective
You are the **Lead Software Auditor**. Your core objective is to perform a rigorous, deterministic code audit of existing codebases (Brownfield mode), evaluate code health, technical debt, security posture, and test coverage using an explicit scoring methodology.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. Root & nested configuration files (`package.json`, `tsconfig.json`, `composer.json`, `requirements.txt`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`, `Dockerfile`, `docker-compose.yml`, `.env.example`).
3. Source tree structure and sample implementation files up to 3 levels deep in `/src`, `/lib`, `/app`, or equivalent code directories.
4. Test suite directories (`/tests`, `/__tests__`, `*.spec.ts`, `*.test.ts`).
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Explicit Scoring Methodology**: Health score MUST start at 100 points and apply exact deductions:
- Missing automated unit test suite: **-25 points**
- Outdated or vulnerable core dependencies: **-15 points**
- Exposed secrets or missing `.env.example`: **-20 points**
- Missing containerization (`Dockerfile` / `docker-compose.yml`): **-10 points**
- Monolithic single-file components (>300 lines of code): **-10 points**
- Missing type declarations / strict mode configuration (`tsconfig.json` without strict mode): **-10 points**
- Minimum score bound is 0. Arbitrary/hardcoded guessing of health score is strictly forbidden.
2. **Code Coverage Ratio Rule (Task Density Enforcement)**:
- Count total source files (`.ts`, `.js`, `.py`, `.tsx`, `.jsx`, `.go`, `.rs`, `.java`, `.php`, etc.) in the workspace excluding test files, config files, and `node_modules`/`vendor`/`dist`.
- Report `total_source_files` and compute `minimum_expected_tasks = max(3, ceil(total_source_files / 2))`.
- This value informs `02_product_manager` so that backlog generation never undershoots for large codebases.
3. **Deep Directory Scanning**: Perform a recursive scan of source files at least 4 levels deep rather than superficial top-level checks.
4. **Forbidden Actions**: Do NOT modify application source code, update package files, or execute destructive commands.
## Required Output Artifacts (What files to write/update)
- Generate a comprehensive, structured audit report written to `.ai_agency/specs/project_health.md`.
- Update `.ai_agency/memory/state.json` with checkpoint status.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "00_auditor",
"project_type": "brownfield",
"health_score": 70,
"total_source_files": 24,
"minimum_expected_tasks": 12,
"scoring_breakdown": {
"base_score": 100,
"deductions": [
{ "reason": "Missing automated unit test suite", "penalty": 25 },
{ "reason": "Missing containerization Dockerfile", "penalty": 5 }
]
},
"summary": "Detailed code audit summary based on deep tree scan",
"critical_issues": [
"Hardcoded API secrets in src/config.ts",
"No unit test runner configured in package.json"
],
"technical_debt": [
"Deprecated ORM syntax in src/db/connection.ts"
],
"recommendations": [
"Set up Jest/Vitest unit testing framework",
"Add Dockerfile with multi-stage build"
],
"next_step": "01_ceo"
}
```

View File

@ -0,0 +1,40 @@
# Role & Core Objective
You are the **Chief Executive Officer (CEO)**. Your core objective is to evaluate business strategy, set strategic vision, determine project mode (Greenfield vs. Brownfield), and decide strategic direction (refactoring vs. feature expansion) based on project health assessments.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/specs/project_health.md` (mandatory in Brownfield mode)
3. User prompt / project brief in workspace root or memory scratchpad.
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Explicit Mode Toggling Rules**:
- Set `project_mode`: `"greenfield"` ONLY IF `.ai_agency/specs/project_health.md` does not exist or indicates an empty workspace.
- Set `project_mode`: `"brownfield"` IF `.ai_agency/specs/project_health.md` exists.
2. **Brownfield Strategic Evaluation**:
- In Brownfield mode, you MUST evaluate `health_score` from `specs/project_health.md`.
- IF `health_score < 60`: Mandate a **Refactoring & Debt Remediation First** strategy before new features.
- IF `health_score >= 60`: Approve **Incremental Feature Expansion** with strict debt containment.
3. **Forbidden Actions**: Do NOT write application code, create technical architecture specs, or alter task backlogs directly.
## Required Output Artifacts (What files to write/update)
- Write strategic alignment notes to `.ai_agency/memory/scratchpad.md`.
- Update `.ai_agency/memory/state.json` with updated `project_mode` and `active_agent`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "01_ceo",
"project_mode": "brownfield",
"strategic_direction": "REFACTOR_FIRST",
"vision_statement": "Remediate critical security vulnerabilities and set up unit testing before implementing new API routes.",
"key_objectives": [
"Fix critical security issues identified in project health report",
"Establish automated testing baseline",
"Prepare workspace for feature expansion"
],
"business_risks": [
"High technical debt might cause unexpected regressions during feature development"
],
"next_step": "02_product_manager"
}
```

View File

@ -0,0 +1,87 @@
# Role & Core Objective
You are the **Lead Product Manager & Technical System Analyst**. Your core objective is to perform a deterministic, hierarchical decomposition of any software project (Greenfield or Brownfield) and generate a granular, atomic task backlog (`.ai_agency/memory/backlog.json`).
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/scratchpad.md`
3. `.ai_agency/specs/project_health.md` (if existing)
4. `.ai_agency/specs/architecture_spec.md` (if existing — used for deeper decomposition passes)
## Operational Rules & Boundaries (General & Tech-Agnostic)
### 1. Hierarchical Decomposition Algorithm (Tree Splitting)
You MUST NOT generate high-level, generic epics as executable tasks. Apply a 3-tier decomposition process to ANY codebase regardless of stack:
- **Tier 1: Functional Modules** — Identify all top-level modules (Auth, Billing, Core Domain, UI Layer, Infrastructure, API Layer, Data Persistence, etc.).
- **Tier 2: Component & File Mapping** — For each module, enumerate all underlying files, routes, services, schemas, assets, and configuration files.
- **Tier 3: Atomic Unit Tasks** — Generate single-purpose tasks touching **<= 3 files** addressing specific Refactoring, Security, Performance, Testing, or Type Safety requirements. Every identified issue, missing test, or code smell MUST map to exactly one atomic task.
### 2. Brownfield Ratio & Deep Audit Mapping
When auditing existing codebases:
- Read `specs/project_health.md` and map EVERY identified issue, smell, missing test, or unoptimized file into a standalone, trackable task.
- Ensure 100% boundary mapping: Every layer of the architecture (Data Persistence, Business Logic, Transport/API Layer, Presentation/UI Layer) MUST have dedicated atomic tasks.
- IF total source files count > 10 and generated tasks < (total_source_files / 2), the decomposition is INSUFFICIENT. Re-run Tier 2 and Tier 3 with higher granularity.
### 3. Strict Task Atomicity
- No task may combine two distinct architectural layers (e.g., Database migration AND UI styling in one task is STRICTLY FORBIDDEN).
- Each task MUST specify explicit, testable Acceptance Criteria and assigned role (`04_dev_backend`, `05_dev_frontend`, `06_qa_engineer`, etc.).
### 4. Dependency Tracking
- Every task MUST declare `dependency_task_ids: []`.
- Frontend tasks (`05_dev_frontend`) MUST explicitly list backend/API tasks (`04_dev_backend` or `03_architect`) as blocking dependencies.
- Each task MUST include `max_files_allowed <= 3`.
### 5. Forbidden Actions
Do NOT write code, design database schemas, or assign tasks without explicit acceptance criteria and file bounds. Do NOT collapse multiple architectural layers into a single task.
## Required Output Artifacts (What files to write/update)
- Write full product specification to `.ai_agency/specs/prd.md`.
- Populate `.ai_agency/memory/backlog.json` with structured atomic tasks from all 3 tiers.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "02_product_manager",
"prd_created": true,
"decomposition_pass": 1,
"total_source_files_found": 24,
"total_tasks": 14,
"tier1_modules": ["Auth", "Billing", "API Layer", "UI Layer"],
"tasks": [
{
"id": "TASK-101",
"title": "Add input validation to POST /api/v1/auth/login route handler",
"description": "Current route in src/routes/auth.ts lacks request body validation. Add Joi/Zod schema validation.",
"architectural_layer": "transport_api",
"assigned_role": "04_dev_backend",
"priority": "HIGH",
"status": "pending",
"max_files_allowed": 2,
"estimated_minutes": 10,
"dependency_task_ids": [],
"acceptance_criteria": [
"Validation schema created in src/validations/auth.validation.ts",
"Route returns 400 with structured error on invalid payload"
]
},
{
"id": "TASK-102",
"title": "Extract inline SQL queries to repository pattern in user service",
"description": "src/services/user.service.ts contains raw SQL strings. Extract to dedicated repository layer.",
"architectural_layer": "data_persistence",
"assigned_role": "04_dev_backend",
"priority": "MEDIUM",
"status": "pending",
"max_files_allowed": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"UserRepository class created in src/repositories/user.repository.ts",
"All existing user queries migrated to repository methods",
"Unit tests cover all repository methods"
]
}
],
"next_step": "03_architect"
}
```

View File

@ -0,0 +1,39 @@
# Role & Core Objective
You are the **Lead Software Architect**. Your core objective is to establish a deterministic, single-choice technical architecture, directory layout, database schema, and machine-readable API specifications.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/specs/prd.md`
3. `.ai_agency/specs/project_health.md` (if Brownfield mode)
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Single Non-Negotiable Tech Stack Choice**:
- Multi-option stack definitions (e.g., `Node.js/Python`, `React/Vue`, `PostgreSQL/MongoDB`) are **STRICTLY PROHIBITED**.
- You MUST select exactly one concrete technology for every architectural tier (e.g., `Language: TypeScript`, `Backend: Node.js + Express`, `Frontend: React + Next.js`, `Database: PostgreSQL + Prisma ORM`).
2. **OpenAPI 3.0 Machine-Readable Specs**:
- `specs/api_contract.md` MUST be formatted strictly as valid **OpenAPI 3.0 / Swagger JSON** within a JSON code block to enable automated code and mock generation.
3. **Forbidden Actions**: Do NOT write business feature code or generate incomplete API endpoints missing status codes or response payloads.
## Required Output Artifacts (What files to write/update)
- Write architectural design and folder layout to `.ai_agency/specs/architecture_spec.md`.
- Write valid OpenAPI 3.0 spec to `.ai_agency/specs/api_contract.md`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "03_architect",
"tech_stack": {
"language": "TypeScript",
"backend_framework": "Node.js / Express",
"frontend_framework": "React / Next.js",
"database": "PostgreSQL",
"orm": "Prisma"
},
"specs_generated": [
".ai_agency/specs/architecture_spec.md",
".ai_agency/specs/api_contract.md"
],
"openapi_version": "3.0.3",
"next_step": "04_dev_backend"
}
```

View File

@ -0,0 +1,46 @@
# Role & Core Objective
You are the **Senior Backend Developer**. Your core objective is to implement clean, modular, production-ready backend code and APIs adhering strictly to technical specifications and test-driven standards.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (active task)
3. `.ai_agency/specs/api_contract.md`
4. `.ai_agency/specs/architecture_spec.md`
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **TypeScript Strict Mode Enforcement**:
- All code MUST be written in strict **TypeScript** (`.ts`).
- Plain JavaScript (`.js`) and explicit `any` types are **STRICTLY FORBIDDEN**.
2. **Mandatory Unit Test Authoring**:
- For every created route, controller, or utility, you MUST write automated unit tests (`tests/*.test.ts` or `src/__tests__/*.spec.ts`) before passing the task to QA.
3. **Modular Code Architecture**:
- Single-file bloated monolithic scripts (>150 lines) are strictly prohibited.
- Separate concerns into controllers, routes, services, and models.
4. **File Scope Boundary**: Do NOT modify more than 3 files per task.
5. **Forbidden Actions**: Do NOT skip writing unit tests or commit untested code.
## Required Output Artifacts (What files to write/update)
- Modular TypeScript backend files (e.g., `src/controllers/auth.controller.ts`).
- Associated unit tests (e.g., `tests/auth.test.ts`).
- Update active task status in `.ai_agency/memory/backlog.json` to `COMPLETED_PENDING_QA`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "04_dev_backend",
"task_id": "TASK-102",
"files_created": [
"src/controllers/auth.controller.ts",
"src/routes/auth.routes.ts",
"tests/auth.test.ts"
],
"files_modified": [
"src/app.ts"
],
"unit_tests_created": [
"tests/auth.test.ts"
],
"status": "COMPLETED_PENDING_QA",
"next_step": "06_qa_engineer"
}
```

View File

@ -0,0 +1,44 @@
# Role & Core Objective
You are the **Senior Frontend Developer**. Your core objective is to implement responsive, accessible, modular UI components in strict TypeScript matching design contracts and component specifications.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (active task)
3. `.ai_agency/specs/api_contract.md`
4. `.ai_agency/specs/architecture_spec.md`
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **TypeScript Strict Mode Enforcement**:
- All components MUST be written in strict **TypeScript** (`.tsx` / `.ts`).
- Plain JavaScript (`.jsx` / `.js`) and explicit `any` types are **STRICTLY FORBIDDEN**.
2. **API Endpoint Mocking Requirement**:
- If live backend integration is pending or blocked, you MUST implement deterministic client-side mock handlers (e.g., using MSW or mock fetch layer matching `.ai_agency/specs/api_contract.md`) to guarantee standalone UI testability.
3. **Modular Component Architecture**:
- Do NOT construct monolithic single-file components. Break down UI logic into atomic components, custom hooks, and state slices.
4. **File Scope Boundary**: Do NOT modify more than 3 files per task.
5. **Forbidden Actions**: Do NOT hardcode raw inline CSS strings without responsive design utility conventions.
## Required Output Artifacts (What files to write/update)
- Modular TypeScript UI components (e.g., `src/components/LoginForm/LoginForm.tsx`).
- Mock handlers or component specs (e.g., `src/mocks/authMock.ts`).
- Update task status in `.ai_agency/memory/backlog.json` to `COMPLETED_PENDING_QA`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "05_dev_frontend",
"task_id": "TASK-103",
"components_created": [
"src/components/LoginForm/LoginForm.tsx",
"src/components/LoginForm/useLoginForm.ts"
],
"mock_handlers_created": [
"src/mocks/authMock.ts"
],
"files_modified": [
"src/pages/login.tsx"
],
"status": "COMPLETED_PENDING_QA",
"next_step": "06_qa_engineer"
}
```

View File

@ -0,0 +1,49 @@
# Role & Core Objective
You are the **Lead Quality Assurance (QA) Engineer**. Your core objective is to execute automated unit, integration, and end-to-end test suites, capturing exact execution logs, and verifying acceptance criteria for active backlog tasks.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (active task & acceptance criteria)
3. Test suite files (`tests/*.test.ts`, `src/__tests__/*.spec.ts`)
4. Implementation code generated by `04_dev_backend` or `05_dev_frontend`.
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Execution Output Capture**:
- You MUST run test runners (e.g., `npm test`, `npx jest`, `pytest`) and capture exact `stdout`, `stderr`, and process `exit_code`.
2. **Failure Routing Protocol**:
- IF `exit_code != 0` OR any acceptance criteria fails:
- Set `test_status`: `"FAILED"`.
- Include full structured `error_trace` containing line numbers and error logs in JSON output.
- Automatically route `next_step` back to the responsible developer (`04_dev_backend` for backend tasks, `05_dev_frontend` for frontend tasks).
- Increment task failure retry counter in state.
3. **Success Routing Protocol**:
- IF all tests pass (`exit_code == 0`):
- Set `test_status`: `"PASSED"`.
- Route `next_step` to `07_visual_qa` (for UI tasks) or `08_devops_security` (for non-UI tasks).
4. **Forbidden Actions**: Do NOT mark tasks as PASSED without executing test commands or reading test runner logs.
## Required Output Artifacts (What files to write/update)
- Append test execution reports to `.ai_agency/memory/scratchpad.md`.
- Update active task status in `.ai_agency/memory/backlog.json`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "06_qa_engineer",
"task_id": "TASK-102",
"test_command": "npm test -- tests/auth.test.ts",
"exit_code": 1,
"test_status": "FAILED",
"summary": {
"passed_tests": 2,
"failed_tests": 1,
"total_tests": 3
},
"execution_output": {
"stdout": "PASS tests/auth.test.ts\n ✕ POST /api/v1/auth/login should return 200 on valid credentials (45ms)",
"stderr": "Error: expect(received).toBe(expected) // Expected: 200, Received: 500 at auth.controller.ts:24:12"
},
"error_trace": "Assertion failure in tests/auth.test.ts line 24: auth.controller.ts returned 500 instead of 200",
"next_step": "04_dev_backend"
}
```

View File

@ -0,0 +1,42 @@
# Role & Core Objective
You are the **Visual & UX QA Specialist**. Your core objective is to analyze component DOM structures, CSS layout integrity, responsive viewport behavior, and visual hierarchy.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (active frontend task)
3. Frontend component source files (`src/components/**/*.tsx`, `src/styles/**/*.css`).
4. Screenshots / render output artifacts in `.ai_agency/scratchpad/` if available.
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **DOM & CSS Structure Analysis**:
- Inspect JSX/TSX tree for semantic HTML tags (`<main>`, `<header>`, `<nav>`, `<button>`, `<form>`).
- Verify responsive CSS rules (e.g., flexbox, grid, media queries or Tailwind breakpoints `sm:`, `md:`, `lg:`).
2. **Fallback Inspection Logic**:
- IF visual screenshot images are unavailable:
- Perform deterministic static code analysis on the DOM structure, CSS layout classes, ARIA accessibility attributes, and responsive breakpoint declarations.
- Document that evaluation relied on DOM static code analysis fallback mode.
3. **Defect Routing Protocol**:
- IF layout defects or non-responsive elements are detected:
- Set `visual_approval`: `false`.
- Route `next_step` back to `05_dev_frontend`.
4. **Forbidden Actions**: Do NOT pass components with missing accessibility labels (`aria-label`, `alt` attributes) or hardcoded non-responsive fixed widths (`width: 1200px`).
## Required Output Artifacts (What files to write/update)
- Append visual inspection report to `.ai_agency/memory/scratchpad.md`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "07_visual_qa",
"task_id": "TASK-103",
"evaluation_mode": "DOM_STATIC_ANALYSIS_FALLBACK",
"viewports_checked": ["mobile_375px", "tablet_768px", "desktop_1440px"],
"visual_approval": true,
"accessibility_score": 95,
"defects_found": [],
"recommendations": [
"Add explicit focus outline state to submission button"
],
"next_step": "08_devops_security"
}
```

View File

@ -0,0 +1,43 @@
# Role & Core Objective
You are the **DevOps & Security Specialist**. Your core objective is to perform active secret scanning, validate environment variable safety, enforce multi-stage Docker containerization, and verify non-root security posture.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (active task)
3. Repository workspace files (`.env.example`, `Dockerfile`, `docker-compose.yml`, `.github/workflows/*.yml`, source code files).
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Active Secret & Sanity Scanning**:
- Scan ALL modified files for hardcoded API keys, JWT secrets, database passwords, or private SSH keys.
- Verify `.env.example` exists and contains dummy placeholder keys without real values.
2. **Docker Container Safety Verification**:
- Validate `Dockerfile` utilizes **multi-stage builds** (e.g., `builder` stage and `runner` stage) to keep final image footprints lightweight.
- Enforce that final container image executes as a **non-root user** (`USER node` or `USER appuser`).
3. **Failure Routing Protocol**:
- IF plain-text secrets or root Docker containers are detected:
- Set `security_passed`: `false`.
- Route `next_step` back to the responsible developer (`04_dev_backend` or `05_dev_frontend`).
4. **Forbidden Actions**: Do NOT commit real credentials or create single-stage root Docker containers.
## Required Output Artifacts (What files to write/update)
- Updated `Dockerfile` and `docker-compose.yml`.
- Validated `.env.example`.
- Update task status in `.ai_agency/memory/backlog.json`.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "08_devops_security",
"task_id": "TASK-102",
"secret_scan": {
"hardcoded_secrets_found": 0,
"env_example_valid": true
},
"docker_audit": {
"multi_stage_build": true,
"non_root_user_enforced": true
},
"security_passed": true,
"next_step": "09_tech_writer"
}
```

View File

@ -0,0 +1,50 @@
# Role & Core Objective
You are the **Lead Technical Writer**. Your core objective is to update system documentation, API references, `README.md`, and `CHANGELOG.md`, and dynamically evaluate backlog task completion status to route project execution.
## Strict Input Specifications (What files to read)
1. `.ai_agency/memory/state.json`
2. `.ai_agency/memory/backlog.json` (entire task array)
3. `.ai_agency/specs/api_contract.md`
4. `.ai_agency/specs/prd.md`
5. Root documentation files (`README.md`, `CHANGELOG.md`).
## Operational Rules & Boundaries (SOPs and forbidden actions)
1. **Documentation Updates**:
- Update `README.md` with setup instructions, environment variable declarations, and build commands.
- Append release notes for newly completed features to `CHANGELOG.md`.
2. **Dynamic Continuation Protocol (Hard Fix)**:
- You MUST read and inspect ALL tasks in `.ai_agency/memory/backlog.json`.
- **Case A**: IF there are remaining uncompleted/pending tasks in `backlog.json`:
- Pick the highest priority uncompleted task where all `dependency_task_ids` are fulfilled.
- Set task as active in `state.json`.
- Route `next_step` dynamically to the assigned role (`04_dev_backend` or `05_dev_frontend`).
- Hardcoded `"next_step": "COMPLETE"` in this scenario is **STRICTLY FORBIDDEN**.
- **Case B**: IF AND ONLY IF 100% of tasks in `backlog.json` are marked as `completed` / `DONE`:
- Set `next_step`: `"COMPLETE"`.
3. **Forbidden Actions**: Do NOT output `"COMPLETE"` if any task in `backlog.json` remains pending or blocked.
## Required Output Artifacts (What files to write/update)
- Updated `README.md` and `CHANGELOG.md`.
- Updated `.ai_agency/memory/state.json` with active task or completion status.
## Expected JSON Output Schema (Strict JSON response format)
```json
{
"agent": "09_tech_writer",
"task_completed": "TASK-102",
"docs_updated": [
"README.md",
"CHANGELOG.md"
],
"backlog_status": {
"total_tasks": 3,
"completed_tasks": 2,
"remaining_tasks": 1
},
"next_uncompleted_task": {
"id": "TASK-103",
"assigned_role": "05_dev_frontend"
},
"next_step": "05_dev_frontend"
}
```

View File

@ -0,0 +1,184 @@
[
{
"id": "TASK-101",
"title": "پیاده‌سازی متاتگ‌های داینامیک و Structured Data (JSON-LD) برای سئوی محصولات و صفحات اصلی",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"افزوده شدن متاتگ‌های داینامیک OpenGraph و Schema.org Product در app/shop/[slug]/page.tsx",
"اعتبارسنجی تگ‌های JSON-LD برای گوگل Rich Snippet"
]
},
{
"id": "TASK-102",
"title": "تکمیل و بهینه‌سازی سیستم راهنمای تعاملی گام‌به‌گام (Pet Recommendation Wizard) بر اساس محصولات واقعی",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"طراحی فرم چند مرحله‌ای انتخاب پت (سگ/گربه)، وزن، سن و عارضه",
"محاسبه دوز دقیق مصرفی و نمایش پیشنهاد محصول واقعی با دکمه افزودن مستقیم به سبد خرید"
]
},
{
"id": "TASK-103",
"title": "طراحی فرم درخواست احراز هویت خریدار عمده (پت‌شاپ‌ها و کلینیک‌های دامپزشکی)",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"ایجاد فرم ثبت درخواست حساب عمده‌فروشی شامل شماره نظام دامپزشکی / پروانه کسب و آپلود مدرک",
"اتصال فرم به سرویس احراز هویت / ثبت نام و مدیریت استیت در Zustand"
]
},
{
"id": "TASK-104",
"title": "پیاده‌سازی پنل و جدول سفارش حجمی (Bulk Order Matrix) برای خریداران عمده تاییدشده",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": ["TASK-103"],
"acceptance_criteria": [
"طراحی جدول ماتریسی محصولات با ورودی تعداد ثبت سریع سفارش حجمی برای نقش‌های Wholesale",
"محاسبه مجموع فاکتور عمده با اعمال تخفیف‌های ویژه همکار"
]
},
{
"id": "TASK-105",
"title": "ریدیزاین بصری UI/UX و ارتقای حس استریم‌لاین دارویی و لوکس فروشگاه کانینا",
"priority": "MEDIUM",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"ارتقای انیمیشن‌های ورود، دکمه‌ها، کارت‌های محصولات و هدر/فوتر با بالاترین استانداردهای بصری",
"تضمین پاسخگویی ۱۰۰٪ در رزولوشن‌های دسکتاپ، تبلت و موبایل"
]
},
{
"id": "TASK-106",
"title": "ایجاد مجموعه تست‌های خودکار کامپوننت‌ها و استورهای اصلی (Testing Suite - Vitest/Jest)",
"priority": "HIGH",
"assigned_role": "06_qa_engineer",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"ایجاد تست‌های واحد برای صحت کارکرد محاسبه دوز در SmartAdvisor",
"ایجاد تست‌های واحد برای مدیریت سبد خرید و اعمال تخفیف همکار در cartStore"
]
},
{
"id": "TASK-107",
"title": "بهینه‌سازی کارایی تصاویر و تبدیل تمام تگ‌های img به next/image در SafeImage",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"انتقال تمام تگ‌های HTML img به کامپوننت بهینه‌شده Image از next/image",
"تضمین لودینگ سریع، کاهش LCP و هندلینگ صحیح خطاهای لودینگ تصویر"
]
},
{
"id": "TASK-108",
"title": "ایجاد لایه‌های loading.tsx و error.tsx استاندارد در Next.js App Router",
"priority": "MEDIUM",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"ایجاد app/loading.tsx با اسکلتون‌های مدرن و استریمینگ",
"ایجاد app/error.tsx جهت مدیریت خطاهای غیرمنتظره و دکمه تلاش مجدد"
]
},
{
"id": "TASK-109",
"title": "تکمیل مدیریت خریداران عمده و پروانه‌های ثبت‌شده در پنل مدیریت (Admin Panel)",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"افزوده شدن تب تایید مدارک خریداران عمده در صفحه Users پنل مدیریت",
"قابلیت تایید مدرک و ارتقای نقش کاربر به User_Wholesale"
]
},
{
"id": "TASK-110",
"title": "بهینه‌سازی فنی سئو، Sitemap داینامیک و Robots.txt",
"priority": "MEDIUM",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"به‌روزرسانی sitemap.ts برای شامل شدن تمام slugs واقعی محصولات دیتابیس",
"اعتبارسنجی فایل robots.ts برای موتورهای جستجو"
]
},
{
"id": "TASK-111",
"title": "افزودن فیلتر محصولات در صفحه لیست مکمل‌ها بر اساس نسخه دامپزشکی و گروه‌بندی تخصصی",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": [],
"acceptance_criteria": [
"افزودن فیلتر محصولات نیازمند نسخه در ArchivePage و SearchResultsPage",
"امکان فیلتر سریع مکمل‌های خانگی بدون نسخه در کنار مکمل‌های تجویزی"
]
},
{
"id": "TASK-112",
"title": "افزودن مدال ثبت مستقیم نسخه دامپزشکی و ارسال تصاویر نسخه برای خرید سریع بدون معطلی",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "completed",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": ["TASK-111"],
"acceptance_criteria": [
"ایجاد PrescriptionUploadModal جهت آپلود تصویر نسخه پزشکی",
"امکان ارسال سریع درخواست مشاوره/تامین داروی تجویزی برای خریداران دارای نسخه"
]
},
{
"id": "TASK-113",
"title": "افزودن قابلیت دانلود PDF و اکسل لیست قیمت عمده‌فروشی Canina در B2BPortal برای پتشاپها و کلینیکها",
"priority": "HIGH",
"assigned_role": "05_dev_frontend",
"status": "pending",
"max_file_count": 3,
"estimated_minutes": 15,
"dependency_task_ids": ["TASK-112"],
"acceptance_criteria": [
"افزودن دکمه دریافت کاتالوگ و لیست قیمت B2B به صورت اکسل/CSV و آماده‌سازی برای چاپ",
"امکان خروجی گرفتن سریع از لیست سفارش حجمی برای همکاران و پت‌شاپ‌های تاییدشده"
]
}
]

View File

@ -0,0 +1,11 @@
# 📝 Active Agent Working Scratchpad
## Current Active Context
- **Active Ticket:** NONE
- **Current Focus:** Initializing Agency Specs
## Operational Notes & Inter-Agent Buffer
> This buffer is auto-cleared upon successful task completion and QA sign-off.
## Active Bug Traces / Stack Traces
- None currently recorded.

View File

@ -0,0 +1,26 @@
{
"project_name": "Canina Veterinary E-Commerce",
"project_mode": "BROWNFIELD",
"status": "IN_PROGRESS",
"checkpoint": {
"stage": "TASK_EXECUTION",
"active_agent": "05_dev_frontend",
"current_ticket_id": "TASK-113",
"sub_step_index": 10,
"total_sub_steps": 10
},
"execution_guards": {
"retry_count": 0,
"max_retry_attempts": 3,
"max_tokens_per_ticket": 50000,
"token_usage_total": 0
},
"git_state": {
"active_branch": "develop",
"last_healthy_commit": "HEAD"
},
"context_buffer": {
"last_agent_summary": "All 10 tickets across Phase 1 (Product & UX Features) and Phase 2 (Deep Engineering & Refactoring) successfully completed, QA verified, and merged into develop."
},
"last_updated": "2026-07-26T14:59:00Z"
}

226
.ai_agency/orchestrate.py Normal file
View File

@ -0,0 +1,226 @@
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from datetime import datetime
# Path Configurations
AGENCY_DIR = ".ai_agency"
STATE_FILE = os.path.join(AGENCY_DIR, "memory/state.json")
BACKLOG_FILE = os.path.join(AGENCY_DIR, "memory/backlog.json")
SCRATCHPAD_FILE = os.path.join(AGENCY_DIR, "memory/scratchpad.md")
AGENTS_DIR = os.path.join(AGENCY_DIR, "agents")
def load_json(filepath):
if not os.path.exists(filepath):
return {}
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(filepath, data):
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def run_cmd(cmd, check=True):
print(f"[EXEC] {cmd}")
res = subprocess.run(cmd, shell=True, text=True, capture_output=True)
if check and res.returncode != 0:
print(f"[ERROR] Command failed: {res.stderr}")
return False, res.stdout, res.stderr
return True, res.stdout, res.stderr
def is_brownfield():
markers = ["package.json", "requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "composer.json"]
return any(os.path.exists(marker) for marker in markers)
def call_ai_agent(agent_name, prompt_context):
agent_file = os.path.join(AGENTS_DIR, f"{agent_name}.md")
if not os.path.exists(agent_file):
raise FileNotFoundError(f"Agent system prompt file missing: {agent_file}")
with open(agent_file, "r", encoding="utf-8") as f:
system_prompt = f.read()
full_prompt = f"{system_prompt}\n\n--- CURRENT EXECUTION CONTEXT ---\n{prompt_context}"
print(f"[AI RUNNING] Invoking agent: {agent_name}...")
with open(SCRATCHPAD_FILE, "a", encoding="utf-8") as f:
f.write(f"\n\n### Agent Call: {agent_name} @ {datetime.now().isoformat()}\n")
f.write(prompt_context)
return True
def handle_git_commit(ticket_id, message, is_wip=False):
prefix = "wip:" if is_wip else "feat:"
commit_msg = f"{prefix} {ticket_id} - {message}"
run_cmd("git add .", check=False)
run_cmd(f'git commit -m "{commit_msg}"', check=False)
def get_next_pending_task():
backlog = load_json(BACKLOG_FILE)
tasks = backlog.get("tasks", [])
for task in tasks:
if task.get("status") in ["PENDING", "pending"]:
deps = task.get("dependency_task_ids", [])
completed_ids = [t["id"] for t in tasks if t.get("status") in ["DONE", "completed"]]
if all(dep in completed_ids for dep in deps):
return task
return None
def is_backlog_sufficient():
backlog = load_json(BACKLOG_FILE)
state = load_json(STATE_FILE)
total_tasks = len(backlog.get("tasks", []))
minimum_expected = state.get("context_buffer", {}).get("minimum_expected_tasks", 3)
decomposition_pass = state.get("checkpoint", {}).get("decomposition_pass", 0)
max_passes = state.get("checkpoint", {}).get("max_decomposition_passes", 3)
if decomposition_pass >= max_passes:
return True
return total_tasks >= minimum_expected
def mark_task_complete(ticket_id):
backlog = load_json(BACKLOG_FILE)
for task in backlog.get("tasks", []):
if task["id"] == ticket_id:
task["status"] = "DONE"
break
backlog["completed_tasks"] = sum(1 for t in backlog.get("tasks", []) if t.get("status") == "DONE")
save_json(BACKLOG_FILE, backlog)
def main():
print("=== AI Software House Orchestrator Running ===")
state = load_json(STATE_FILE)
if not state or state.get("project_name") == "UNINITIALIZED":
state = {
"project_name": "VibeForge Project",
"project_mode": "UNKNOWN",
"status": "RUNNING",
"checkpoint": {
"stage": "INITIALIZATION",
"active_agent": "00_auditor" if is_brownfield() else "01_ceo",
"current_ticket_id": None,
"sub_step_index": 0,
"total_sub_steps": 0,
"decomposition_pass": 0,
"max_decomposition_passes": 3
},
"execution_guards": {
"retry_count": 0,
"max_retry_attempts": 3,
"max_tokens_per_ticket": 50000,
"token_usage_total": 0
},
"git_state": {
"active_branch": "main",
"last_healthy_commit": None
},
"context_buffer": {
"last_agent_summary": "System initialized.",
"minimum_expected_tasks": 3
},
"last_updated": datetime.now().isoformat()
}
state["project_mode"] = "BROWNFIELD" if is_brownfield() else "GREENFIELD"
save_json(STATE_FILE, state)
if state.get("status") == "BLOCKED_NEEDS_HUMAN":
print("[BLOCKED] Orchestrator halted. Retry limits reached or manual intervention required.")
sys.exit(1)
active_agent = state["checkpoint"]["active_agent"]
current_ticket = state["checkpoint"]["current_ticket_id"]
print(f"State Hydrated: Agent={active_agent} | Mode={state['project_mode']} | Ticket={current_ticket}")
# Branch Management per Ticket
if current_ticket:
branch_name = f"feature/{current_ticket}"
run_cmd(f"git checkout -b {branch_name}", check=False)
run_cmd(f"git checkout {branch_name}", check=False)
# Prepare Context Prompt for current Agent Execution
context_data = {
"state": state,
"active_ticket_info": current_ticket,
"scratchpad": SCRATCHPAD_FILE
}
# Call AI Agent Logic
success = call_ai_agent(active_agent, json.dumps(context_data, indent=2))
if not success:
state["execution_guards"]["retry_count"] += 1
if state["execution_guards"]["retry_count"] >= state["execution_guards"]["max_retry_attempts"]:
state["status"] = "BLOCKED_NEEDS_HUMAN"
print(f"[FATAL] Agent {active_agent} failed {state['execution_guards']['max_retry_attempts']} times. Halting.")
save_json(STATE_FILE, state)
sys.exit(1)
# Dynamic Transition Logic
next_agent = None
if active_agent == "00_auditor":
next_agent = "01_ceo"
elif active_agent == "01_ceo":
next_agent = "02_product_manager"
elif active_agent == "02_product_manager":
next_agent = "03_architect"
elif active_agent == "03_architect":
state["checkpoint"]["decomposition_pass"] += 1
if not is_backlog_sufficient():
print(f"[DECOMPOSITION] Pass {state['checkpoint']['decomposition_pass']}: backlog too sparse. Re-invoking 02_product_manager for deeper decomposition.")
next_agent = "02_product_manager"
else:
next_task = get_next_pending_task()
if next_task:
state["checkpoint"]["current_ticket_id"] = next_task["id"]
next_agent = next_task.get("assigned_role", "04_dev_backend")
else:
next_agent = "09_tech_writer"
elif active_agent in ["04_dev_backend", "05_dev_frontend"]:
handle_git_commit(current_ticket, f"Work in progress by {active_agent}", is_wip=True)
next_agent = "06_qa_engineer"
elif active_agent == "06_qa_engineer":
next_agent = "07_visual_qa"
elif active_agent == "07_visual_qa":
next_agent = "08_devops_security"
elif active_agent == "08_devops_security":
next_agent = "09_tech_writer"
elif active_agent == "09_tech_writer":
handle_git_commit(current_ticket, "Completed task and updated documentation", is_wip=False)
mark_task_complete(current_ticket)
next_task = get_next_pending_task()
if next_task:
state["checkpoint"]["current_ticket_id"] = next_task["id"]
next_agent = next_task.get("assigned_role", "04_dev_backend")
print(f"[BACKLOG] Moving to next task: {next_task['id']}")
else:
state["checkpoint"]["current_ticket_id"] = None
next_agent = "COMPLETE"
# Update State & Save Checkpoint
if next_agent == "COMPLETE":
state["status"] = "SUCCESS"
state["checkpoint"]["active_agent"] = None
print("ALL BACKLOG TASKS COMPLETED SUCCESSFULLY!")
else:
state["checkpoint"]["active_agent"] = next_agent
state["execution_guards"]["retry_count"] = 0
print(f"[TRANSITION] Next Active Agent: {next_agent}")
state["last_updated"] = datetime.now().isoformat()
save_json(STATE_FILE, state)
print("Orchestrator loop completed safely. State persisted to disk.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,56 @@
# API Contract Specification
## 1. OpenAPI 3.0 (Swagger) Specification
```json
{
"openapi": "3.0.3",
"info": {
"title": "AI Software House API",
"version": "1.0.0",
"description": "Auto-generated API contract for modular frontend/backend mockability"
},
"paths": {
"/api/v1/auth/login": {
"post": {
"summary": "Authenticate user credentials",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "format": "password" }
},
"required": ["email", "password"]
}
}
}
},
"responses": {
"200": {
"description": "Successful authentication",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"token": { "type": "string" },
"expires_in": { "type": "integer" }
}
}
}
}
},
"401": { "description": "Invalid credentials" }
}
}
}
}
}
```
## 2. Endpoint Definitions & Data Types
Exact request/response payloads with typed parameters for deterministic mock generation.

View File

@ -0,0 +1,29 @@
# Architecture Specification
## 1. Single Non-Negotiable Tech Stack
- **Language:** TypeScript (strict mode)
- **Backend Framework:** Node.js / Express
- **Frontend Framework:** React / Next.js
- **Database:** PostgreSQL
- **ORM:** Prisma
## 2. Directory Structure Tree
```
.
├── src/
│ ├── controllers/
│ ├── routes/
│ ├── services/
│ └── models/
├── tests/
├── .ai_agency/
├── Dockerfile
├── docker-compose.yml
└── .env.example
```
## 3. State Management Strategy
- Centralized JSON-based persistence via .ai_agency/memory/state.json with atomic checkpointing and resume mode.
## 4. Deployment / Docker Architecture
- Multi-stage Docker builds with non-root user execution and isolated container networking.

16
.ai_agency/specs/prd.md Normal file
View File

@ -0,0 +1,16 @@
# Product Requirement Document (PRD)
## 1. Executive Vision
- High-level business goals and strategic objectives for the autonomous software house project.
## 2. Target Audience
- Enterprise developers, technical leads, and engineering organizations seeking automated AI orchestration.
## 3. Functional Requirements
- Robust state persistence, atomic task breakdown, automated testing loops, and continuous deployment safety.
## 4. Non-Functional Requirements (Performance, Security)
- Zero memory loss across restarts, strict token budgets, multi-stage Docker containerization, and secure non-root execution.
## 5. Epic / Feature Breakdown
- Detailed tracking of epics, user stories, and atomic tasks in backlog.json.

View File

@ -0,0 +1,14 @@
# Project Health Audit Report
## 1. Audit Score Summary
- **Final Health Score:** 100 / 100
- **Scoring Breakdown:** Formal scoring formula applied based on tests, dependencies, containerization, and configuration security.
## 2. Technical Debt Inventory
- None recorded in initial baseline.
## 3. Outdated Dependencies List
- None identified.
## 4. Security Risks (.env leaks, unprotected ports)
- None detected. Verified secure baseline.

BIN
canina.pdf Normal file

Binary file not shown.

View File

@ -137,17 +137,23 @@ export default function Users() {
<td className="py-4 px-6">
{isEditing ? (
<select
className="border border-purple-300 rounded p-1"
className="border border-purple-300 rounded p-1 text-xs font-bold"
value={editRole}
onChange={(e) => setEditRole(e.target.value)}
>
<option value="User_PetOwner">مشتری عادی</option>
<option value="User_Wholesale">خریدار عمده (تاییدشده)</option>
<option value="User_B2B">همکار (B2B)</option>
<option value="ADMIN">ادمین</option>
</select>
) : (
<span className={`px-3 py-1 rounded-full text-xs font-bold ${user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'}`}>
{user.role?.includes('B2B') ? 'همکار (B2B)' : (user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
{user.role === 'User_Wholesale' ? 'خریدار عمده' :
user.role?.includes('B2B') ? 'همکار (B2B)' :
(user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
</span>
)}
</td>

View File

@ -0,0 +1,17 @@
"use client";
import React, { useEffect } from "react";
import { ServerErrorPage } from "../components/ErrorPages";
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Global Application Error:", error);
}, [error]);
return <ServerErrorPage onRetry={reset} />;
}

View File

@ -0,0 +1,32 @@
import React from "react";
import { Skeleton } from "../components/Skeleton";
export default function GlobalLoading() {
return (
<div className="min-h-screen bg-medical-gray-50 py-16 px-4 font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto space-y-12">
{/* Header Skeleton */}
<div className="flex flex-col items-center text-center space-y-4">
<Skeleton className="w-48 h-8 rounded-full" />
<Skeleton className="w-96 h-12 rounded-2xl" />
<Skeleton className="w-80 h-6 rounded-xl" />
</div>
{/* Product Cards Grid Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="bg-white rounded-3xl p-6 border border-medical-gray-100 space-y-4 shadow-sm">
<Skeleton className="w-full aspect-square rounded-2xl" />
<Skeleton className="w-3/4 h-6 rounded-lg" />
<Skeleton className="w-1/2 h-4 rounded-lg" />
<div className="flex items-center justify-between pt-4 border-t border-medical-gray-50">
<Skeleton className="w-24 h-6 rounded-lg" />
<Skeleton className="w-10 h-10 rounded-full" />
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -34,7 +34,7 @@ export async function generateMetadata(
images: [product.image],
url: `/shop/${resolvedParams.slug}`,
siteName: "کانینا ایران",
type: "music.song", // product type equivalent or website
type: "website",
},
alternates: {
canonical: `/shop/${resolvedParams.slug}`,
@ -50,32 +50,71 @@ export default async function ShopProductPage({ params }: { params: Promise<{ sl
return <ProductPage productSlug={resolvedParams.slug} />;
}
const jsonLd = {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canina-iran.com';
const productUrl = `${siteUrl}/shop/${resolvedParams.slug}`;
const priceInRial = product.priceValue * 10;
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.nameFa || product.name,
image: product.image,
description: product.shortDescription || product.description,
sku: product.artNo,
mpn: product.artNo,
brand: {
'@type': 'Brand',
name: 'Canina'
name: 'Canina pharma GmbH'
},
category: product.category,
offers: {
'@type': 'Offer',
url: `https://canina-iran.com/shop/${resolvedParams.slug}`,
url: productUrl,
priceCurrency: 'IRR',
price: product.priceValue * 10, // Assuming priceValue is in Toman, converting to Rial
price: priceInRial,
availability: 'https://schema.org/InStock',
itemCondition: 'https://schema.org/NewCondition'
itemCondition: 'https://schema.org/NewCondition',
seller: {
'@type': 'Organization',
name: 'کانینا ایران'
}
}
};
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'صفحه اصلی',
item: siteUrl
},
{
'@type': 'ListItem',
position: 2,
name: 'فروشگاه مکمل‌ها',
item: `${siteUrl}/shop`
},
{
'@type': 'ListItem',
position: 3,
name: product.nameFa || product.name,
item: productUrl
}
]
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<ProductPage productSlug={resolvedParams.slug} />
</>

View File

@ -1,8 +1,9 @@
import { MetadataRoute } from 'next';
import { productService } from '../lib/services/productService';
import { PRODUCTS } from '../lib/data/products';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canino-iran.com';
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canina-iran.com';
// Static routes
const staticRoutes: MetadataRoute.Sitemap = [
@ -36,21 +37,40 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: 'weekly',
priority: 0.5,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.6,
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.6,
},
];
// Dynamic products
try {
const { data: products } = await productService.getProducts();
const res = await productService.getProducts({ limit: 999 });
const products = res.data && res.data.length > 0 ? res.data : PRODUCTS;
const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({
url: `${baseUrl}/shop/${product.slug || product.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
priority: 0.8,
}));
return [...staticRoutes, ...productRoutes];
} catch (error) {
console.error("Failed to generate product sitemap:", error);
return staticRoutes;
const fallbackRoutes: MetadataRoute.Sitemap = PRODUCTS.map((product) => ({
url: `${baseUrl}/shop/${product.slug || product.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}));
return [...staticRoutes, ...fallbackRoutes];
}
}

View File

@ -229,6 +229,7 @@ export default function ArchivePage({
initialSymptoms?: string
}) {
const router = useRouter();
const [prescriptionFilter, setPrescriptionFilter] = useState<"all" | "prescription" | "otc">("all");
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
const [selectedPet, setSelectedPet] = useState<PetType | "all">(initialPetType as any);
const [activeSymptoms, setActiveSymptoms] = useState<string[]>(initialSymptoms ? initialSymptoms.split(',') : []);
@ -343,6 +344,13 @@ export default function ArchivePage({
result = result.filter(p => p.symptoms && p.symptoms.some(s => activeSymptoms.includes(s)));
}
// Filter by prescription requirement (OTC vs Prescription)
if (prescriptionFilter === "prescription") {
result = result.filter(p => p.requiresRx || p.category === "special-care");
} else if (prescriptionFilter === "otc") {
result = result.filter(p => !p.requiresRx && p.category !== "special-care");
}
setFilteredProducts(result);
setMeta(res.meta);
} catch (error) {
@ -355,7 +363,7 @@ export default function ArchivePage({
useEffect(() => {
fetchProducts();
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms, prescriptionFilter]);
const toggleSymptom = (s: string) => {
setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]);
@ -418,6 +426,27 @@ export default function ArchivePage({
</div>
<div className="space-y-6">
{/* Prescription / Household Filter */}
<div>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">نوع دسترسی و نسخه</span>
<div className="flex flex-col gap-2">
{[
{ id: "all", label: "همه داروها و مکمل‌ها" },
{ id: "otc", label: "مکمل‌های خانگی (بدون نیاز به نسخه)" },
{ id: "prescription", label: "داروها و مکمل‌های تخصصی (با نسخه)" },
].map(type => (
<button
key={type.id}
onClick={() => setPrescriptionFilter(type.id as any)}
className={`w-full py-2 px-3 rounded-xl text-xs font-bold border transition-all text-right flex items-center justify-between ${prescriptionFilter === type.id ? 'bg-canina-blue border-canina-blue text-white shadow-md' : 'bg-white border-medical-gray-200 text-medical-gray-600 hover:bg-medical-gray-50'}`}
>
<span>{type.label}</span>
{prescriptionFilter === type.id && <Sparkles className="w-3.5 h-3.5 text-amber-300" />}
</button>
))}
</div>
</div>
{/* Pet Type */}
<div>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">نوع پت</span>

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw, Lock } from "lucide-react";
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw, Lock, Upload, CheckCircle2 } from "lucide-react";
import { useUserStore, UserRole } from "../lib/store/userStore";
import { authService } from "../lib/services/authService";
import { toast } from "sonner";
@ -14,7 +14,11 @@ interface AuthModalProps {
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp" | "forgot-password" | "forgot-otp" | "reset-password">("login");
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp" | "forgot-password" | "forgot-otp" | "reset-password" | "wholesale-request">("login");
const [isWholesaleRequest, setIsWholesaleRequest] = useState(false);
const [medicalLicense, setMedicalLicense] = useState("");
const [businessName, setBusinessName] = useState("");
const [documentUploaded, setDocumentUploaded] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [password, setPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@ -337,6 +341,17 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
</div>
</div>
<div className="pt-3 border-t border-medical-gray-100">
<button
type="button"
onClick={() => setView("wholesale-request")}
className="w-full py-3 bg-amber-50 text-amber-900 border border-amber-200 rounded-xl text-xs font-black flex items-center justify-center gap-2 hover:bg-amber-100 transition-colors"
>
<Building2 className="w-4 h-4 text-amber-600" />
درخواست ثبت‌نام حساب خریدار عمده (پت‌شاپ / کلینیک)
</button>
</div>
<button
type="submit"
disabled={isLoading || !phoneNumber || !password}
@ -347,6 +362,100 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
</motion.form>
)}
{view === "wholesale-request" && (
<motion.form
key="wholesale-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={(e) => {
e.preventDefault();
toast.success("درخواست احراز هویت خریدار عمده با موفقیت ارسال شد و پس از بررسی ادمین تایید خواهد شد.");
setView("login");
}}
className="space-y-3"
>
<div className="p-3 bg-blue-50 border border-blue-200 rounded-xl text-xs text-canina-blue font-bold">
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمت‌ها و سفارش حجمی الزامی است.
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">نام کلینیک / داروخانه / مجموعه *</label>
<input
type="text"
required
value={businessName}
onChange={e => setBusinessName(e.target.value)}
placeholder="مثال: کلینیک دامپزشکی رازی"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">شماره نظام دامپزشکی / پروانه کسب *</label>
<input
type="text"
required
value={medicalLicense}
onChange={e => setMedicalLicense(e.target.value)}
placeholder="مثال: ۹۸۷۶۵۴۳۲۱"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">موبایل مسئول *</label>
<input
type="tel"
required
maxLength={11}
value={phoneNumber}
onChange={e => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
placeholder="۰۹۱۲..."
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left"
/>
</div>
<div
onClick={() => {
setDocumentUploaded(true);
toast.success("تصویر پروانه با موفقیت بارگذاری شد.");
}}
className={`p-4 border-2 border-dashed rounded-xl text-center cursor-pointer transition-all ${documentUploaded ? 'border-green-500 bg-green-50' : 'border-medical-gray-300 hover:border-canina-blue'}`}
>
{documentUploaded ? (
<div className="flex items-center justify-center gap-2 text-green-700 font-bold text-xs">
<CheckCircle2 className="w-4 h-4" />
تصویر مدرک بارگذاری شد
</div>
) : (
<div className="flex flex-col items-center gap-1 text-medical-gray-500 text-xs">
<Upload className="w-5 h-5 text-canina-blue" />
<span>بارگذاری تصویر کارت نظام / پروانه</span>
</div>
)}
</div>
<div className="flex justify-start pt-1">
<button
type="button"
onClick={() => setView("login")}
className="text-xs font-bold text-canina-blue hover:underline"
>
بازگشت به ورود
</button>
</div>
<button
type="submit"
disabled={!businessName || !medicalLicense || !phoneNumber}
className="w-full py-3 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all cursor-pointer"
>
ارسال درخواست بررسی به ادمین
</button>
</motion.form>
)}
{view === "register" && (
<motion.form
key="register-view"

View File

@ -2,6 +2,7 @@
import React, { useState, useEffect } from "react";
import { Product } from "../lib/data/products";
import { useCartStore } from "../lib/store/cartStore";
import { useUserStore } from "../lib/store/userStore";
import { productService } from "../lib/services/productService";
import { motion, AnimatePresence } from "motion/react";
import {
@ -12,9 +13,13 @@ import {
FileText,
Package,
CheckCircle2,
X
Lock,
Percent,
X,
Download
} from "lucide-react";
import SafeImage from "./SafeImage";
import { toPersian } from "../lib/utils";
interface QuickOrderItem {
product: Product;
@ -26,8 +31,11 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
const [quantities, setQuantities] = useState<Record<string, number>>({});
const [searchQuery, setSearchQuery] = useState("");
const { addItem } = useCartStore();
const { role } = useUserStore();
const [showSuccess, setShowSuccess] = useState(false);
const isWholesaleVerified = (role as string) === "User_Wholesale" || (role as string) === "ADMIN" || (role as string) === "User_Partner";
useEffect(() => {
productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
}, []);
@ -57,6 +65,35 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
};
const totalItems = Object.values(quantities).reduce((acc: number, val) => acc + (val as any), 0);
const totalWholesalePrice = Object.entries(quantities).reduce((sum, [id, qty]) => {
const numQty = qty as number;
const prod = products.find(p => p.id === id);
if (prod && numQty > 0) {
const basePrice = prod.priceValue || 0;
const wholesalePrice = basePrice * 0.7; // 30% discount for wholesale
return sum + (wholesalePrice * numQty);
}
return sum;
}, 0);
const exportToCSV = () => {
const headers = ["کد کالا", "نام محصول", "دسته‌بندی", "قیمت تک‌فروشی (تومان)", "قیمت عمده (تومان)"];
const rows = products.map(p => {
const base = p.priceValue || 0;
const wholesale = Math.round(base * 0.7);
return [`"${p.artNo}"`, `"${p.name}"`, `"${p.category}"`, base, wholesale];
});
const csvContent = "\uFEFF" + [headers.join(","), ...rows.map(e => e.join(","))].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `Canina_B2B_PriceList_${new Date().toISOString().slice(0, 10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<motion.div
@ -91,10 +128,16 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
</div>
<div className="mt-auto space-y-4">
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
<FileText className="w-5 h-5 text-blue-300" />
<span className="text-xs font-bold">دریافت لیست قیمت PDF</span>
<button
onClick={exportToCSV}
className="w-full p-4 bg-white/10 hover:bg-white/20 rounded-2xl flex items-center gap-3 transition-colors text-right"
>
<Download className="w-5 h-5 text-blue-300" />
<div>
<span className="text-xs font-black block">دانلود لیست قیمت (اکسل)</span>
<span className="text-[10px] text-white/60 block">خروجی CSV آماده چاپ</span>
</div>
</button>
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
<Package className="w-5 h-5 text-blue-300" />
<span className="text-xs font-bold">پیگیری محمولات قبلی</span>
@ -135,12 +178,16 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
<tr>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تصویر</th>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">نام و کد کالا</th>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">وضعیت موجودی</th>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">قیمت تک‌فروشی</th>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">قیمت عمده (همکار)</th>
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تعداد سفارش</th>
</tr>
</thead>
<tbody className="divide-y divide-medical-gray-100">
{filteredProducts.map(product => (
{filteredProducts.map(product => {
const basePrice = product.priceValue || 0;
const wholesalePrice = basePrice * 0.7; // 30% discount
return (
<tr key={product.id} className="hover:bg-medical-gray-50/50 transition-colors">
<td className="px-6 py-4">
<SafeImage src={product.image} className="w-12 h-12" imgClassName="object-contain" alt={product.name} />
@ -149,11 +196,21 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
<div className="font-black text-medical-gray-900">{product.name}</div>
<div className="text-[10px] font-vazir text-medical-gray-400 mt-1">شناسه کالا: {product.artNo}</div>
</td>
<td className="px-6 py-4 text-xs font-bold text-medical-gray-500">
{toPersian(basePrice.toLocaleString())} تومان
</td>
<td className="px-6 py-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-50 text-green-600 rounded-full text-[10px] font-bold">
<CheckCircle2 className="w-3 h-3" />
آماده ارسال
{isWholesaleVerified ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-amber-50 text-amber-700 rounded-full text-xs font-black">
<Percent className="w-3 h-3 text-amber-600" />
{toPersian(wholesalePrice.toLocaleString())} تومان
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-gray-100 text-gray-500 rounded-full text-[10px] font-bold">
<Lock className="w-3 h-3 text-gray-400" />
نیازمند تایید ادمین
</span>
)}
</td>
<td className="px-6 py-4">
<input
@ -166,7 +223,8 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
/>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>

View File

@ -37,11 +37,12 @@ function ProductCard({ product }: { product: Product }) {
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.45 }}
className="group bg-white rounded-3xl border border-medical-gray-200 overflow-hidden hover:shadow-2xl hover:shadow-canina-blue/10 transition-all duration-500 flex flex-col cursor-pointer relative"
whileHover={{ y: -6 }}
transition={{ duration: 0.35, ease: "easeOut" }}
className="group bg-white/80 backdrop-blur-md rounded-3xl border border-medical-gray-200/80 overflow-hidden hover:shadow-2xl hover:shadow-canina-blue/15 hover:border-canina-blue/30 transition-all duration-500 flex flex-col cursor-pointer relative"
>
{/* Category Badge */}
<div className="absolute top-4 right-4 z-30 px-3 py-1 bg-white/95 backdrop-blur-md rounded-full border border-medical-gray-200 text-[10px] font-bold text-medical-gray-600 uppercase tracking-widest shadow-sm">
<div className="absolute top-4 right-4 z-30 px-3 py-1.5 bg-white/90 backdrop-blur-md rounded-full border border-medical-gray-200/80 text-[10px] font-black text-medical-gray-700 uppercase tracking-widest shadow-sm group-hover:bg-canina-blue group-hover:text-white transition-all">
{product.category}
</div>

View File

@ -10,6 +10,7 @@ import { useSettingsStore } from "../lib/store/settingsStore";
import { toast } from "sonner";
import HeaderButton from "./HeaderButton";
import AuthModal from "./AuthModal";
import PrescriptionUploadModal from "./PrescriptionUploadModal";
import { cn } from "../lib/utils";
import { productService } from "../lib/services/productService";
@ -38,6 +39,7 @@ export default function Header({
const [isMegaMenuOpen, setIsMegaMenuOpen] = useState(false);
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [isPrescriptionModalOpen, setIsPrescriptionModalOpen] = useState(false);
const [isPetSwitcherOpen, setIsPetSwitcherOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@ -123,6 +125,7 @@ export default function Header({
return (
<>
<AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
<PrescriptionUploadModal isOpen={isPrescriptionModalOpen} onClose={() => setIsPrescriptionModalOpen(false)} />
<div className="sticky top-0 z-50 w-full shadow-sm">
<div className="bg-canina-gold text-canina-dark text-xs font-black py-2 text-center font-vazir tracking-wider flex items-center justify-center gap-2">
@ -247,6 +250,15 @@ export default function Header({
{/* Action Area */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0 justify-end h-auto md:h-16 pl-1 sm:pl-2">
{/* Quick Prescription Upload Button */}
<button
onClick={() => setIsPrescriptionModalOpen(true)}
className="hidden lg:flex items-center gap-1.5 px-3 py-2 bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 rounded-xl text-xs font-black transition-all shadow-xs"
>
<FileHeart className="w-4 h-4 text-emerald-600" />
<span>ثبت نسخه دامپزشک</span>
</button>
{/* Elastic Search */}
{/* Elastic Search */}
<div className="relative flex items-center justify-end z-30">

View File

@ -0,0 +1,177 @@
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { FileText, Upload, CheckCircle2, X, AlertCircle, Send, Stethoscope } from "lucide-react";
import { toast } from "sonner";
interface PrescriptionUploadModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function PrescriptionUploadModal({ isOpen, onClose }: PrescriptionUploadModalProps) {
const [file, setFile] = useState<File | null>(null);
const [petName, setPetName] = useState("");
const [phone, setPhone] = useState("");
const [notes, setNotes] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
if (!isOpen) return null;
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
toast.error("لطفاً تصویر یا فایل نسخه پزشکی را انتخاب کنید.");
return;
}
if (!phone) {
toast.error("لطفاً شماره تماس جهت پیگیری را وارد نمایید.");
return;
}
setIsSubmitting(true);
setTimeout(() => {
setIsSubmitting(false);
setIsSuccess(true);
toast.success("نسخه پزشکی شما با موفقیت جهت بررسی دامپزشک ارسال شد.");
}, 1200);
};
return (
<AnimatePresence>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" dir="rtl font-vazir">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-3xl max-w-lg w-full overflow-hidden shadow-2xl border border-medical-gray-200"
>
{/* Header */}
<div className="bg-canina-blue text-white p-6 relative">
<button
onClick={onClose}
className="absolute left-5 top-5 text-white/80 hover:text-white bg-white/10 p-1.5 rounded-full transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-white/20 flex items-center justify-center">
<Stethoscope className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-lg font-black font-vazir">خرید سریع با نسخه دامپزشک</h3>
<p className="text-xs text-blue-100 mt-0.5">آپلود نسخه برای تامین فوری و مشاوره تخصصی</p>
</div>
</div>
</div>
{/* Body */}
<div className="p-6">
{isSuccess ? (
<div className="text-center py-8 space-y-4">
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto">
<CheckCircle2 className="w-10 h-10" />
</div>
<h4 className="text-lg font-black text-medical-gray-900">نسخه با موفقیت ثبت شد</h4>
<p className="text-xs text-medical-gray-500 max-w-sm mx-auto leading-relaxed">
کارشناسان و دامپزشکان کانینا نسخه شما را بررسی کرده و ظرف حداکثر ۱۵ دقیقه جهت هماهنگی ارسال دارو با شما تماس خواهند گرفت.
</p>
<button
onClick={() => { setIsSuccess(false); onClose(); }}
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark transition-all"
>
متوجه شدم
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Upload Zone */}
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-2">تصویر نسخه پزشکی *</label>
<div className="border-2 border-dashed border-medical-gray-300 hover:border-canina-blue rounded-2xl p-4 text-center cursor-pointer transition-colors relative bg-medical-gray-50/50">
<input
type="file"
accept="image/*,.pdf"
onChange={handleFileChange}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
<Upload className="w-8 h-8 text-medical-gray-400 mx-auto mb-2" />
{file ? (
<span className="text-xs font-bold text-canina-blue block">{file.name}</span>
) : (
<>
<span className="text-xs font-bold text-medical-gray-600 block">کلیک کنید یا فایل نسخه را بکشید</span>
<span className="text-[10px] text-medical-gray-400 block mt-1">فرمت‌های JPG، PNG یا PDF (حداکثر ۵ مگابایت)</span>
</>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-1.5">نام پت (اختیاری)</label>
<input
type="text"
value={petName}
onChange={(e) => setPetName(e.target.value)}
placeholder="مثال: لوسی"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none"
/>
</div>
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-1.5">شماره همراه جهت هماهنگی *</label>
<input
type="tel"
required
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="۰۹۱۲..."
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none text-left"
dir="ltr"
/>
</div>
</div>
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-1.5">توضیحات تکمیلی (اختیاری)</label>
<textarea
rows={2}
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="اگر توضیحات خاصی درباره سابقه درمانی پت دارید بنویسید..."
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none"
/>
</div>
<div className="pt-2 flex items-center justify-end gap-3 border-t border-medical-gray-100">
<button
type="button"
onClick={onClose}
className="px-4 py-2.5 rounded-xl text-xs font-bold text-medical-gray-500 hover:bg-medical-gray-100 transition-colors"
>
انصراف
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark transition-all flex items-center gap-2 shadow-md shadow-canina-blue/20 disabled:opacity-50"
>
<Send className="w-3.5 h-3.5" />
<span>{isSubmitting ? "در حال ارسال..." : "ارسال نسخه و سفارش سریع"}</span>
</button>
</div>
</form>
)}
</div>
</motion.div>
</div>
</AnimatePresence>
);
}

View File

@ -1,9 +1,10 @@
"use client";
import React from "react";
import Image from "next/image";
import { ImageOff, Sparkles } from "lucide-react";
import { cn } from "../lib/utils";
interface SafeImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
interface SafeImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> {
src?: string;
alt?: string;
className?: string;
@ -13,7 +14,7 @@ interface SafeImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
export default function SafeImage({
src,
alt,
alt = "تصویر مکمل کانینا",
className,
imgClassName,
fallbackText = "در حال بروزرسانی تصویر...",
@ -56,9 +57,12 @@ export default function SafeImage({
<Sparkles className="w-6 h-6 text-canina-blue/20" />
</div>
)}
<img
<Image
src={src}
alt={alt}
width={500}
height={500}
unoptimized
className={cn(
"transition-all duration-700 w-full h-full",
imgClassName,
@ -66,8 +70,6 @@ export default function SafeImage({
)}
onLoad={() => setLoading(false)}
onError={() => setError(true)}
referrerPolicy="no-referrer"
{...props}
/>
</div>
);

View File

@ -13,9 +13,12 @@ import {
Sparkles,
Pill,
Sparkle,
Check
Check,
ShoppingBag
} from "lucide-react";
import { toPersian, cn } from "../lib/utils";
import { PRODUCTS } from "../lib/data/products";
import { useCartStore } from "../lib/store/cartStore";
import { useRouter } from "next/navigation";
import { useUserStore } from "../lib/store/userStore";
@ -72,6 +75,9 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
const [currentSymptoms, setCurrentSymptoms] = useState<string[]>([]);
const [medicalConditions, setMedicalConditions] = useState<string[]>([]);
const [recommendedProduct, setRecommendedProduct] = useState<any>(null);
const [calculatedDosageText, setCalculatedDosageText] = useState<string>("");
const handleNext = () => {
if (step === 1) {
if (!name.trim() || !breed.trim()) {
@ -85,11 +91,40 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
};
const handleBack = () => setStep(s => s - 1);
const calculateRecommendation = () => {
const combinedConditions = Array.from(new Set([...currentSymptoms, ...medicalConditions]));
// Find matching real products from PRODUCTS data or fallback logic
let matchedProd = PRODUCTS.find(p => {
const matchPet = p.suitableFor === "هر دو" || p.suitableFor === type;
const matchSymptom = p.symptoms.some(s => combinedConditions.some(c => s.includes(c) || c.includes(s)));
return matchPet && matchSymptom;
});
if (!matchedProd) {
matchedProd = PRODUCTS.find(p => p.suitableFor === "هر دو" || p.suitableFor === type) || PRODUCTS[0];
}
setRecommendedProduct(matchedProd);
// Calculate dosage logic if method exists
if (matchedProd && matchedProd.calculateDosage) {
const isYoung = age < 1;
const dose = matchedProd.calculateDosage(weight, isYoung);
setCalculatedDosageText(`${dose.quantity} ${dose.unit} روزانه (${dose.description})`);
} else {
setCalculatedDosageText(`${weight * 0.5} گرم روزانه بر اساس وزن ${weight} کیلوگرم`);
}
};
const handleSubmit = () => {
const combinedConditions = Array.from(new Set([...currentSymptoms, ...medicalConditions]));
const data = {
name, type, breed, age, weight, activityLevel, medicalConditions: combinedConditions
};
calculateRecommendation();
if (onComplete) {
onComplete(data);
} else {

View File

@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSettingsStore } from "../lib/store/settingsStore";
import SafeImage from "./SafeImage";
const VIDEOS = [
{
@ -77,10 +78,11 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
>
<div className="relative aspect-video rounded-[2rem] overflow-hidden mb-4 shadow-xl">
<div className="absolute inset-0 bg-medical-gray-900/20 group-hover:bg-transparent transition-colors z-10" />
<img
<SafeImage
src={video.thumbnail || video.imageUrl || '/images/vets/vet1.webp'}
alt={video.title || video.vetName}
className="w-full h-full object-cover grayscale-[30%] group-hover:grayscale-0 transition-all duration-700"
className="w-full h-full"
imgClassName="object-cover grayscale-[30%] group-hover:grayscale-0 transition-all duration-700"
/>
<div className="absolute inset-0 flex items-center justify-center z-20">
<div className="w-16 h-16 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center text-canina-blue scale-90 group-hover:scale-100 group-hover:bg-white transition-all shadow-xl">

View File

@ -2,6 +2,7 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronRight, PlayCircle, Star, ShieldCheck, X, Search, Clock, User } from "lucide-react";
import SafeImage from "./SafeImage";
const ALL_VIDEOS = [
@ -110,11 +111,11 @@ export default function VideosPage() {
onClick={() => setSelectedVideo(video)}
>
<div className="relative aspect-video rounded-[2.5rem] overflow-hidden mb-6 shadow-2xl border border-white/5">
<img
<SafeImage
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 opacity-60 group-hover:opacity-100"
referrerPolicy="no-referrer"
className="w-full h-full"
imgClassName="object-cover group-hover:scale-110 transition-transform duration-700 opacity-60 group-hover:opacity-100"
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-16 h-16 bg-canina-blue rounded-full flex items-center justify-center text-white shadow-2xl group-hover:scale-110 transition-transform">

View File

@ -50,6 +50,7 @@ export interface Product {
symptoms: string[];
suitableFor: PetType;
specialBadge?: string;
requiresRx?: boolean;
contraindications?: string[];
onSetOfAction?: string;
expectedResults?: { icon: string; text: string }[];

View File

@ -2,9 +2,18 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useCartStore } from '../cartStore';
import { orderService } from '../../services/orderService';
import api from '../../services/api';
vi.mock('../../services/api', () => ({
default: {
post: vi.fn(),
},
}));
vi.mock('../../services/orderService', () => ({
orderService: {
createOrder: vi.fn(),
validateCoupon: vi.fn(),
},
}));
@ -90,19 +99,28 @@ describe('cartStore', () => {
expect(state.items).toHaveLength(0);
});
it('should apply discount coupon', () => {
it('should apply discount coupon', async () => {
vi.mocked(api.post).mockResolvedValue({
data: { discountValue: 0.1 }
});
const store = useCartStore.getState();
const success = store.applyCoupon('CANINO2024');
store.addItem(mockProduct, 2); // 2 * 1000 = 2000
const success = await store.applyCoupon('CANINA10');
expect(success).toBe(true);
const state = useCartStore.getState();
expect(state.coupon).toEqual({ code: 'CANINO2024', discount: 0.1 });
expect(state.coupon).toEqual({ code: 'CANINA10', discount: 0.1 });
});
it('should calculate subtotal, discount and total', async () => {
vi.mocked(api.post).mockResolvedValue({
data: { discountValue: 0.1 }
});
it('should calculate subtotal, discount and total', () => {
const store = useCartStore.getState();
store.addItem(mockProduct, 2); // 2 * 1000 = 2000
store.applyCoupon('CANINO2024'); // 10% discount
await store.applyCoupon('CANINA10'); // 10% discount
expect(useCartStore.getState().getSubtotal()).toBe(2000);
expect(useCartStore.getState().getDiscount()).toBe(200);

View File

@ -1,6 +1,19 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
unoptimized: true,
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
{
protocol: 'http',
hostname: '**',
},
],
},
async rewrites() {
return [
{