Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e20bd33541 | |||
| 2376d57cbb | |||
| 7b4e46c4e4 | |||
| 3a5cfd0494 | |||
| 27bb95dea5 | |||
| 9f8695a870 |
@@ -1,15 +1,26 @@
|
|||||||
---
|
---
|
||||||
name: feature-workflow-tracer
|
name: feature-workflow-tracer
|
||||||
description: Trace and explain the end-to-end workflow of any feature or functionality in a codebase. Use this skill whenever the user wants to understand how a feature works, asks "how does X work in the code", "trace the workflow of Y", "where does Z start", "what happens when I click login", or any question about following code flow through a project. Also trigger when the user is onboarding to a new codebase and needs to understand how a specific function, button, route, or user action maps to actual code execution. Trigger even for vague questions like "how is auth handled here?" or "explain the checkout flow".
|
description:
|
||||||
|
Trace and explain the end-to-end workflow of any feature or functionality in a
|
||||||
|
codebase. Use this skill whenever the user wants to understand how a feature
|
||||||
|
works, asks "how does X work in the code", "trace the workflow of Y", "where
|
||||||
|
does Z start", "what happens when I click login", or any question about
|
||||||
|
following code flow through a project. Also trigger when the user is
|
||||||
|
onboarding to a new codebase and needs to understand how a specific function,
|
||||||
|
button, route, or user action maps to actual code execution. Trigger even for
|
||||||
|
vague questions like "how is auth handled here?" or "explain the checkout
|
||||||
|
flow".
|
||||||
---
|
---
|
||||||
|
|
||||||
# Feature Workflow Tracer
|
# Feature Workflow Tracer
|
||||||
|
|
||||||
You are a **senior software engineer** helping a new team member understand how a specific feature works end-to-end in an unfamiliar codebase.
|
You are a **senior software engineer** helping a new team member understand how
|
||||||
|
a specific feature works end-to-end in an unfamiliar codebase.
|
||||||
|
|
||||||
## Your Task
|
## Your Task
|
||||||
|
|
||||||
When the user provides a **feature name** (e.g., "login", "checkout", "forgot password"), follow these phases:
|
When the user provides a **feature name** (e.g., "login", "checkout", "forgot
|
||||||
|
password"), follow these phases:
|
||||||
|
|
||||||
### Phase 1 — Explore Project Structure
|
### Phase 1 — Explore Project Structure
|
||||||
|
|
||||||
@@ -30,9 +41,11 @@ ls src/ 2>/dev/null || ls app/ 2>/dev/null || ls pages/ 2>/dev/null
|
|||||||
```
|
```
|
||||||
|
|
||||||
Identify:
|
Identify:
|
||||||
|
|
||||||
- **Stack** (React, Laravel, Next.js, Vue, Express, etc.)
|
- **Stack** (React, Laravel, Next.js, Vue, Express, etc.)
|
||||||
- **Architecture pattern** (MVC, feature-based, domain-driven, etc.)
|
- **Architecture pattern** (MVC, feature-based, domain-driven, etc.)
|
||||||
- **Entry point** for the requested feature (route file, page component, or controller)
|
- **Entry point** for the requested feature (route file, page component, or
|
||||||
|
controller)
|
||||||
|
|
||||||
### Phase 2 — Trace the Workflow
|
### Phase 2 — Trace the Workflow
|
||||||
|
|
||||||
@@ -70,6 +83,7 @@ Produce a structured workflow report in this format:
|
|||||||
### Step-by-step Flow
|
### Step-by-step Flow
|
||||||
|
|
||||||
**1. [Action description]** — `path/to/file.ext:LINE`
|
**1. [Action description]** — `path/to/file.ext:LINE`
|
||||||
|
|
||||||
> What this step does in plain English
|
> What this step does in plain English
|
||||||
|
|
||||||
```language
|
```language
|
||||||
@@ -77,9 +91,11 @@ Produce a structured workflow report in this format:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**2. [Next step]** — `path/to/file.ext:LINE`
|
**2. [Next step]** — `path/to/file.ext:LINE`
|
||||||
|
|
||||||
> ...
|
> ...
|
||||||
|
|
||||||
*(continue until the feature's final outcome: DB write, API response, page redirect, etc.)*
|
_(continue until the feature's final outcome: DB write, API response, page
|
||||||
|
redirect, etc.)_
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
@@ -89,31 +105,35 @@ Produce a structured workflow report in this format:
|
|||||||
|
|
||||||
## Tracing Rules
|
## Tracing Rules
|
||||||
|
|
||||||
| Rule | Detail |
|
| Rule | Detail |
|
||||||
|------|--------|
|
| --------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| **Max depth** | 5 layers (UI → handler → service → repo → DB). Stop before third-party library internals. |
|
| **Max depth** | 5 layers (UI → handler → service → repo → DB). Stop before third-party library internals. |
|
||||||
| **Always cite** | Every step must have `file:line`. Never say "somewhere in the codebase." |
|
| **Always cite** | Every step must have `file:line`. Never say "somewhere in the codebase." |
|
||||||
| **Skip** | Config files, `.env`, migrations, boilerplate. Focus on application logic only. |
|
| **Skip** | Config files, `.env`, migrations, boilerplate. Focus on application logic only. |
|
||||||
| **Branches** | If a step has success/error paths, show both as `1a` and `1b`. |
|
| **Branches** | If a step has success/error paths, show both as `1a` and `1b`. |
|
||||||
| **Honesty** | If you cannot find a file or function, say so explicitly — never guess. |
|
| **Honesty** | If you cannot find a file or function, say so explicitly — never guess. |
|
||||||
|
|
||||||
## Tips for Common Stacks
|
## Tips for Common Stacks
|
||||||
|
|
||||||
### React + REST API
|
### React + REST API
|
||||||
|
|
||||||
- Start from the component with the button/form
|
- Start from the component with the button/form
|
||||||
- Find the `onClick` / `onSubmit` handler
|
- Find the `onClick` / `onSubmit` handler
|
||||||
- Follow the API call (`axios.post`, `fetch`) to the backend route
|
- Follow the API call (`axios.post`, `fetch`) to the backend route
|
||||||
- In the backend, trace: route → controller → service → model
|
- In the backend, trace: route → controller → service → model
|
||||||
|
|
||||||
### Laravel (MVC)
|
### Laravel (MVC)
|
||||||
|
|
||||||
- Start from `routes/web.php` or `routes/api.php`
|
- Start from `routes/web.php` or `routes/api.php`
|
||||||
- Follow: route → Controller method → Service (if any) → Model / DB query
|
- Follow: route → Controller method → Service (if any) → Model / DB query
|
||||||
|
|
||||||
### Next.js
|
### Next.js
|
||||||
|
|
||||||
- Start from the page in `pages/` or `app/`
|
- Start from the page in `pages/` or `app/`
|
||||||
- For server actions: trace `action=` or `use server` functions
|
- For server actions: trace `action=` or `use server` functions
|
||||||
- For API routes: trace `pages/api/` or `app/api/`
|
- For API routes: trace `pages/api/` or `app/api/`
|
||||||
|
|
||||||
### Express.js
|
### Express.js
|
||||||
|
|
||||||
- Start from route definition in `routes/` or `app.js`
|
- Start from route definition in `routes/` or `app.js`
|
||||||
- Follow: router → middleware → controller → DB call
|
- Follow: router → middleware → controller → DB call
|
||||||
|
|||||||
@@ -2,40 +2,51 @@
|
|||||||
name: learning-assistant
|
name: learning-assistant
|
||||||
description: >
|
description: >
|
||||||
A personal learning companion for students and self-learners. Use this skill
|
A personal learning companion for students and self-learners. Use this skill
|
||||||
whenever the user is studying, learning something new, asking about code, trying to
|
whenever the user is studying, learning something new, asking about code,
|
||||||
understand a concept, or exploring a technical or academic topic. Triggers include:
|
trying to understand a concept, or exploring a technical or academic topic.
|
||||||
"how does X work", "explain Y to me", "what is Z", "I'm learning about...",
|
Triggers include: "how does X work", "explain Y to me", "what is Z", "I'm
|
||||||
"can you help me understand...", "what does this code do", "I don't get X",
|
learning about...", "can you help me understand...", "what does this code do",
|
||||||
or any question that sounds like it comes from someone trying to build understanding
|
"I don't get X", or any question that sounds like it comes from someone trying
|
||||||
rather than just get a quick fact. Use this skill proactively — if the user is clearly
|
to build understanding rather than just get a quick fact. Use this skill
|
||||||
in learning mode (asking follow-up questions, exploring a topic step by step, studying
|
proactively — if the user is clearly in learning mode (asking follow-up
|
||||||
for a course), stay in this mode for the whole conversation.
|
questions, exploring a topic step by step, studying for a course), stay in
|
||||||
|
this mode for the whole conversation.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Learning Assistant
|
# Learning Assistant
|
||||||
|
|
||||||
Your job is to help the user learn effectively. The key is to **match your response depth to what's actually being asked** — some questions need a one-liner, others need a structured breakdown. Read the question carefully and pick the right mode.
|
Your job is to help the user learn effectively. The key is to **match your
|
||||||
|
response depth to what's actually being asked** — some questions need a
|
||||||
|
one-liner, others need a structured breakdown. Read the question carefully and
|
||||||
|
pick the right mode.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mode 1: Simple / factual question
|
## Mode 1: Simple / factual question
|
||||||
|
|
||||||
If the user asks a direct question with a clear, concise answer, just answer it. Don't pad it out.
|
If the user asks a direct question with a clear, concise answer, just answer it.
|
||||||
|
Don't pad it out.
|
||||||
|
|
||||||
**Examples of simple questions:**
|
**Examples of simple questions:**
|
||||||
|
|
||||||
- "What does `===` mean in JavaScript?"
|
- "What does `===` mean in JavaScript?"
|
||||||
- "What's the difference between TCP and UDP?"
|
- "What's the difference between TCP and UDP?"
|
||||||
- "What does the `final` keyword do in Java?"
|
- "What does the `final` keyword do in Java?"
|
||||||
|
|
||||||
**How to respond:** One to three sentences. Be accurate and direct. If there's a one-line example that makes it instantly clear, include it. Don't add sections or menus.
|
**How to respond:** One to three sentences. Be accurate and direct. If there's a
|
||||||
|
one-line example that makes it instantly clear, include it. Don't add sections
|
||||||
|
or menus.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mode 2: Code question
|
## Mode 2: Code question
|
||||||
|
|
||||||
If the user shares code or asks about a specific piece of code (what it does, how to use it, why it works a certain way), offer a numbered menu so they can choose what kind of help they want.
|
If the user shares code or asks about a specific piece of code (what it does,
|
||||||
|
how to use it, why it works a certain way), offer a numbered menu so they can
|
||||||
|
choose what kind of help they want.
|
||||||
|
|
||||||
**Format:**
|
**Format:**
|
||||||
|
|
||||||
```
|
```
|
||||||
[Short one-sentence explanation of what the code does]
|
[Short one-sentence explanation of what the code does]
|
||||||
|
|
||||||
@@ -47,15 +58,20 @@ What would you like to know more about?
|
|||||||
5. Something else — just ask
|
5. Something else — just ask
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep the initial explanation brief. The menu is there so the user drives the depth, not you.
|
Keep the initial explanation brief. The menu is there so the user drives the
|
||||||
|
depth, not you.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mode 3: Complex topic requiring deep explanation
|
## Mode 3: Complex topic requiring deep explanation
|
||||||
|
|
||||||
If the user asks about a topic that has multiple meaningful dimensions — a concept with theory, practice, tradeoffs, and examples all worth exploring — don't try to cover everything at once. Surface the structure first and let them pick.
|
If the user asks about a topic that has multiple meaningful dimensions — a
|
||||||
|
concept with theory, practice, tradeoffs, and examples all worth exploring —
|
||||||
|
don't try to cover everything at once. Surface the structure first and let them
|
||||||
|
pick.
|
||||||
|
|
||||||
**Format:**
|
**Format:**
|
||||||
|
|
||||||
```
|
```
|
||||||
[One or two sentences situating the topic — what it is and why it matters]
|
[One or two sentences situating the topic — what it is and why it matters]
|
||||||
|
|
||||||
@@ -67,15 +83,21 @@ Which part do you want to dig into?
|
|||||||
5. [A specific angle relevant to this topic]
|
5. [A specific angle relevant to this topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Adjust the menu options to fit the topic. Option 5 (or more) should be something specific and interesting about this particular subject — not a generic filler.
|
Adjust the menu options to fit the topic. Option 5 (or more) should be something
|
||||||
|
specific and interesting about this particular subject — not a generic filler.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## General principles
|
## General principles
|
||||||
|
|
||||||
- **Be concise by default.** If you can say it clearly in two sentences, don't write six.
|
- **Be concise by default.** If you can say it clearly in two sentences, don't
|
||||||
|
write six.
|
||||||
- **Don't front-load.** Get to the point before adding caveats or context.
|
- **Don't front-load.** Get to the point before adding caveats or context.
|
||||||
- **When you give examples, make them concrete.** Toy examples are fine; vague pseudocode is not.
|
- **When you give examples, make them concrete.** Toy examples are fine; vague
|
||||||
- **Let the user lead the depth.** Your job is to open doors, not walk through all of them at once.
|
pseudocode is not.
|
||||||
- **If a follow-up question changes the depth needed, switch modes.** The modes are a guide, not a rigid script.
|
- **Let the user lead the depth.** Your job is to open doors, not walk through
|
||||||
- **Use plain language.** Avoid unnecessary jargon. If a technical term is necessary, define it briefly the first time.
|
all of them at once.
|
||||||
|
- **If a follow-up question changes the depth needed, switch modes.** The modes
|
||||||
|
are a guide, not a rigid script.
|
||||||
|
- **Use plain language.** Avoid unnecessary jargon. If a technical term is
|
||||||
|
necessary, define it briefly the first time.
|
||||||
|
|||||||
@@ -1,132 +1,171 @@
|
|||||||
---
|
---
|
||||||
name: prompt-optimizer
|
name: prompt-optimizer
|
||||||
description: "Help users rewrite and improve AI/LLM prompts by adding specificity, context, and constraints. Trigger this skill whenever users ask to improve, rewrite, optimize, or refine prompts for AI models. Focus on making prompts clearer, more specific, and more likely to produce better AI results. Present suggestions interactively so users can choose which improvements to apply."
|
description:
|
||||||
|
"Help users rewrite and improve AI/LLM prompts by adding specificity, context,
|
||||||
|
and constraints. Trigger this skill whenever users ask to improve, rewrite,
|
||||||
|
optimize, or refine prompts for AI models. Focus on making prompts clearer,
|
||||||
|
more specific, and more likely to produce better AI results. Present
|
||||||
|
suggestions interactively so users can choose which improvements to apply."
|
||||||
---
|
---
|
||||||
|
|
||||||
# Prompt Optimizer
|
# Prompt Optimizer
|
||||||
|
|
||||||
A beginner-friendly skill for improving AI/LLM prompts to get better results.
|
A beginner-friendly skill for improving AI/LLM prompts to get better results.
|
||||||
|
|
||||||
## What This Skill Does
|
## What This Skill Does
|
||||||
|
|
||||||
This skill helps you rewrite prompts to work better with AI models like Claude. Instead of just giving you a rewritten prompt, it shows you specific improvement suggestions that you can choose to apply or skip.
|
This skill helps you rewrite prompts to work better with AI models like Claude.
|
||||||
|
Instead of just giving you a rewritten prompt, it shows you specific improvement
|
||||||
|
suggestions that you can choose to apply or skip.
|
||||||
|
|
||||||
## Key Improvements
|
## Key Improvements
|
||||||
|
|
||||||
When optimizing a prompt, focus on three main areas:
|
When optimizing a prompt, focus on three main areas:
|
||||||
|
|
||||||
### 1. **Specificity** — Making the Request Clear
|
### 1. **Specificity** — Making the Request Clear
|
||||||
|
|
||||||
Good prompts are specific about what you want. Vague prompts get vague results.
|
Good prompts are specific about what you want. Vague prompts get vague results.
|
||||||
|
|
||||||
**Example improvements:**
|
**Example improvements:**
|
||||||
- Add details about format: "Give me a bullet list of 5 items" instead of "tell me about X"
|
|
||||||
|
- Add details about format: "Give me a bullet list of 5 items" instead of "tell
|
||||||
|
me about X"
|
||||||
- Be clear about length: "Write 200 words" instead of "Write something short"
|
- Be clear about length: "Write 200 words" instead of "Write something short"
|
||||||
- Define who the audience is: "Explain this for a 10-year-old" or "Use technical language"
|
- Define who the audience is: "Explain this for a 10-year-old" or "Use technical
|
||||||
|
language"
|
||||||
|
|
||||||
### 2. **Context** — Giving the AI Background Information
|
### 2. **Context** — Giving the AI Background Information
|
||||||
|
|
||||||
More context helps the AI make better decisions.
|
More context helps the AI make better decisions.
|
||||||
|
|
||||||
**Example improvements:**
|
**Example improvements:**
|
||||||
|
|
||||||
- Explain the goal: "I'm writing a resume, so focus on professional language"
|
- Explain the goal: "I'm writing a resume, so focus on professional language"
|
||||||
- Share constraints: "We only have $500 budget" or "It needs to work on mobile"
|
- Share constraints: "We only have $500 budget" or "It needs to work on mobile"
|
||||||
- Provide background: "I already know Python but not JavaScript"
|
- Provide background: "I already know Python but not JavaScript"
|
||||||
|
|
||||||
### 3. **Constraints** — Setting Boundaries
|
### 3. **Constraints** — Setting Boundaries
|
||||||
|
|
||||||
Constraints prevent unwanted outputs.
|
Constraints prevent unwanted outputs.
|
||||||
|
|
||||||
**Example improvements:**
|
**Example improvements:**
|
||||||
|
|
||||||
- Set length limits: "Keep it under 100 words"
|
- Set length limits: "Keep it under 100 words"
|
||||||
- Specify format: "Use JSON format" or "Write as a numbered list"
|
- Specify format: "Use JSON format" or "Write as a numbered list"
|
||||||
- Define tone: "Be casual and friendly, not formal"
|
- Define tone: "Be casual and friendly, not formal"
|
||||||
- Say what NOT to include: "Don't use technical jargon"
|
- Say what NOT to include: "Don't use technical jargon"
|
||||||
|
|
||||||
## How to Use This Skill
|
## How to Use This Skill
|
||||||
|
|
||||||
1. **Share your prompt** — Give me the original prompt you want to improve
|
1. **Share your prompt** — Give me the original prompt you want to improve
|
||||||
2. **Review suggestions** — I'll show you specific improvements in each area
|
2. **Review suggestions** — I'll show you specific improvements in each area
|
||||||
3. **Choose what you like** — Pick which suggestions to apply
|
3. **Choose what you like** — Pick which suggestions to apply
|
||||||
4. **Get the final version** — I'll rewrite your prompt with your chosen improvements
|
4. **Get the final version** — I'll rewrite your prompt with your chosen
|
||||||
|
improvements
|
||||||
|
|
||||||
## Interactive Selection Process
|
## Interactive Selection Process
|
||||||
|
|
||||||
When you use this skill, you'll see:
|
When you use this skill, you'll see:
|
||||||
|
|
||||||
- **Original prompt** — Your starting point
|
- **Original prompt** — Your starting point
|
||||||
- **Improvement suggestions** — Specific changes grouped by category (Specificity, Context, Constraints)
|
- **Improvement suggestions** — Specific changes grouped by category
|
||||||
- **Preview examples** — What each change would look like with the improvement applied
|
(Specificity, Context, Constraints)
|
||||||
- **Your choices** — You pick which suggestions help most (you can apply all, some, or none)
|
- **Preview examples** — What each change would look like with the improvement
|
||||||
Then you get a rewritten prompt combining all your choices.
|
applied
|
||||||
|
- **Your choices** — You pick which suggestions help most (you can apply all,
|
||||||
|
some, or none) Then you get a rewritten prompt combining all your choices.
|
||||||
|
|
||||||
### Example Interaction
|
### Example Interaction
|
||||||
|
|
||||||
**Original:** "Write me a blog post"
|
**Original:** "Write me a blog post"
|
||||||
|
|
||||||
**Suggestions I might offer:**
|
**Suggestions I might offer:**
|
||||||
|
|
||||||
- **Specificity**: Add a topic (e.g., "about sustainable living")
|
- **Specificity**: Add a topic (e.g., "about sustainable living")
|
||||||
- **Context**: Explain your goal (e.g., "to build authority on my website")
|
- **Context**: Explain your goal (e.g., "to build authority on my website")
|
||||||
- **Constraints**: Set a word count (e.g., "800-1000 words")
|
- **Constraints**: Set a word count (e.g., "800-1000 words") **Your choice:** "I
|
||||||
**Your choice:** "I want all three — add topic, goal, and word count"
|
want all three — add topic, goal, and word count"
|
||||||
|
|
||||||
**Final rewritten prompt:**
|
**Final rewritten prompt:** "Write an 800-1000 word blog post about sustainable
|
||||||
"Write an 800-1000 word blog post about sustainable living for my website. The goal is to establish my authority on eco-friendly practices. Target an audience of people interested in reducing their carbon footprint. Include 3-4 practical tips they can implement immediately, and end with a call-to-action encouraging them to sign up for my newsletter."
|
living for my website. The goal is to establish my authority on eco-friendly
|
||||||
|
practices. Target an audience of people interested in reducing their carbon
|
||||||
|
footprint. Include 3-4 practical tips they can implement immediately, and end
|
||||||
|
with a call-to-action encouraging them to sign up for my newsletter."
|
||||||
|
|
||||||
## Tips for Best Results
|
## Tips for Best Results
|
||||||
|
|
||||||
- **Start simple** — Even small improvements help
|
- **Start simple** — Even small improvements help
|
||||||
- **Focus on your goal** — What outcome do you want?
|
- **Focus on your goal** — What outcome do you want?
|
||||||
- **Add one constraint at a time** — Too many rules can be confusing
|
- **Add one constraint at a time** — Too many rules can be confusing
|
||||||
- **Test and iterate** — Try the new prompt and see if results improve
|
- **Test and iterate** — Try the new prompt and see if results improve
|
||||||
|
|
||||||
## What Makes a Good Prompt
|
## What Makes a Good Prompt
|
||||||
|
|
||||||
A prompt becomes "good" when:
|
A prompt becomes "good" when:
|
||||||
|
|
||||||
- The AI understands exactly what you want ✓
|
- The AI understands exactly what you want ✓
|
||||||
- You've given enough context to explain why ✓
|
- You've given enough context to explain why ✓
|
||||||
- You've set boundaries to prevent bad outputs ✓
|
- You've set boundaries to prevent bad outputs ✓
|
||||||
- Someone else could read it and understand your intent ✓
|
- Someone else could read it and understand your intent ✓
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Anti-Pattern Detection
|
## Anti-Pattern Detection
|
||||||
|
|
||||||
Before suggesting improvements, scan the original prompt for these common mistakes and report them with a warning:
|
Before suggesting improvements, scan the original prompt for these common
|
||||||
|
mistakes and report them with a warning:
|
||||||
| Anti-Pattern | Description | Example |
|
|
||||||
|---|---|---|
|
| Anti-Pattern | Description | Example |
|
||||||
| Too generic | No clear subject, action, or goal | "Tell me about AI" |
|
| ---------------------- | ----------------------------------------------------- | ------------------------------------------- |
|
||||||
| Ambiguous pronouns | "it", "that", "this" with no clear referent | "Fix it so it works better" |
|
| Too generic | No clear subject, action, or goal | "Tell me about AI" |
|
||||||
| Internal contradiction | Two requirements that cancel each other out | "Be concise but cover everything in detail" |
|
| Ambiguous pronouns | "it", "that", "this" with no clear referent | "Fix it so it works better" |
|
||||||
| Missing context | Requests an action without explaining why or for whom | "Rewrite this paragraph" |
|
| Internal contradiction | Two requirements that cancel each other out | "Be concise but cover everything in detail" |
|
||||||
|
| Missing context | Requests an action without explaining why or for whom | "Rewrite this paragraph" |
|
||||||
**Output format:** Before showing improvement suggestions, print a "Detected Issues" block listing any anti-patterns found. If none are found, skip this block silently. Example:
|
|
||||||
|
**Output format:** Before showing improvement suggestions, print a "Detected
|
||||||
|
Issues" block listing any anti-patterns found. If none are found, skip this
|
||||||
|
block silently. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ Detected Issues:
|
⚠️ Detected Issues:
|
||||||
- Too generic: No output format specified
|
- Too generic: No output format specified
|
||||||
- Missing context: No audience or goal provided
|
- Missing context: No audience or goal provided
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prompt Classification
|
## Prompt Classification
|
||||||
|
|
||||||
Automatically classify the input prompt into one of these types, then tailor your improvement suggestions accordingly:
|
Automatically classify the input prompt into one of these types, then tailor
|
||||||
|
your improvement suggestions accordingly:
|
||||||
| Type | Trigger Keywords | Extra Suggestions to Offer |
|
|
||||||
|---|---|---|
|
| Type | Trigger Keywords | Extra Suggestions to Offer |
|
||||||
| `code` | write, fix, debug, refactor, implement | Programming language & version, runtime environment, input/output examples |
|
| ---------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||||
| `content` | write, blog, email, post, describe, summarize | Target audience, tone (formal/casual), word count, platform |
|
| `code` | write, fix, debug, refactor, implement | Programming language & version, runtime environment, input/output examples |
|
||||||
| `analysis` | analyze, compare, evaluate, review, assess | Criteria for evaluation, output format (table, prose), depth of detail |
|
| `content` | write, blog, email, post, describe, summarize | Target audience, tone (formal/casual), word count, platform |
|
||||||
| `qa` | explain, what is, how does, why, define | Knowledge level of audience, analogies allowed?, length of answer |
|
| `analysis` | analyze, compare, evaluate, review, assess | Criteria for evaluation, output format (table, prose), depth of detail |
|
||||||
| `task` | do, create, set up, build, generate, automate | Step-by-step vs one-shot, tools/permissions available, success criteria |
|
| `qa` | explain, what is, how does, why, define | Knowledge level of audience, analogies allowed?, length of answer |
|
||||||
|
| `task` | do, create, set up, build, generate, automate | Step-by-step vs one-shot, tools/permissions available, success criteria |
|
||||||
Show the detected type at the top of your response: `📌 Prompt type detected: [type]`
|
|
||||||
|
Show the detected type at the top of your response:
|
||||||
|
`📌 Prompt type detected: [type]`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chain-of-Thought Mode
|
## Chain-of-Thought Mode
|
||||||
|
|
||||||
Offer "Chain-of-Thought Enhancement" as an optional improvement when showing suggestions. When the user selects it, add reasoning instructions to the end of the optimized prompt based on type:
|
Offer "Chain-of-Thought Enhancement" as an optional improvement when showing
|
||||||
|
suggestions. When the user selects it, add reasoning instructions to the end of
|
||||||
| Prompt Type | Chain-of-Thought Addition |
|
the optimized prompt based on type:
|
||||||
|---|---|
|
|
||||||
| General | `"Think step by step before answering."` |
|
| Prompt Type | Chain-of-Thought Addition |
|
||||||
| Analysis | `"Explain your reasoning for each point before giving a conclusion."` |
|
| --------------- | -------------------------------------------------------------------------- |
|
||||||
| Code | `"Outline your approach and data structures before writing any code."` |
|
| General | `"Think step by step before answering."` |
|
||||||
|
| Analysis | `"Explain your reasoning for each point before giving a conclusion."` |
|
||||||
|
| Code | `"Outline your approach and data structures before writing any code."` |
|
||||||
| Decision-making | `"List pros and cons, then state your recommendation with justification."` |
|
| Decision-making | `"List pros and cons, then state your recommendation with justification."` |
|
||||||
|
|
||||||
**Why it works:** Chain-of-Thought forces the AI to externalize its reasoning process. This reduces hallucination (the AI catches its own errors mid-reasoning), makes outputs easier to verify, and produces more structured answers. Studies show CoT improves accuracy on complex tasks by 20–40%.
|
**Why it works:** Chain-of-Thought forces the AI to externalize its reasoning
|
||||||
|
process. This reduces hallucination (the AI catches its own errors
|
||||||
|
mid-reasoning), makes outputs easier to verify, and produces more structured
|
||||||
|
answers. Studies show CoT improves accuracy on complex tasks by 20–40%.
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# TODO - Add image upload field for manager add/update menu item
|
# TODO - Add image upload field for manager add/update menu item
|
||||||
|
|
||||||
- [x] Update GraphQL queries/mutations in `lib/manager-context.tsx` to include `imageUrl`
|
- [x] Update GraphQL queries/mutations in `lib/manager-context.tsx` to include
|
||||||
|
`imageUrl`
|
||||||
- [x] Update `components/organisms/manager/ProductModal.tsx`
|
- [x] Update `components/organisms/manager/ProductModal.tsx`
|
||||||
- [x] Add file input for image selection
|
- [x] Add file input for image selection
|
||||||
- [x] Add upload button to call `POST /api/file`
|
- [x] Add upload button to call `POST /api/file`
|
||||||
- [x] Store returned URL string into `form.imageUrl`
|
- [x] Store returned URL string into `form.imageUrl`
|
||||||
- [x] Show upload/loading/error states and image preview
|
- [x] Show upload/loading/error states and image preview
|
||||||
- [x] Update `components/organisms/manager/ProductsTab.tsx` to show image thumbnail in table
|
- [x] Update `components/organisms/manager/ProductsTab.tsx` to show image
|
||||||
|
thumbnail in table
|
||||||
- [x] Mark TODO progress after each step completed
|
- [x] Mark TODO progress after each step completed
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ export default function ManagerSignupPage() {
|
|||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
eateryName: "",
|
eateryName: "",
|
||||||
|
bankAccount: "", // 1. Thêm bankAccount vào state
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
eateryName: "",
|
eateryName: "",
|
||||||
|
bankAccount: "", // 2. Thêm bankAccount vào errors state
|
||||||
submit: "",
|
submit: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,9 +34,6 @@ export default function ManagerSignupPage() {
|
|||||||
fetch("/api/manager/signup")
|
fetch("/api/manager/signup")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setPageState("available");
|
setPageState("available");
|
||||||
// if (res.ok) ;
|
|
||||||
// else if (res.status === 403) setPageState("closed");
|
|
||||||
// else setPageState("error");
|
|
||||||
})
|
})
|
||||||
.catch(() => setPageState("error"));
|
.catch(() => setPageState("error"));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -54,6 +53,7 @@ export default function ManagerSignupPage() {
|
|||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
eateryName: "",
|
eateryName: "",
|
||||||
|
bankAccount: "",
|
||||||
submit: "",
|
submit: "",
|
||||||
};
|
};
|
||||||
if (!form.name.trim()) next.name = "Please enter your full name";
|
if (!form.name.trim()) next.name = "Please enter your full name";
|
||||||
@@ -65,8 +65,18 @@ export default function ManagerSignupPage() {
|
|||||||
next.password = "Password must be at least 6 characters";
|
next.password = "Password must be at least 6 characters";
|
||||||
if (!form.eateryName.trim())
|
if (!form.eateryName.trim())
|
||||||
next.eateryName = "Please enter the restaurant name";
|
next.eateryName = "Please enter the restaurant name";
|
||||||
|
|
||||||
|
if (!form.bankAccount.trim())
|
||||||
|
next.bankAccount = "Please enter your bank account number";
|
||||||
|
|
||||||
setErrors(next);
|
setErrors(next);
|
||||||
return !next.name && !next.phone && !next.password && !next.eateryName;
|
return (
|
||||||
|
!next.name &&
|
||||||
|
!next.phone &&
|
||||||
|
!next.password &&
|
||||||
|
!next.eateryName &&
|
||||||
|
!next.bankAccount
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -232,6 +242,14 @@ export default function ManagerSignupPage() {
|
|||||||
field: "eateryName" as const,
|
field: "eateryName" as const,
|
||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "bankAccount",
|
||||||
|
label: "Bank account number (example: acb-44359797)",
|
||||||
|
icon: "fa-credit-card",
|
||||||
|
placeholder: "Enter account number (for QR payment)",
|
||||||
|
field: "bankAccount" as const,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
].map(({ id, label, icon, placeholder, field, type }) => (
|
].map(({ id, label, icon, placeholder, field, type }) => (
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<label
|
<label
|
||||||
@@ -251,7 +269,7 @@ export default function ManagerSignupPage() {
|
|||||||
onChange={handleChange(field)}
|
onChange={handleChange(field)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 pl-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors[field] ? "border-red-400" : "border-(--color-border)"}`}
|
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 py-3 pl-10 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors[field] ? "border-red-400" : "border-(--color-border)"}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors[field] && (
|
{errors[field] && (
|
||||||
@@ -280,7 +298,8 @@ export default function ManagerSignupPage() {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>Processing...
|
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||||
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Register"
|
"Register"
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { SearchBar } from "@/components/molecules/search-bar";
|
import { SearchBar } from "@/components/molecules/search-bar";
|
||||||
import { ProductGrid } from "@/components/organisms/product-grid";
|
import { ProductGrid } from "@/components/organisms/product-grid";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
|
||||||
import { eateryClient } from "@/lib/apollo-clients";
|
import { eateryClient } from "@/lib/apollo-clients";
|
||||||
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import { allEateriesQuery } from "@/lib/types";
|
import { allEateriesQuery } from "@/lib/types";
|
||||||
import { gql } from "@apollo/client";
|
import { gql } from "@apollo/client";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
@@ -34,7 +34,7 @@ function StaffHomePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-background flex min-h-[calc(100vh-var(--spacing-header-height))] flex-col items-start">
|
<div className="bg-background flex min-h-[calc(100vh-var(--spacing-header-height))] flex-col items-start">
|
||||||
{/* ── Body — same as Customer ── */}
|
{/* ── Body — same as Customer ── */}
|
||||||
<main className="min-w-0 w-full flex-1 px-4 py-6 md:px-6 lg:px-8">
|
<main className="w-full min-w-0 flex-1 px-4 py-6 md:px-6 lg:px-8">
|
||||||
<div className="mb-5 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
|
<div className="mb-5 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import Button from "@/components/atoms/buttons/Button";
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
import PaymentSummaryCard from "@/components/molecules/cards/PaymentSummaryCard";
|
import PaymentSummaryCard from "@/components/molecules/cards/PaymentSummaryCard";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
|
||||||
import { useCart } from "@/lib/cart-context";
|
import { useCart } from "@/lib/cart-context";
|
||||||
import { useManager } from "@/lib/manager-context";
|
import { useManager } from "@/lib/manager-context";
|
||||||
import { MenuItemEntity } from "@/lib/types";
|
import { MenuItemEntity } from "@/lib/types";
|
||||||
@@ -20,7 +19,7 @@ export default function PaymentPage() {
|
|||||||
removeFromCart,
|
removeFromCart,
|
||||||
setQuantity,
|
setQuantity,
|
||||||
} = useCart();
|
} = useCart();
|
||||||
const { user } = useAuth();
|
|
||||||
const { products } = useManager();
|
const { products } = useManager();
|
||||||
|
|
||||||
const findProduct = (id: string): MenuItemEntity =>
|
const findProduct = (id: string): MenuItemEntity =>
|
||||||
@@ -31,8 +30,6 @@ export default function PaymentPage() {
|
|||||||
price: 0,
|
price: 0,
|
||||||
} as MenuItemEntity);
|
} as MenuItemEntity);
|
||||||
|
|
||||||
const isCustomer = user?.role === "customer";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
|
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
|
||||||
@@ -82,7 +79,6 @@ export default function PaymentPage() {
|
|||||||
quantity,
|
quantity,
|
||||||
}) => {
|
}) => {
|
||||||
const { name, description } = findProduct(id);
|
const { name, description } = findProduct(id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={id}
|
key={id}
|
||||||
@@ -102,24 +98,20 @@ export default function PaymentPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => decreaseQty(id)}
|
onClick={() => decreaseQty(id)}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
aria-label={`Decrease quantity of ${name}`}
|
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
|
||||||
value={quantity}
|
value={quantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setQuantity(id, Number(e.target.value))
|
setQuantity(id, Number(e.target.value))
|
||||||
}
|
}
|
||||||
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
||||||
title="Enter quantity"
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => increaseQty(id)}
|
onClick={() => increaseQty(id)}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
aria-label={`Increase quantity of ${name}`}
|
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
@@ -131,7 +123,6 @@ export default function PaymentPage() {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
size="md"
|
size="md"
|
||||||
style="payment"
|
style="payment"
|
||||||
aria-label={`Remove ${name} from cart`}
|
|
||||||
>
|
>
|
||||||
Delete product
|
Delete product
|
||||||
</Button>
|
</Button>
|
||||||
@@ -149,13 +140,12 @@ export default function PaymentPage() {
|
|||||||
|
|
||||||
<PaymentSummaryCard
|
<PaymentSummaryCard
|
||||||
totalPrice={totalPrice}
|
totalPrice={totalPrice}
|
||||||
isCustomer={isCustomer}
|
isCustomer={true}
|
||||||
backHref="/"
|
backHref="/"
|
||||||
eateryId={eateryId}
|
eateryId={eateryId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function CreateStaffPage() {
|
|||||||
onChange={handleChange(field)}
|
onChange={handleChange(field)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 pl-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors[field] ? "border-red-400" : "border-(--color-border)"}`}
|
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 py-3 pl-10 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors[field] ? "border-red-400" : "border-(--color-border)"}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors[field] && (
|
{errors[field] && (
|
||||||
@@ -160,7 +160,8 @@ export default function CreateStaffPage() {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>Processing...
|
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||||
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Create account"
|
"Create account"
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export default function ManagerPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{dropdownOpen && (
|
{dropdownOpen && (
|
||||||
<div className="absolute right-0 top-full z-50 mt-2 w-52 overflow-hidden rounded-2xl border border-(--color-border-light) bg-white shadow-xl">
|
<div className="absolute top-full right-0 z-50 mt-2 w-52 overflow-hidden rounded-2xl border border-(--color-border-light) bg-white shadow-xl">
|
||||||
<div className="p-1.5">
|
<div className="p-1.5">
|
||||||
{/* Tab buttons */}
|
{/* Tab buttons */}
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
|
|||||||
@@ -12,8 +12,18 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
"January", "February", "March", "April", "May", "June",
|
"January",
|
||||||
"July", "August", "September", "October", "November", "December",
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
];
|
];
|
||||||
|
|
||||||
function getMonday(d: Date): Date {
|
function getMonday(d: Date): Date {
|
||||||
@@ -31,10 +41,16 @@ function formatDateShort(d: Date): string {
|
|||||||
export default function StaffSchedulePage() {
|
export default function StaffSchedulePage() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const {
|
const {
|
||||||
view, setView, currentDate,
|
view,
|
||||||
goToNextWeek, goToPrevWeek,
|
setView,
|
||||||
goToNextMonth, goToPrevMonth,
|
currentDate,
|
||||||
goToToday, getWeeklyBudget, shifts,
|
goToNextWeek,
|
||||||
|
goToPrevWeek,
|
||||||
|
goToNextMonth,
|
||||||
|
goToPrevMonth,
|
||||||
|
goToToday,
|
||||||
|
getWeeklyBudget,
|
||||||
|
shifts,
|
||||||
} = useShift();
|
} = useShift();
|
||||||
|
|
||||||
const [selectedShift, setSelectedShift] = useState<ShiftEntity | null>(null);
|
const [selectedShift, setSelectedShift] = useState<ShiftEntity | null>(null);
|
||||||
@@ -70,7 +86,8 @@ export default function StaffSchedulePage() {
|
|||||||
// Count shifts this week for stats
|
// Count shifts this week for stats
|
||||||
const totalShiftsThisWeek = shifts.filter((s) => {
|
const totalShiftsThisWeek = shifts.filter((s) => {
|
||||||
if (!s.date) return false;
|
if (!s.date) return false;
|
||||||
const d = s.date instanceof Date ? s.date : new Date(s.date as unknown as string);
|
const d =
|
||||||
|
s.date instanceof Date ? s.date : new Date(s.date as unknown as string);
|
||||||
return d >= monday && d <= sunday;
|
return d >= monday && d <= sunday;
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
@@ -107,7 +124,9 @@ export default function StaffSchedulePage() {
|
|||||||
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<i className={`fa-solid fa-table-columns w-4 text-center ${view === "week" ? "text-(--color-primary)" : ""}`}></i>
|
<i
|
||||||
|
className={`fa-solid fa-table-columns w-4 text-center ${view === "week" ? "text-(--color-primary)" : ""}`}
|
||||||
|
></i>
|
||||||
<span className="flex-1 text-left">Weekly</span>
|
<span className="flex-1 text-left">Weekly</span>
|
||||||
{view === "week" && (
|
{view === "week" && (
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></span>
|
||||||
@@ -122,7 +141,9 @@ export default function StaffSchedulePage() {
|
|||||||
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<i className={`fa-solid fa-calendar w-4 text-center ${view === "month" ? "text-(--color-primary)" : ""}`}></i>
|
<i
|
||||||
|
className={`fa-solid fa-calendar w-4 text-center ${view === "month" ? "text-(--color-primary)" : ""}`}
|
||||||
|
></i>
|
||||||
<span className="flex-1 text-left">Monthly</span>
|
<span className="flex-1 text-left">Monthly</span>
|
||||||
{view === "month" && (
|
{view === "month" && (
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></span>
|
||||||
@@ -161,7 +182,10 @@ export default function StaffSchedulePage() {
|
|||||||
{/* Create shift button */}
|
{/* Create shift button */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setCreateDate(undefined); setCreateOpen(true); }}
|
onClick={() => {
|
||||||
|
setCreateDate(undefined);
|
||||||
|
setCreateOpen(true);
|
||||||
|
}}
|
||||||
className="mt-1 flex w-full cursor-pointer items-center gap-3 rounded-xl border-none bg-(--color-primary)/10 px-3 py-2.5 text-sm font-semibold text-(--color-primary) transition-all hover:bg-(--color-primary)/20"
|
className="mt-1 flex w-full cursor-pointer items-center gap-3 rounded-xl border-none bg-(--color-primary)/10 px-3 py-2.5 text-sm font-semibold text-(--color-primary) transition-all hover:bg-(--color-primary)/20"
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-plus w-4 text-center"></i>
|
<i className="fa-solid fa-plus w-4 text-center"></i>
|
||||||
@@ -201,15 +225,21 @@ export default function StaffSchedulePage() {
|
|||||||
<div className="border-t border-(--color-border-light) p-3">
|
<div className="border-t border-(--color-border-light) p-3">
|
||||||
<div className="flex items-center gap-3 rounded-xl bg-gray-50 p-3">
|
<div className="flex items-center gap-3 rounded-xl bg-gray-50 p-3">
|
||||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-(--color-primary)/15">
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-(--color-primary)/15">
|
||||||
<i className={`fa-solid ${isManager ? "fa-user-tie" : "fa-user"} text-sm text-(--color-primary)`}></i>
|
<i
|
||||||
|
className={`fa-solid ${isManager ? "fa-user-tie" : "fa-user"} text-sm text-(--color-primary)`}
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-foreground truncate text-sm font-semibold">
|
<p className="text-foreground truncate text-sm font-semibold">
|
||||||
{user?.name ?? "Staff"}
|
{user?.name ?? "Staff"}
|
||||||
</p>
|
</p>
|
||||||
<span className={`inline-block rounded-full px-2 py-px text-[10px] font-semibold ${
|
<span
|
||||||
isManager ? "bg-(--color-primary)/10 text-(--color-primary)" : "bg-gray-100 text-(--color-text-muted)"
|
className={`inline-block rounded-full px-2 py-px text-[10px] font-semibold ${
|
||||||
}`}>
|
isManager
|
||||||
|
? "bg-(--color-primary)/10 text-(--color-primary)"
|
||||||
|
: "bg-gray-100 text-(--color-text-muted)"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{isManager ? "Manager" : "Staff"}
|
{isManager ? "Manager" : "Staff"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,7 +339,10 @@ export default function StaffSchedulePage() {
|
|||||||
{isManager && (
|
{isManager && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setCreateDate(undefined); setCreateOpen(true); }}
|
onClick={() => {
|
||||||
|
setCreateDate(undefined);
|
||||||
|
setCreateOpen(true);
|
||||||
|
}}
|
||||||
className="hidden cursor-pointer items-center gap-2 rounded-xl border-none bg-(--color-primary) px-4 py-2 text-xs font-bold text-white shadow-sm transition hover:opacity-90 md:flex"
|
className="hidden cursor-pointer items-center gap-2 rounded-xl border-none bg-(--color-primary) px-4 py-2 text-xs font-bold text-white shadow-sm transition hover:opacity-90 md:flex"
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-plus"></i>
|
<i className="fa-solid fa-plus"></i>
|
||||||
@@ -375,7 +408,10 @@ export default function StaffSchedulePage() {
|
|||||||
<button
|
<button
|
||||||
title="Create shift"
|
title="Create shift"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setCreateDate(undefined); setCreateOpen(true); }}
|
onClick={() => {
|
||||||
|
setCreateDate(undefined);
|
||||||
|
setCreateOpen(true);
|
||||||
|
}}
|
||||||
className="fixed right-4 bottom-6 z-30 flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-(--color-primary) text-white shadow-xl transition hover:opacity-90 md:hidden"
|
className="fixed right-4 bottom-6 z-30 flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-(--color-primary) text-white shadow-xl transition hover:opacity-90 md:hidden"
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-plus text-lg"></i>
|
<i className="fa-solid fa-plus text-lg"></i>
|
||||||
@@ -388,7 +424,10 @@ export default function StaffSchedulePage() {
|
|||||||
<ShiftDetailModal
|
<ShiftDetailModal
|
||||||
shift={selectedShift}
|
shift={selectedShift}
|
||||||
isOpen={detailOpen}
|
isOpen={detailOpen}
|
||||||
onClose={() => { setDetailOpen(false); setSelectedShift(null); }}
|
onClose={() => {
|
||||||
|
setDetailOpen(false);
|
||||||
|
setSelectedShift(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ShiftCreateModal
|
<ShiftCreateModal
|
||||||
isOpen={createOpen}
|
isOpen={createOpen}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { formatPrice } from "@/app/(main)/payment/page";
|
import { formatPrice } from "@/app/(main)/payment/page";
|
||||||
import Button from "@/components/atoms/buttons/Button";
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
import { ReviewModal } from "@/components/organisms/modals";
|
import { ReviewModal } from "@/components/organisms/modals";
|
||||||
|
import { cartClient } from "@/lib/apollo-clients";
|
||||||
|
import { useCart } from "@/lib/cart-context";
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
import { useMutation } from "@apollo/client/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -12,9 +16,10 @@ export default function PaymentSummaryCard({
|
|||||||
backHref,
|
backHref,
|
||||||
eateryId,
|
eateryId,
|
||||||
}: PaymentSummaryCardProps) {
|
}: PaymentSummaryCardProps) {
|
||||||
|
const { cart, removeCart } = useCart();
|
||||||
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
||||||
|
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
|
||||||
const handlePayment = () => {
|
const handlePayment = () => {
|
||||||
// UI-only: open review modal after "payment"
|
|
||||||
if (isCustomer) {
|
if (isCustomer) {
|
||||||
setIsReviewOpen(true);
|
setIsReviewOpen(true);
|
||||||
}
|
}
|
||||||
@@ -44,7 +49,9 @@ export default function PaymentSummaryCard({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="payment"
|
style="payment"
|
||||||
onClick={handlePayment}
|
onClick={() => {
|
||||||
|
setIsQrModalOpen(true);
|
||||||
|
}}
|
||||||
icon="fa-solid fa-qrcode"
|
icon="fa-solid fa-qrcode"
|
||||||
size="md"
|
size="md"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -52,22 +59,7 @@ export default function PaymentSummaryCard({
|
|||||||
QR Code
|
QR Code
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isCustomer && (
|
<Link href={backHref || "/"} className={"col-span-2"}>
|
||||||
<Button
|
|
||||||
style="payment"
|
|
||||||
onClick={handlePayment}
|
|
||||||
icon="fa-solid fa-star"
|
|
||||||
size="md"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Review
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={backHref || "/"}
|
|
||||||
className={isCustomer ? "" : "col-span-2"}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
style="payment"
|
style="payment"
|
||||||
onClick={() => setIsReviewOpen(false)}
|
onClick={() => setIsReviewOpen(false)}
|
||||||
@@ -84,9 +76,51 @@ export default function PaymentSummaryCard({
|
|||||||
|
|
||||||
<ReviewModal
|
<ReviewModal
|
||||||
isOpen={isReviewOpen}
|
isOpen={isReviewOpen}
|
||||||
onClose={() => setIsReviewOpen(false)}
|
onClose={() => {
|
||||||
|
setIsReviewOpen(false);
|
||||||
|
|
||||||
|
removeCart();
|
||||||
|
}}
|
||||||
eateryId={eateryId}
|
eateryId={eateryId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isQrModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
|
||||||
|
<div className="animate-in fade-in zoom-in w-full max-w-sm rounded-3xl bg-white p-6 text-center shadow-2xl duration-300">
|
||||||
|
<h2 className="mb-4 text-xl font-bold">Scan to Pay</h2>
|
||||||
|
|
||||||
|
{cart.paymentQrUrl ? (
|
||||||
|
<div className="mb-4 rounded-2xl bg-gray-100 p-4">
|
||||||
|
<img
|
||||||
|
src={cart.paymentQrUrl}
|
||||||
|
alt="Payment QR Code"
|
||||||
|
className="mx-auto h-auto w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-10 text-gray-400">QR Code not available</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="mb-6 text-sm text-gray-500">
|
||||||
|
Total:{" "}
|
||||||
|
<span className="font-bold text-black">
|
||||||
|
{formatPrice(totalPrice)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsQrModalOpen(false);
|
||||||
|
setIsReviewOpen(true);
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ function formatWage(wage: number): string {
|
|||||||
return wage.toLocaleString("vi-VN");
|
return wage.toLocaleString("vi-VN");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShiftPeriod(startTime: string): "morning" | "afternoon" | "evening" {
|
function getShiftPeriod(
|
||||||
|
startTime: string,
|
||||||
|
): "morning" | "afternoon" | "evening" {
|
||||||
const h = parseInt(startTime.split(":")[0] ?? "0", 10);
|
const h = parseInt(startTime.split(":")[0] ?? "0", 10);
|
||||||
if (h < 12) return "morning";
|
if (h < 12) return "morning";
|
||||||
if (h < 18) return "afternoon";
|
if (h < 18) return "afternoon";
|
||||||
@@ -36,7 +38,11 @@ const PERIOD_STYLES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ShiftCard({ shift, compact = false, onClick }: ShiftCardProps) {
|
export default function ShiftCard({
|
||||||
|
shift,
|
||||||
|
compact = false,
|
||||||
|
onClick,
|
||||||
|
}: ShiftCardProps) {
|
||||||
const registeredCount = shift.registeredStaff?.length ?? 0;
|
const registeredCount = shift.registeredStaff?.length ?? 0;
|
||||||
const period = getShiftPeriod(shift.startTime);
|
const period = getShiftPeriod(shift.startTime);
|
||||||
const s = PERIOD_STYLES[period];
|
const s = PERIOD_STYLES[period];
|
||||||
@@ -49,12 +55,16 @@ export default function ShiftCard({ shift, compact = false, onClick }: ShiftCard
|
|||||||
className={`group w-full cursor-pointer rounded-xl border-l-[3px] bg-white text-left shadow-sm transition hover:-translate-y-px hover:shadow-md ${s.border}`}
|
className={`group w-full cursor-pointer rounded-xl border-l-[3px] bg-white text-left shadow-sm transition hover:-translate-y-px hover:shadow-md ${s.border}`}
|
||||||
>
|
>
|
||||||
<div className="px-2.5 py-2">
|
<div className="px-2.5 py-2">
|
||||||
<p className={`text-xs font-bold leading-tight ${s.text}`}>
|
<p className={`text-xs leading-tight font-bold ${s.text}`}>
|
||||||
{shift.startTime}–{shift.endTime}
|
{shift.startTime}–{shift.endTime}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 flex items-center justify-between gap-1">
|
<div className="mt-1 flex items-center justify-between gap-1">
|
||||||
<p className="text-[10px] text-(--color-text-muted)">{formatWage(shift.wage ?? 0)}đ</p>
|
<p className="text-[10px] text-(--color-text-muted)">
|
||||||
<span className={`rounded-full px-1.5 py-px text-[9px] font-semibold ${s.badge}`}>
|
{formatWage(shift.wage ?? 0)}đ
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-1.5 py-px text-[9px] font-semibold ${s.badge}`}
|
||||||
|
>
|
||||||
{registeredCount}/{shift.maxStaff}
|
{registeredCount}/{shift.maxStaff}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +92,9 @@ export default function ShiftCard({ shift, compact = false, onClick }: ShiftCard
|
|||||||
{(shift.wage ?? 0).toLocaleString("vi-VN")} VND/ca
|
{(shift.wage ?? 0).toLocaleString("vi-VN")} VND/ca
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ${s.badge}`}>
|
<span
|
||||||
|
className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ${s.badge}`}
|
||||||
|
>
|
||||||
{registeredCount}/{shift.maxStaff} staff
|
{registeredCount}/{shift.maxStaff} staff
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export default function LoginForm() {
|
|||||||
|
|
||||||
const role = (await res.text().catch(() => "")).trim().toLowerCase();
|
const role = (await res.text().catch(() => "")).trim().toLowerCase();
|
||||||
|
|
||||||
if (res.ok && (role === "customer" || role === "manager" || role === "staff")) {
|
if (
|
||||||
|
res.ok &&
|
||||||
|
(role === "customer" || role === "manager" || role === "staff")
|
||||||
|
) {
|
||||||
sessionStorage.setItem("login_phone", phone);
|
sessionStorage.setItem("login_phone", phone);
|
||||||
sessionStorage.setItem("login_role", role);
|
sessionStorage.setItem("login_role", role);
|
||||||
router.push(role === "customer" ? "/login/otp" : "/login/password");
|
router.push(role === "customer" ? "/login/otp" : "/login/password");
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export default function DeleteConfirm({
|
|||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||||
<i className="fa-solid fa-trash-can text-xl text-red-500"></i>
|
<i className="fa-solid fa-trash-can text-xl text-red-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-foreground text-base font-bold">Delete "{name}"?</h3>
|
<h3 className="text-foreground text-base font-bold">
|
||||||
|
Delete "{name}"?
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-(--color-text-muted)">
|
<p className="text-sm text-(--color-text-muted)">
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ export default function ProductsTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-(--color-text-muted)">
|
<p className="text-sm text-(--color-text-muted)">
|
||||||
Showing <strong className="text-foreground">{filtered.length}</strong>{" "}
|
Showing <strong className="text-foreground">{filtered.length}</strong> /{" "}
|
||||||
/ {products.length} items
|
{products.length} items
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|||||||
@@ -90,6 +90,44 @@ export default function ReviewsTab() {
|
|||||||
const avgRating =
|
const avgRating =
|
||||||
reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
|
reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
|
||||||
|
|
||||||
|
const CustomerName = ({ customerId }: { customerId: string }) => {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Hàm gọi API
|
||||||
|
const getName = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/customer/${customerId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Mạng lỗi hoặc không tìm thấy nhân viên");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setName(data["name"]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Lỗi khi lấy tên:", error);
|
||||||
|
setName("Lỗi tải tên");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (customerId) {
|
||||||
|
getName();
|
||||||
|
}
|
||||||
|
}, [customerId]); // Chạy lại nếu customerId thay đổi
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <span className="animate-pulse text-gray-400">...</span>;
|
||||||
|
|
||||||
|
return <span>{name || "N/A"}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Summary header */}
|
{/* Summary header */}
|
||||||
@@ -124,7 +162,7 @@ export default function ReviewsTab() {
|
|||||||
<i className="fa-solid fa-user text-xs text-(--color-primary)"></i>
|
<i className="fa-solid fa-user text-xs text-(--color-primary)"></i>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-(--color-text-secondary)">
|
<span className="text-sm font-medium text-(--color-text-secondary)">
|
||||||
{review.reviewerId}
|
<CustomerName customerId={review.reviewerId!} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||||
@@ -142,7 +180,7 @@ export default function ReviewsTab() {
|
|||||||
{review.comment}
|
{review.comment}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm italic text-(--color-text-muted)">
|
<p className="text-sm text-(--color-text-muted) italic">
|
||||||
No comment provided.
|
No comment provided.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export default function ReviewModal({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!user?.id || !eateryId) return;
|
if (!eateryId) return;
|
||||||
|
|
||||||
const input: CreateReviewInput = {
|
const input: CreateReviewInput = {
|
||||||
reviewerId: user.id,
|
reviewerId: user?.id || "",
|
||||||
eateryId,
|
eateryId,
|
||||||
rating,
|
rating,
|
||||||
...(review.trim() ? { comment: review.trim() } : {}),
|
...(review.trim() ? { comment: review.trim() } : {}),
|
||||||
@@ -169,9 +169,7 @@ export default function ReviewModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error && (
|
{error && <p className="mb-4 text-sm text-red-500">{error}</p>}
|
||||||
<p className="mb-4 text-sm text-red-500">{error}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Footer buttons */}
|
{/* Footer buttons */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { ProductCard } from "@/components/molecules/cards";
|
|||||||
import { useCart } from "@/lib/cart-context";
|
import { useCart } from "@/lib/cart-context";
|
||||||
import { useManager } from "@/lib/manager-context";
|
import { useManager } from "@/lib/manager-context";
|
||||||
|
|
||||||
|
import type { ProductGridProps } from "./ProductGrid.types";
|
||||||
|
|
||||||
function toDisplayUrl(filename: string) {
|
function toDisplayUrl(filename: string) {
|
||||||
if (!filename) return "/imgs/products/placeholder.jpg";
|
if (!filename) return "/imgs/products/placeholder.jpg";
|
||||||
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
|
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
|
||||||
return `/api/file/${filename}`;
|
return `/api/file/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { ProductGridProps } from "./ProductGrid.types";
|
|
||||||
|
|
||||||
export default function ProductGrid({
|
export default function ProductGrid({
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
isSidebarOpen = false,
|
isSidebarOpen = false,
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ import type { MobileShiftViewProps } from "./ShiftSchedule.types";
|
|||||||
|
|
||||||
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
"January", "February", "March", "April", "May", "June",
|
"January",
|
||||||
"July", "August", "September", "October", "November", "December",
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
];
|
];
|
||||||
|
|
||||||
function formatDateKey(d: Date): string {
|
function formatDateKey(d: Date): string {
|
||||||
@@ -24,8 +34,11 @@ function isToday(d: Date): boolean {
|
|||||||
return formatDateKey(d) === formatDateKey(today);
|
return formatDateKey(d) === formatDateKey(today);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps) {
|
export default function MobileShiftView({
|
||||||
const { currentDate, getShiftsForDate, goToNextMonth, goToPrevMonth } = useShift();
|
onShiftClick,
|
||||||
|
}: MobileShiftViewProps) {
|
||||||
|
const { currentDate, getShiftsForDate, goToNextMonth, goToPrevMonth } =
|
||||||
|
useShift();
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date(2026, 3, 10));
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date(2026, 3, 10));
|
||||||
|
|
||||||
const calendarDays = useMemo(() => {
|
const calendarDays = useMemo(() => {
|
||||||
@@ -39,12 +52,16 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
|
|||||||
|
|
||||||
const days: (Date | null)[] = [];
|
const days: (Date | null)[] = [];
|
||||||
for (let i = 0; i < startOffset; i++) days.push(null);
|
for (let i = 0; i < startOffset; i++) days.push(null);
|
||||||
for (let d = 1; d <= lastDay.getDate(); d++) days.push(new Date(year, month, d));
|
for (let d = 1; d <= lastDay.getDate(); d++)
|
||||||
|
days.push(new Date(year, month, d));
|
||||||
while (days.length % 7 !== 0) days.push(null);
|
while (days.length % 7 !== 0) days.push(null);
|
||||||
return days;
|
return days;
|
||||||
}, [currentDate]);
|
}, [currentDate]);
|
||||||
|
|
||||||
const selectedShifts = useMemo(() => getShiftsForDate(selectedDate), [selectedDate, getShiftsForDate]);
|
const selectedShifts = useMemo(
|
||||||
|
() => getShiftsForDate(selectedDate),
|
||||||
|
[selectedDate, getShiftsForDate],
|
||||||
|
);
|
||||||
|
|
||||||
const dayOfWeek = DAY_HEADERS[(selectedDate.getDay() + 6) % 7];
|
const dayOfWeek = DAY_HEADERS[(selectedDate.getDay() + 6) % 7];
|
||||||
|
|
||||||
@@ -96,7 +113,8 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
|
|||||||
if (!date) return <div key={`empty-${i}`} className="p-1" />;
|
if (!date) return <div key={`empty-${i}`} className="p-1" />;
|
||||||
|
|
||||||
const today = isToday(date);
|
const today = isToday(date);
|
||||||
const selected = formatDateKey(date) === formatDateKey(selectedDate);
|
const selected =
|
||||||
|
formatDateKey(date) === formatDateKey(selectedDate);
|
||||||
const dayShifts = getShiftsForDate(date);
|
const dayShifts = getShiftsForDate(date);
|
||||||
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
||||||
|
|
||||||
@@ -136,14 +154,16 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-3 flex items-center justify-between px-1">
|
<div className="mb-3 flex items-center justify-between px-1">
|
||||||
<h3 className="text-sm font-bold text-(--color-primary-dark)">
|
<h3 className="text-sm font-bold text-(--color-primary-dark)">
|
||||||
{dayOfWeek},{" "}
|
{dayOfWeek}, {selectedDate.getDate()}/{selectedDate.getMonth() + 1}/
|
||||||
{selectedDate.getDate()}/{selectedDate.getMonth() + 1}/{selectedDate.getFullYear()}
|
{selectedDate.getFullYear()}
|
||||||
</h3>
|
</h3>
|
||||||
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
|
<span
|
||||||
selectedShifts.length > 0
|
className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
|
||||||
? "bg-(--color-primary)/10 text-(--color-primary)"
|
selectedShifts.length > 0
|
||||||
: "bg-gray-100 text-(--color-text-muted)"
|
? "bg-(--color-primary)/10 text-(--color-primary)"
|
||||||
}`}>
|
: "bg-gray-100 text-(--color-text-muted)"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{selectedShifts.length} shifts
|
{selectedShifts.length} shifts
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,8 +171,12 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
|
|||||||
{selectedShifts.length === 0 ? (
|
{selectedShifts.length === 0 ? (
|
||||||
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
|
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
|
||||||
<i className="fa-regular fa-calendar-xmark mb-3 text-3xl text-gray-200"></i>
|
<i className="fa-regular fa-calendar-xmark mb-3 text-3xl text-gray-200"></i>
|
||||||
<p className="text-sm font-medium text-(--color-text-muted)">No shifts</p>
|
<p className="text-sm font-medium text-(--color-text-muted)">
|
||||||
<p className="mt-0.5 text-xs text-(--color-text-muted)">No shifts created for this day</p>
|
No shifts
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-xs text-(--color-text-muted)">
|
||||||
|
No shifts created for this day
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ function isToday(d: Date): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MonthlyCalendar({ onShiftClick, onDateSelect }: MonthlyCalendarProps) {
|
export default function MonthlyCalendar({
|
||||||
|
onShiftClick,
|
||||||
|
onDateSelect,
|
||||||
|
}: MonthlyCalendarProps) {
|
||||||
const { currentDate, getShiftsForDate } = useShift();
|
const { currentDate, getShiftsForDate } = useShift();
|
||||||
|
|
||||||
const calendarDays = useMemo(() => {
|
const calendarDays = useMemo(() => {
|
||||||
@@ -37,7 +40,8 @@ export default function MonthlyCalendar({ onShiftClick, onDateSelect }: MonthlyC
|
|||||||
|
|
||||||
const days: (Date | null)[] = [];
|
const days: (Date | null)[] = [];
|
||||||
for (let i = 0; i < startOffset; i++) days.push(null);
|
for (let i = 0; i < startOffset; i++) days.push(null);
|
||||||
for (let d = 1; d <= lastDay.getDate(); d++) days.push(new Date(year, month, d));
|
for (let d = 1; d <= lastDay.getDate(); d++)
|
||||||
|
days.push(new Date(year, month, d));
|
||||||
while (days.length % 7 !== 0) days.push(null);
|
while (days.length % 7 !== 0) days.push(null);
|
||||||
return days;
|
return days;
|
||||||
}, [currentDate]);
|
}, [currentDate]);
|
||||||
|
|||||||
@@ -21,8 +21,16 @@ function TimeInput({
|
|||||||
const hour12 = h24 === 0 ? 12 : h24 > 12 ? h24 - 12 : h24;
|
const hour12 = h24 === 0 ? 12 : h24 > 12 ? h24 - 12 : h24;
|
||||||
|
|
||||||
const emit = (newH12: number, newM: number, newIsPM: boolean) => {
|
const emit = (newH12: number, newM: number, newIsPM: boolean) => {
|
||||||
let h = newIsPM ? (newH12 === 12 ? 12 : newH12 + 12) : (newH12 === 12 ? 0 : newH12);
|
let h = newIsPM
|
||||||
onChange(`${h.toString().padStart(2, "0")}:${newM.toString().padStart(2, "0")}`);
|
? newH12 === 12
|
||||||
|
? 12
|
||||||
|
: newH12 + 12
|
||||||
|
: newH12 === 12
|
||||||
|
? 0
|
||||||
|
: newH12;
|
||||||
|
onChange(
|
||||||
|
`${h.toString().padStart(2, "0")}:${newM.toString().padStart(2, "0")}`,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,23 +41,27 @@ function TimeInput({
|
|||||||
min={1}
|
min={1}
|
||||||
max={12}
|
max={12}
|
||||||
value={hour12}
|
value={hour12}
|
||||||
onChange={(e) => emit(Math.min(12, Math.max(1, Number(e.target.value))), m, isPM)}
|
onChange={(e) =>
|
||||||
|
emit(Math.min(12, Math.max(1, Number(e.target.value))), m, isPM)
|
||||||
|
}
|
||||||
className="text-foreground w-12 border-none bg-transparent py-2.5 pl-3 text-sm outline-none"
|
className="text-foreground w-12 border-none bg-transparent py-2.5 pl-3 text-sm outline-none"
|
||||||
/>
|
/>
|
||||||
<span className="select-none text-sm text-(--color-text-muted)">:</span>
|
<span className="text-sm text-(--color-text-muted) select-none">:</span>
|
||||||
<input
|
<input
|
||||||
title={`${title} minute`}
|
title={`${title} minute`}
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={59}
|
max={59}
|
||||||
value={m.toString().padStart(2, "0")}
|
value={m.toString().padStart(2, "0")}
|
||||||
onChange={(e) => emit(hour12, Math.min(59, Math.max(0, Number(e.target.value))), isPM)}
|
onChange={(e) =>
|
||||||
|
emit(hour12, Math.min(59, Math.max(0, Number(e.target.value))), isPM)
|
||||||
|
}
|
||||||
className="text-foreground w-10 border-none bg-transparent py-2.5 text-sm outline-none"
|
className="text-foreground w-10 border-none bg-transparent py-2.5 text-sm outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => emit(hour12, m, !isPM)}
|
onClick={() => emit(hour12, m, !isPM)}
|
||||||
className="ml-1 mr-2 cursor-pointer rounded-lg border-none bg-(--color-primary)/10 px-2 py-1 text-xs font-bold text-(--color-primary) transition hover:bg-(--color-primary) hover:text-white"
|
className="mr-2 ml-1 cursor-pointer rounded-lg border-none bg-(--color-primary)/10 px-2 py-1 text-xs font-bold text-(--color-primary) transition hover:bg-(--color-primary) hover:text-white"
|
||||||
>
|
>
|
||||||
{isPM ? "PM" : "AM"}
|
{isPM ? "PM" : "AM"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useAuth } from "@/lib/auth-context";
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import { DEPARTMENTS } from "@/lib/constants";
|
import { DEPARTMENTS } from "@/lib/constants";
|
||||||
import { useShift } from "@/lib/shift-context";
|
import { useShift } from "@/lib/shift-context";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import type { ShiftDetailModalProps } from "./ShiftSchedule.types";
|
import type { ShiftDetailModalProps } from "./ShiftSchedule.types";
|
||||||
|
|
||||||
@@ -32,7 +32,9 @@ export default function ShiftDetailModal({
|
|||||||
// const dept = DEPARTMENTS.find((d) => d.id === shift.department);
|
// const dept = DEPARTMENTS.find((d) => d.id === shift.department);
|
||||||
const isManager = user?.role === "manager";
|
const isManager = user?.role === "manager";
|
||||||
const registeredStaff = shift.registeredStaff ?? [];
|
const registeredStaff = shift.registeredStaff ?? [];
|
||||||
const isRegistered = user ? registeredStaff.some((s) => s.id === user.id) : false;
|
const isRegistered = user
|
||||||
|
? registeredStaff.some((s) => s.id === user.id)
|
||||||
|
: false;
|
||||||
const isFull = registeredStaff.length >= shift.maxStaff;
|
const isFull = registeredStaff.length >= shift.maxStaff;
|
||||||
|
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
@@ -80,6 +82,44 @@ export default function ShiftDetailModal({
|
|||||||
absent: "bg-red-100 text-red-700",
|
absent: "bg-red-100 text-red-700",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StaffName = ({ staffId }: { staffId: string }) => {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Hàm gọi API
|
||||||
|
const getName = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/staff/${staffId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Mạng lỗi hoặc không tìm thấy nhân viên");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setName(data["name"]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Lỗi khi lấy tên:", error);
|
||||||
|
setName("Lỗi tải tên");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (staffId) {
|
||||||
|
getName();
|
||||||
|
}
|
||||||
|
}, [staffId]); // Chạy lại nếu staffId thay đổi
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <span className="animate-pulse text-gray-400">...</span>;
|
||||||
|
|
||||||
|
return <span>{name || "N/A"}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -126,15 +166,14 @@ export default function ShiftDetailModal({
|
|||||||
Date
|
Date
|
||||||
</p>
|
</p>
|
||||||
<p className="text-foreground mt-1 text-sm font-bold">
|
<p className="text-foreground mt-1 text-sm font-bold">
|
||||||
{parseShiftDate(shift.date as Date | string | undefined)?.toLocaleDateString(
|
{parseShiftDate(
|
||||||
"vi-VN",
|
shift.date as Date | string | undefined,
|
||||||
{
|
)?.toLocaleDateString("vi-VN", {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
},
|
}) ?? "—"}
|
||||||
) ?? "—"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl bg-gray-50 p-3">
|
<div className="rounded-xl bg-gray-50 p-3">
|
||||||
@@ -164,8 +203,7 @@ export default function ShiftDetailModal({
|
|||||||
{/* Registered staff */}
|
{/* Registered staff */}
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-semibold text-(--color-text-secondary)">
|
<p className="mb-2 text-xs font-semibold text-(--color-text-secondary)">
|
||||||
Registered staff ({registeredStaff.length}/
|
Registered staff ({registeredStaff.length}/{shift.maxStaff})
|
||||||
{shift.maxStaff})
|
|
||||||
</p>
|
</p>
|
||||||
{registeredStaff.length === 0 ? (
|
{registeredStaff.length === 0 ? (
|
||||||
<p className="text-xs text-(--color-text-muted) italic">
|
<p className="text-xs text-(--color-text-muted) italic">
|
||||||
@@ -183,7 +221,7 @@ export default function ShiftDetailModal({
|
|||||||
<i className="fa-solid fa-user text-[10px] text-(--color-primary)"></i>
|
<i className="fa-solid fa-user text-[10px] text-(--color-primary)"></i>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-foreground text-sm font-medium">
|
<span className="text-foreground text-sm font-medium">
|
||||||
{staff.staffId}
|
<StaffName staffId={staff.staffId} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isManager && (
|
{isManager && (
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ import { useMemo, useState } from "react";
|
|||||||
import type { WeeklyScheduleProps } from "./ShiftSchedule.types";
|
import type { WeeklyScheduleProps } from "./ShiftSchedule.types";
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
"January", "February", "March", "April", "May", "June",
|
"January",
|
||||||
"July", "August", "September", "October", "November", "December",
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
@@ -32,13 +42,23 @@ export default function WeeklySchedule({
|
|||||||
onCreateShift,
|
onCreateShift,
|
||||||
mobileCalendarHeader = false,
|
mobileCalendarHeader = false,
|
||||||
}: WeeklyScheduleProps) {
|
}: WeeklyScheduleProps) {
|
||||||
const { currentDate, getWeekDates, getShiftsForDate, goToNextWeek, goToPrevWeek } = useShift();
|
const {
|
||||||
|
currentDate,
|
||||||
|
getWeekDates,
|
||||||
|
getShiftsForDate,
|
||||||
|
goToNextWeek,
|
||||||
|
goToPrevWeek,
|
||||||
|
} = useShift();
|
||||||
const weekDates = getWeekDates();
|
const weekDates = getWeekDates();
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(weekDates[0] ?? currentDate);
|
const [selectedDate, setSelectedDate] = useState<Date>(
|
||||||
|
weekDates[0] ?? currentDate,
|
||||||
|
);
|
||||||
|
|
||||||
const selectedDateRef = useMemo(() => {
|
const selectedDateRef = useMemo(() => {
|
||||||
return weekDates.find((d) => d === selectedDate) ?? weekDates[0] ?? currentDate;
|
return (
|
||||||
|
weekDates.find((d) => d === selectedDate) ?? weekDates[0] ?? currentDate
|
||||||
|
);
|
||||||
}, [selectedDate, weekDates, currentDate]);
|
}, [selectedDate, weekDates, currentDate]);
|
||||||
|
|
||||||
// ── Mobile calendar header view ────────────────────────────────────────────
|
// ── Mobile calendar header view ────────────────────────────────────────────
|
||||||
@@ -83,18 +103,22 @@ export default function WeeklySchedule({
|
|||||||
onClick={() => setSelectedDate(date)}
|
onClick={() => setSelectedDate(date)}
|
||||||
className="flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none bg-transparent py-1.5 transition"
|
className="flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none bg-transparent py-1.5 transition"
|
||||||
>
|
>
|
||||||
<span className={`text-[10px] font-semibold uppercase ${i >= 5 ? "text-rose-400" : "text-(--color-text-muted)"}`}>
|
<span
|
||||||
|
className={`text-[10px] font-semibold uppercase ${i >= 5 ? "text-rose-400" : "text-(--color-text-muted)"}`}
|
||||||
|
>
|
||||||
{DAY_LABELS[i]}
|
{DAY_LABELS[i]}
|
||||||
</span>
|
</span>
|
||||||
<span className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition ${
|
<span
|
||||||
today
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition ${
|
||||||
? "bg-(--color-primary) text-white shadow-sm"
|
today
|
||||||
: active
|
? "bg-(--color-primary) text-white shadow-sm"
|
||||||
? "bg-(--color-primary)/15 text-(--color-primary)"
|
: active
|
||||||
: i >= 5
|
? "bg-(--color-primary)/15 text-(--color-primary)"
|
||||||
? "text-rose-500"
|
: i >= 5
|
||||||
: "text-(--color-text-secondary)"
|
? "text-rose-500"
|
||||||
}`}>
|
: "text-(--color-text-secondary)"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{new Date(date).getDate()}
|
{new Date(date).getDate()}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex min-h-2 items-center gap-px">
|
<div className="flex min-h-2 items-center gap-px">
|
||||||
@@ -123,12 +147,18 @@ export default function WeeklySchedule({
|
|||||||
{dayShifts.length === 0 && !onCreateShift ? (
|
{dayShifts.length === 0 && !onCreateShift ? (
|
||||||
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
|
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
|
||||||
<i className="fa-regular fa-calendar-xmark mb-2 text-3xl text-gray-200"></i>
|
<i className="fa-regular fa-calendar-xmark mb-2 text-3xl text-gray-200"></i>
|
||||||
<p className="text-sm text-(--color-text-muted)">No shifts today</p>
|
<p className="text-sm text-(--color-text-muted)">
|
||||||
|
No shifts today
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
{dayShifts.map((shift) => (
|
{dayShifts.map((shift) => (
|
||||||
<ShiftCard key={shift.id} shift={shift} onClick={onShiftClick} />
|
<ShiftCard
|
||||||
|
key={shift.id}
|
||||||
|
shift={shift}
|
||||||
|
onClick={onShiftClick}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{onCreateShift && (
|
{onCreateShift && (
|
||||||
<button
|
<button
|
||||||
@@ -169,18 +199,23 @@ export default function WeeklySchedule({
|
|||||||
: "bg-gray-50/80 font-semibold text-(--color-text-muted)"
|
: "bg-gray-50/80 font-semibold text-(--color-text-muted)"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="block text-[11px] font-semibold uppercase tracking-wider">
|
<span className="block text-[11px] font-semibold tracking-wider uppercase">
|
||||||
{DAY_LABELS[i]}
|
{DAY_LABELS[i]}
|
||||||
</span>
|
</span>
|
||||||
<span className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold ${
|
<span
|
||||||
today
|
className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold ${
|
||||||
? "bg-(--color-primary) text-white shadow-sm"
|
today
|
||||||
: "text-(--color-text-secondary)"
|
? "bg-(--color-primary) text-white shadow-sm"
|
||||||
}`}>
|
: "text-(--color-text-secondary)"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{date.getDate()}
|
{date.getDate()}
|
||||||
</span>
|
</span>
|
||||||
<span className={`mt-0.5 block text-[10px] font-normal ${today ? "text-(--color-primary)/70" : "text-(--color-text-muted)"}`}>
|
<span
|
||||||
{(date.getMonth() + 1).toString().padStart(2, "0")}/{date.getFullYear()}
|
className={`mt-0.5 block text-[10px] font-normal ${today ? "text-(--color-primary)/70" : "text-(--color-text-muted)"}`}
|
||||||
|
>
|
||||||
|
{(date.getMonth() + 1).toString().padStart(2, "0")}/
|
||||||
|
{date.getFullYear()}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
@@ -189,11 +224,16 @@ export default function WeeklySchedule({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{DEPARTMENTS.map((dept, deptIdx) => (
|
{DEPARTMENTS.map((dept, deptIdx) => (
|
||||||
<tr key={dept.id} className={deptIdx % 2 === 0 ? "bg-white" : "bg-gray-50/40"}>
|
<tr
|
||||||
|
key={dept.id}
|
||||||
|
className={deptIdx % 2 === 0 ? "bg-white" : "bg-gray-50/40"}
|
||||||
|
>
|
||||||
<td className="border-r border-b border-(--color-border-light) px-4 py-3 align-top">
|
<td className="border-r border-b border-(--color-border-light) px-4 py-3 align-top">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-(--color-primary)/10">
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-(--color-primary)/10">
|
||||||
<i className={`${dept.icon} text-xs text-(--color-primary)`}></i>
|
<i
|
||||||
|
className={`${dept.icon} text-xs text-(--color-primary)`}
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-semibold text-(--color-text-secondary)">
|
<span className="text-xs font-semibold text-(--color-text-secondary)">
|
||||||
{dept.name}
|
{dept.name}
|
||||||
@@ -212,7 +252,12 @@ export default function WeeklySchedule({
|
|||||||
>
|
>
|
||||||
<div className="flex min-h-[88px] flex-col gap-1.5">
|
<div className="flex min-h-[88px] flex-col gap-1.5">
|
||||||
{shifts.map((shift) => (
|
{shifts.map((shift) => (
|
||||||
<ShiftCard key={shift.id} shift={shift} compact onClick={onShiftClick} />
|
<ShiftCard
|
||||||
|
key={shift.id}
|
||||||
|
shift={shift}
|
||||||
|
compact
|
||||||
|
onClick={onShiftClick}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{onCreateShift && (
|
{onCreateShift && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CartFab } from "@/components/organisms/cart";
|
import { CartFab } from "@/components/organisms/cart";
|
||||||
import Footer from "@/layouts/footer";
|
import Footer from "@/layouts/footer";
|
||||||
import Header from "@/layouts/header";
|
import Header from "@/layouts/header";
|
||||||
|
import { ManagerProvider } from "@/lib/manager-context";
|
||||||
|
|
||||||
import type { MainLayoutProps } from "./MainLayout.types";
|
import type { MainLayoutProps } from "./MainLayout.types";
|
||||||
|
|
||||||
@@ -12,7 +13,9 @@ export default function MainLayout({ children }: MainLayoutProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Sticky top header */}
|
{/* Sticky top header */}
|
||||||
<Header />
|
<ManagerProvider>
|
||||||
|
<Header />
|
||||||
|
</ManagerProvider>
|
||||||
|
|
||||||
{/* Page content (grows to fill remaining height) */}
|
{/* Page content (grows to fill remaining height) */}
|
||||||
<div className="flex-1">{children}</div>
|
<div className="flex-1">{children}</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: frontend-container
|
- name: frontend-container
|
||||||
image: git.demonkernel.io.vn/foodsurf/frontend:1.2.9
|
image: git.demonkernel.io.vn/foodsurf/frontend:1.2.13
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 3000
|
- containerPort: 3000
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
+4
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useAuth } from "@/lib/auth-context";
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import { SHOP_INFO } from "@/lib/constants";
|
import { SHOP_INFO } from "@/lib/constants";
|
||||||
|
import { useManager } from "@/lib/manager-context";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -25,6 +26,7 @@ import { useRouter } from "next/navigation";
|
|||||||
*/
|
*/
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { eatery } = useManager();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const handleAuthClick = () => {
|
const handleAuthClick = () => {
|
||||||
@@ -47,7 +49,7 @@ export default function Header() {
|
|||||||
<div className="relative h-10 w-10 shrink-0 md:h-11 md:w-11">
|
<div className="relative h-10 w-10 shrink-0 md:h-11 md:w-11">
|
||||||
<Image
|
<Image
|
||||||
src={SHOP_INFO.logo}
|
src={SHOP_INFO.logo}
|
||||||
alt={`${SHOP_INFO.name} logo`}
|
alt={`${eatery?.name} logo`}
|
||||||
fill
|
fill
|
||||||
className="object-contain transition-transform duration-200 group-hover:scale-105"
|
className="object-contain transition-transform duration-200 group-hover:scale-105"
|
||||||
sizes="44px"
|
sizes="44px"
|
||||||
@@ -58,7 +60,7 @@ export default function Header() {
|
|||||||
{/* Name + tagline */}
|
{/* Name + tagline */}
|
||||||
<div className="flex flex-col leading-tight">
|
<div className="flex flex-col leading-tight">
|
||||||
<span className="text-base font-bold text-(--color-primary-dark) transition-colors duration-150 group-hover:text-(--color-primary) md:text-lg">
|
<span className="text-base font-bold text-(--color-primary-dark) transition-colors duration-150 group-hover:text-(--color-primary) md:text-lg">
|
||||||
{SHOP_INFO.name}
|
{eatery?.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden text-xs text-(--color-text-muted) md:block">
|
<span className="hidden text-xs text-(--color-text-muted) md:block">
|
||||||
{SHOP_INFO.tagline}
|
{SHOP_INFO.tagline}
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ interface CartContextValue {
|
|||||||
items: CartItemEntity[];
|
items: CartItemEntity[];
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
|
cart: CartEntity;
|
||||||
eateryId: string | undefined;
|
eateryId: string | undefined;
|
||||||
addToCart: (product: CartItemEntity) => void;
|
addToCart: (product: CartItemEntity) => void;
|
||||||
increaseQty: (id: string) => void;
|
increaseQty: (id: string) => void;
|
||||||
decreaseQty: (id: string) => void;
|
decreaseQty: (id: string) => void;
|
||||||
removeFromCart: (id: string) => void;
|
removeFromCart: (id: string) => void;
|
||||||
setQuantity: (id: string, quantity: number) => void;
|
setQuantity: (id: string, quantity: number) => void;
|
||||||
|
removeCart: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CART_ID = "cartId";
|
const CART_ID = "cartId";
|
||||||
@@ -120,6 +122,7 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Lỗi khi tạo giỏ hàng:", err);
|
console.error("Lỗi khi tạo giỏ hàng:", err);
|
||||||
|
localStorage.removeItem(CART_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -151,6 +154,25 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (result) setCart(result.addItem);
|
if (result) setCart(result.addItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DELETE_CART = gql`
|
||||||
|
mutation deleteCart($cartId: String!) {
|
||||||
|
deleteCart(cartId: $cartId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [mutateDeleteCart] = useMutation(DELETE_CART, {
|
||||||
|
client: cartClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeCart = async () => {
|
||||||
|
localStorage.removeItem(CART_ID);
|
||||||
|
|
||||||
|
await mutateDeleteCart({ variables: { cartId } });
|
||||||
|
|
||||||
|
setCartId(null);
|
||||||
|
setCart(null!);
|
||||||
|
};
|
||||||
|
|
||||||
const setQuantity = async (id: string, newQuantity: number) => {
|
const setQuantity = async (id: string, newQuantity: number) => {
|
||||||
if (!cartId) return;
|
if (!cartId) return;
|
||||||
|
|
||||||
@@ -191,6 +213,8 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
|
|||||||
decreaseQty,
|
decreaseQty,
|
||||||
removeFromCart,
|
removeFromCart,
|
||||||
setQuantity,
|
setQuantity,
|
||||||
|
removeCart,
|
||||||
|
cart,
|
||||||
}),
|
}),
|
||||||
[cart?.items, cart?.totalAmount, cart?.eateryId],
|
[cart?.items, cart?.totalAmount, cart?.eateryId],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { eateryClient } from "./apollo-clients";
|
import { eateryClient } from "./apollo-clients";
|
||||||
import {
|
import {
|
||||||
|
EateryEntity,
|
||||||
type MenuItemEntity,
|
type MenuItemEntity,
|
||||||
type addMenuItemMutation,
|
type addMenuItemMutation,
|
||||||
type allEateriesQuery,
|
type allEateriesQuery,
|
||||||
@@ -27,6 +28,7 @@ interface ManagerContextType {
|
|||||||
// Data
|
// Data
|
||||||
products: MenuItemEntity[];
|
products: MenuItemEntity[];
|
||||||
eateryId: string | null;
|
eateryId: string | null;
|
||||||
|
eatery?: EateryEntity;
|
||||||
|
|
||||||
// Active tab
|
// Active tab
|
||||||
activeTab: ManagerTab;
|
activeTab: ManagerTab;
|
||||||
@@ -49,6 +51,7 @@ const GET_EATERY_MENU = gql`
|
|||||||
query GetEateryMenu {
|
query GetEateryMenu {
|
||||||
allEateries {
|
allEateries {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
menuItems {
|
menuItems {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -97,6 +100,7 @@ const DELETE_MENU_ITEM = gql`
|
|||||||
|
|
||||||
export function ManagerProvider({ children }: { children: ReactNode }) {
|
export function ManagerProvider({ children }: { children: ReactNode }) {
|
||||||
const [products, setProducts] = useState<MenuItemEntity[]>([]);
|
const [products, setProducts] = useState<MenuItemEntity[]>([]);
|
||||||
|
const [eatery, setEatery] = useState<EateryEntity>();
|
||||||
const [activeTab, setActiveTab] = useState<ManagerTab>("products");
|
const [activeTab, setActiveTab] = useState<ManagerTab>("products");
|
||||||
|
|
||||||
const { data } = useQuery<allEateriesQuery>(GET_EATERY_MENU, {
|
const { data } = useQuery<allEateriesQuery>(GET_EATERY_MENU, {
|
||||||
@@ -123,6 +127,7 @@ export function ManagerProvider({ children }: { children: ReactNode }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.allEateries?.[0]) {
|
if (data?.allEateries?.[0]) {
|
||||||
setProducts(data.allEateries[0].menuItems);
|
setProducts(data.allEateries[0].menuItems);
|
||||||
|
setEatery(data.allEateries[0]);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -193,6 +198,7 @@ export function ManagerProvider({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<ManagerContext.Provider
|
<ManagerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
eatery,
|
||||||
products,
|
products,
|
||||||
eateryId,
|
eateryId,
|
||||||
activeTab,
|
activeTab,
|
||||||
|
|||||||
+71
-47
@@ -177,9 +177,12 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
client: eateryClient,
|
client: eateryClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [mutateDeleteShift] = useMutation<{ deleteShift: boolean }>(DELETE_SHIFT, {
|
const [mutateDeleteShift] = useMutation<{ deleteShift: boolean }>(
|
||||||
client: eateryClient,
|
DELETE_SHIFT,
|
||||||
});
|
{
|
||||||
|
client: eateryClient,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const [mutateRegisterShift] = useMutation<
|
const [mutateRegisterShift] = useMutation<
|
||||||
{ registerShift: { staffId: string } },
|
{ registerShift: { staffId: string } },
|
||||||
@@ -285,9 +288,13 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
const weekKeys = new Set(weekDates.map(formatDate));
|
const weekKeys = new Set(weekDates.map(formatDate));
|
||||||
return shifts
|
return shifts
|
||||||
.filter(
|
.filter(
|
||||||
(s) => weekKeys.has(dateKey(s.date)) && (s.registeredStaff ?? []).length > 0,
|
(s) =>
|
||||||
|
weekKeys.has(dateKey(s.date)) && (s.registeredStaff ?? []).length > 0,
|
||||||
)
|
)
|
||||||
.reduce((sum, s) => sum + (s.wage ?? 0) * (s.registeredStaff ?? []).length, 0);
|
.reduce(
|
||||||
|
(sum, s) => sum + (s.wage ?? 0) * (s.registeredStaff ?? []).length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
}, [shifts, getWeekDates]);
|
}, [shifts, getWeekDates]);
|
||||||
|
|
||||||
// ── Shift actions ───────────────────────────────────────────────────────
|
// ── Shift actions ───────────────────────────────────────────────────────
|
||||||
@@ -311,7 +318,13 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
shift.date &&
|
shift.date &&
|
||||||
hasConflict(shift.date, shift.startTime, shift.endTime, staffId, shiftId)
|
hasConflict(
|
||||||
|
shift.date,
|
||||||
|
shift.startTime,
|
||||||
|
shift.endTime,
|
||||||
|
staffId,
|
||||||
|
shiftId,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -338,7 +351,9 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
setShifts((prev) =>
|
setShifts((prev) =>
|
||||||
prev.map((s) => {
|
prev.map((s) => {
|
||||||
if (s.id !== shiftId) return s;
|
if (s.id !== shiftId) return s;
|
||||||
const updated = (s.registeredStaff ?? []).filter((rs) => rs.id !== staffId);
|
const updated = (s.registeredStaff ?? []).filter(
|
||||||
|
(rs) => rs.id !== staffId,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
registeredStaff: updated,
|
registeredStaff: updated,
|
||||||
@@ -347,51 +362,60 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createShift = useCallback(async (shift: Omit<ShiftEntity, "id">) => {
|
const createShift = useCallback(
|
||||||
try {
|
async (shift: Omit<ShiftEntity, "id">) => {
|
||||||
const { data } = await mutateCreateShift({
|
try {
|
||||||
variables: {
|
const { data } = await mutateCreateShift({
|
||||||
shiftInput: {
|
variables: {
|
||||||
date: toLocalDateISO(shift.date ?? new Date()),
|
shiftInput: {
|
||||||
startTime: shift.startTime,
|
date: toLocalDateISO(shift.date ?? new Date()),
|
||||||
endTime: shift.endTime,
|
startTime: shift.startTime,
|
||||||
maxStaff: shift.maxStaff,
|
endTime: shift.endTime,
|
||||||
wage: shift.wage,
|
maxStaff: shift.maxStaff,
|
||||||
|
wage: shift.wage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// Optimistic: dùng date từ server nếu có, fallback sang local
|
// Optimistic: dùng date từ server nếu có, fallback sang local
|
||||||
const serverDate = data.createShift.date
|
const serverDate = data.createShift.date
|
||||||
? new Date(data.createShift.date as unknown as string)
|
? new Date(data.createShift.date as unknown as string)
|
||||||
: shift.date;
|
: shift.date;
|
||||||
setShifts((prev) => [...prev, {
|
setShifts((prev) => [
|
||||||
...data.createShift,
|
...prev,
|
||||||
date: serverDate,
|
{
|
||||||
wage: data.createShift.wage ?? shift.wage,
|
...data.createShift,
|
||||||
registeredStaff: [],
|
date: serverDate,
|
||||||
}]);
|
wage: data.createShift.wage ?? shift.wage,
|
||||||
// Refetch để đồng bộ với server
|
registeredStaff: [],
|
||||||
refetch();
|
},
|
||||||
|
]);
|
||||||
|
// Refetch để đồng bộ với server
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[createShift] mutation failed:", err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
},
|
||||||
console.error("[createShift] mutation failed:", err);
|
[mutateCreateShift, refetch],
|
||||||
}
|
);
|
||||||
}, [mutateCreateShift, refetch]);
|
|
||||||
|
|
||||||
const deleteShift = useCallback(async (shiftId: string) => {
|
const deleteShift = useCallback(
|
||||||
try {
|
async (shiftId: string) => {
|
||||||
const { data } = await mutateDeleteShift({
|
try {
|
||||||
variables: { id: shiftId },
|
const { data } = await mutateDeleteShift({
|
||||||
});
|
variables: { id: shiftId },
|
||||||
if (data?.deleteShift) {
|
});
|
||||||
setShifts((prev) => prev.filter((s) => s.id !== shiftId));
|
if (data?.deleteShift) {
|
||||||
|
setShifts((prev) => prev.filter((s) => s.id !== shiftId));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[deleteShift] mutation failed:", err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
},
|
||||||
console.error("[deleteShift] mutation failed:", err);
|
[mutateDeleteShift],
|
||||||
}
|
);
|
||||||
}, [mutateDeleteShift]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShiftContext.Provider
|
<ShiftContext.Provider
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ const nextConfig: NextConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const gatewayUrl = "http://localhost:32080";
|
const gatewayUrl = "http://172.17.0.1:32080";
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/:path*",
|
source: "/api/:path*",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.2.9",
|
"version": "1.2.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.2.9",
|
"version": "1.2.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^4.1.9",
|
"@apollo/client": "^4.1.9",
|
||||||
"@tailwindcss/postcss": "^4.2.4",
|
"@tailwindcss/postcss": "^4.2.4",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.2.9",
|
"version": "1.2.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Generated
+4
-4
@@ -1485,8 +1485,8 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.355:
|
electron-to-chromium@1.5.356:
|
||||||
resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==}
|
resolution: {integrity: sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==}
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
@@ -4984,7 +4984,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.29
|
baseline-browser-mapping: 2.10.29
|
||||||
caniuse-lite: 1.0.30001792
|
caniuse-lite: 1.0.30001792
|
||||||
electron-to-chromium: 1.5.355
|
electron-to-chromium: 1.5.356
|
||||||
node-releases: 2.0.44
|
node-releases: 2.0.44
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
@@ -5315,7 +5315,7 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.355: {}
|
electron-to-chromium@1.5.356: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user