71 Commits

Author SHA1 Message Date
83fa1ea020 Merge pull request 'feat/onboard-redesign' (#4) from feat/onboard-redesign into development
Reviewed-on: #4
2026-06-02 13:03:43 +00:00
Jose Selesan
4544911f0e refactor(onboarding): restructure onboarding routes and components
- Removed obsolete onboarding routes: complete, create-complex, index, verify-email, and verify.
- Introduced new setup stepper page for onboarding process.
- Created a multi-step setup component to handle complex creation with validation.
- Added new schemas for complex creation and plan features.
- Implemented detailed steps for complex name, location, plan selection, and court setup.
- Enhanced error handling and user feedback during the onboarding process.
2026-06-02 10:02:28 -03:00
Jose Selesan
2aaa91444d feat: add phone field to User model and update authentication flow
- Added phone field to User model in Prisma schema and generated types.
- Updated backend authentication to include phone as an additional field.
- Implemented phone input in the signup form with validation.
- Created a new SignupPage component for user registration.
- Updated routing to include signup path and redirect authenticated users.
- Modified login page to reference Playzer and added link to signup.
2026-06-01 16:43:52 -03:00
4d3867614d Merge pull request 'Refactor header and layout components, remove About page, and introduce Background component for improved UI structure' (#1) from feat/login-redesign into development
Reviewed-on: #1
2026-06-01 18:59:38 +00:00
Jose Selesan
c18db76c7a Refactor header and layout components, remove About page, and introduce Background component for improved UI structure 2026-06-01 15:50:33 -03:00
Jose Selesan
78df95e985 Refactor Dockerfile and entrypoint.sh for improved backend build process 2026-06-01 15:02:06 -03:00
6738db8c60 Merge pull request 'new-public-booking' (#14) from new-public-booking into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/14
2026-05-28 13:03:13 +00:00
Jose Selesan
8acb81e6b4 Refactor UserAvatar component to simplify image handling and remove error state management 2026-05-22 11:58:25 -03:00
Jose Selesan
91125e82f5 Improve sport selector UI with visual button components
Replace dropdown select with interactive pill buttons for sport selection

- Added SportSelector component with icon-based visual buttons (desktop)
- Added MobileSportSelector component for mobile-optimized layout
- Added getSportIcon() function to map sport names to appropriate icons
- Improved visual feedback with emerald highlight on selection
- Better hover states and transitions for improved UX
- Works on both desktop and mobile views

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-19 16:43:39 -03:00
Jose Selesan
e9bd759fa5 Add navigation for previous and next days in public booking 2026-05-19 11:02:32 -03:00
Jose Selesan
57416bf53a Added complex data 2026-05-19 10:35:23 -03:00
Jose Selesan
0b6041d146 Better complex show 2026-05-19 09:42:31 -03:00
Jose Selesan
900e6146e7 Stepper improvement 2026-05-19 09:32:26 -03:00
Jose Selesan
e905052cc1 Added Stepper component 2026-05-19 09:21:21 -03:00
Jose Selesan
c1d910708a New UI for public booking 2026-05-19 09:06:03 -03:00
b8abf224cf Merge pull request 'Fixed mobile bottom toolbar' (#13) from mobile-fixes into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/13
2026-05-08 15:01:32 +00:00
Jose Selesan
b47f00651e Fixed mobile bottom toolbar 2026-05-08 12:00:52 -03:00
a9c74d2f9c Merge pull request 'mobile-booking-admin' (#12) from mobile-booking-admin into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/12
2026-05-08 14:10:17 +00:00
Jose Selesan
d1b5bea1bc Fix lint 2026-05-08 11:07:21 -03:00
Jose Selesan
ab22ec86ec Improved mobile layout 2026-05-08 11:02:03 -03:00
Jose Selesan
65295236c5 Mobile booking view 2026-05-08 10:56:58 -03:00
Jose Selesan
f104161bbd Fixed build error 2026-05-06 20:30:23 -03:00
Jose Selesan
844dac206c Auto show current time indicator 2026-05-06 20:26:31 -03:00
be48b4b295 Merge pull request 'improve-booking-logic' (#11) from improve-booking-logic into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/11
2026-05-06 20:12:39 +00:00
Jose Selesan
1059d767d5 View no shows in booking panel 2026-05-06 17:10:23 -03:00
Jose Selesan
7be776f189 Close dialog on status change 2026-05-06 17:03:42 -03:00
Jose Selesan
41a217e8a9 Added booking logs to keep track of cancelled bookings and no shows 2026-05-06 16:54:48 -03:00
bb48d9c164 Merge pull request 'ui-v2' (#10) from ui-v2 into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/10
2026-05-06 18:32:24 +00:00
Jose Selesan
01688cf042 Booking tools 2026-05-06 15:31:02 -03:00
Jose Selesan
b97eb9c8a4 Minor changes 2026-05-05 18:40:41 -03:00
Jose Selesan
c089215835 New booking panel 2026-05-05 10:16:38 -03:00
Jose Selesan
546a020003 New header and backgroud style 2026-05-04 14:33:58 -03:00
38b2294aad Merge pull request 'add-result-pattern' (#9) from add-result-pattern into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/9
2026-04-27 19:17:41 +00:00
Jose Selesan
c9ecdd2c77 Add interface design references and principles; implement sport service tests
- Introduced critique, example, principles, and validation documents for interface design.
- Enhanced backend service with coverage threshold and fixed variable declaration in sport service.
- Added comprehensive tests for create, update, and list sports functionalities.
2026-04-24 10:40:25 -03:00
Jose Selesan
21d0c27f4d feat: enhance createSport functionality with conflict handling and result management
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 16:46:46 -03:00
Jose Selesan
20d6018836 feat: implement error handling and validation middleware, enhance result handling
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 16:35:57 -03:00
7bfa3b390e Merge pull request 'send-invitation' (#8) from send-invitation into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/8
2026-04-22 18:59:35 +00:00
Jose Selesan
4658b168ed feat: implement complex user invitation handler with tests and configuration 2026-04-22 15:58:34 -03:00
Jose Selesan
6ad4fc9ee9 feat: add complex user invitation and management features
- Implemented complex member and invitation schemas using Zod for validation.
- Created new API endpoints for inviting, accepting, revoking, and canceling complex user invitations.
- Developed handlers for managing complex users, including listing, inviting, revoking, and canceling invitations.
- Added frontend components for displaying and managing complex users and invitations.
- Introduced session storage management for pending invitations.
- Integrated Gravatar for user avatars based on email.
- Created database migration for complex invitations table.
2026-04-22 15:30:04 -03:00
Jose Selesan
5f2a7fd494 Fixed Better Auth infra on local env 2026-04-22 08:42:18 -03:00
Jose Selesan
0fd72ea1de fix: return 404 error when admin user is not found during complex creation 2026-04-21 16:11:14 -03:00
0aecbfa3dd Merge pull request 'redefine-onboard' (#7) from redefine-onboard into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/7
2026-04-21 18:04:01 +00:00
Jose Selesan
62b7a75093 feat: integrate @better-auth/infra for dashboard and sentinel monitoring 2026-04-21 15:03:29 -03:00
Jose Selesan
07408dbbbe feat: implement onboarding flow with complex creation and email verification, and add Zustand for state management 2026-04-21 09:14:45 -03:00
23e4218f6f Merge pull request 'select-complex' (#6) from select-complex into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/6
2026-04-20 19:10:09 +00:00
Jose Selesan
34cb63364c refactor: modularize API client by splitting resources into individual files and standardizing HTTP request handling. 2026-04-20 16:05:37 -03:00
Jose Selesan
28fd51f926 feat: implement complex selection flow 2026-04-20 15:47:47 -03:00
Jose Selesan
a5e39c94c5 fix: use window.location.origin for Google callback URL and remove redundant whitespace in auth config 2026-04-20 13:37:25 -03:00
Jose Selesan
4e327b7318 refactor: remove custom username generation logic from auth configuration 2026-04-20 10:29:33 -03:00
c69ea82f26 Merge pull request 'feat: integrate Google OAuth authentication provider and add login UI button' (#5) from add-google-auth into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/5
2026-04-20 13:21:08 +00:00
Jose Selesan
a4075866f8 feat: integrate Google OAuth authentication provider and add login UI button 2026-04-20 10:16:02 -03:00
Jose Selesan
5b53a10bbc Merge branch 'add-realtime-cappabilities' into development 2026-04-17 17:35:24 -03:00
Jose Selesan
585e7d2226 feat: implement polling fallback for SSE connection failures and add event deduplication 2026-04-17 17:34:04 -03:00
a1dbab98be Merge pull request 'feat: implement real-time booking updates using Server-Sent Events (SSE) and add public booking URL sharing to the dashboard' (#4) from add-realtime-cappabilities into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/4
2026-04-17 20:26:38 +00:00
Jose Selesan
e0d755bef7 feat: implement real-time booking updates using Server-Sent Events (SSE) and add public booking URL sharing to the dashboard 2026-04-17 17:25:46 -03:00
Jose Selesan
a075f13587 chore: ignore backend environment file and remove trailing newline in gitignore 2026-04-17 16:26:41 -03:00
Jose Selesan
7fe1eef297 chore: ignore backend environment file and remove trailing newline in gitignore 2026-04-17 16:26:24 -03:00
Jose Selesan
20b70c06c1 feat: refactor onboarding into a multi-step flow with dedicated routes and session state management 2026-04-17 16:17:19 -03:00
4588e63fa5 Merge pull request 'feat: implement session persistence for onboarding flow and fix typo in password label' (#3) from fix-onboarding into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/3
2026-04-17 19:04:59 +00:00
Jose Selesan
400f67b268 feat: implement session persistence for onboarding flow and fix typo in password label 2026-04-17 16:03:40 -03:00
Jose Selesan
ea8e3f6a24 Updated mailer service 2026-04-17 15:40:29 -03:00
Jose Selesan
32c436e772 feat: make Mailtrap testInboxId configurable via environment variable 2026-04-17 15:32:50 -03:00
Jose Selesan
a1ff9f2831 Updated mailer to use Mailtrap API 2026-04-17 15:23:51 -03:00
Jose Selesan
b3f1b72da4 refactor: migrate authentication from Supabase to Better Auth and update project configuration 2026-04-17 08:35:54 -03:00
Jose Selesan
aabd9a8266 refactor: standardize tsconfig paths, update deprecation settings, and format user creation timestamp to ISO string 2026-04-16 22:23:34 -03:00
Jose Selesan
f98a0c2c02 feat: migrate authentication to Better Auth and update profile management logic 2026-04-16 21:10:38 -03:00
6dc8cae3a8 Merge pull request 'migrate-to-better-auth' (#2) from migrate-to-better-auth into development
Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/2
2026-04-16 23:56:35 +00:00
Jose Selesan
34a24ce6c9 feat: add user role field and implement Gravatar fallback for user avatars 2026-04-16 20:51:53 -03:00
Jose Selesan
e5002ce440 refactor: integrate Better-Auth for authentication and remove Supabase dependencies 2026-04-16 20:30:01 -03:00
Jose Selesan
f158845279 refactor: migrate authentication from Supabase to Better Auth and update Prisma schema models 2026-04-16 20:05:08 -03:00
Jose Selesan
bd30b32b49 Added Better auth mitation 2026-04-16 15:45:49 -03:00
218 changed files with 15442 additions and 5724 deletions

View File

@@ -0,0 +1,391 @@
---
name: interface-design
description: This skill is for interface design — dashboards, admin panels, apps, tools, and interactive products. NOT for marketing design (landing pages, marketing sites, campaigns).
---
# Interface Design
Build interface design with craft and consistency.
## Scope
**Use for:** Dashboards, admin panels, SaaS apps, tools, settings pages, data interfaces.
**Not for:** Landing pages, marketing sites, campaigns. Redirect those to `/frontend-design`.
---
# The Problem
You will generate generic output. Your training has seen thousands of dashboards. The patterns are strong.
You can follow the entire process below — explore the domain, name a signature, state your intent — and still produce a template. Warm colors on cold structures. Friendly fonts on generic layouts. "Kitchen feel" that looks like every other app.
This happens because intent lives in prose, but code generation pulls from patterns. The gap between them is where defaults win.
The process below helps. But process alone doesn't guarantee craft. You have to catch yourself.
---
# Where Defaults Hide
Defaults don't announce themselves. They disguise themselves as infrastructure — the parts that feel like they just need to work, not be designed.
**Typography feels like a container.** Pick something readable, move on. But typography isn't holding your design — it IS your design. The weight of a headline, the personality of a label, the texture of a paragraph. These shape how the product feels before anyone reads a word. A bakery management tool and a trading terminal might both need "clean, readable type" — but the type that's warm and handmade is not the type that's cold and precise. If you're reaching for your usual font, you're not designing.
**Navigation feels like scaffolding.** Build the sidebar, add the links, get to the real work. But navigation isn't around your product — it IS your product. Where you are, where you can go, what matters most. A page floating in space is a component demo, not software. The navigation teaches people how to think about the space they're in.
**Data feels like presentation.** You have numbers, show numbers. But a number on screen is not design. The question is: what does this number mean to the person looking at it? What will they do with it? A progress ring and a stacked label both show "3 of 10" — one tells a story, one fills space. If you're reaching for number-on-label, you're not designing.
**Token names feel like implementation detail.** But your CSS variables are design decisions. `--ink` and `--parchment` evoke a world. `--gray-700` and `--surface-2` evoke a template. Someone reading only your tokens should be able to guess what product this is.
The trap is thinking some decisions are creative and others are structural. There are no structural decisions. Everything is design. The moment you stop asking "why this?" is the moment defaults take over.
---
# Intent First
Before touching code, answer these. Not in your head — out loud, to yourself or the user.
**Who is this human?**
Not "users." The actual person. Where are they when they open this? What's on their mind? What did they do 5 minutes ago, what will they do 5 minutes after? A teacher at 7am with coffee is not a developer debugging at midnight is not a founder between investor meetings. Their world shapes the interface.
**What must they accomplish?**
Not "use the dashboard." The verb. Grade these submissions. Find the broken deployment. Approve the payment. The answer determines what leads, what follows, what hides.
**What should this feel like?**
Say it in words that mean something. "Clean and modern" means nothing — every AI says that. Warm like a notebook? Cold like a terminal? Dense like a trading floor? Calm like a reading app? The answer shapes color, type, spacing, density — everything.
If you cannot answer these with specifics, stop. Ask the user. Do not guess. Do not default.
## Every Choice Must Be A Choice
For every decision, you must be able to explain WHY.
- Why this layout and not another?
- Why this color temperature?
- Why this typeface?
- Why this spacing scale?
- Why this information hierarchy?
If your answer is "it's common" or "it's clean" or "it works" — you haven't chosen. You've defaulted. Defaults are invisible. Invisible choices compound into generic output.
**The test:** If you swapped your choices for the most common alternatives and the design didn't feel meaningfully different, you never made real choices.
## Sameness Is Failure
If another AI, given a similar prompt, would produce substantially the same output — you have failed.
This is not about being different for its own sake. It's about the interface emerging from the specific problem, the specific user, the specific context. When you design from intent, sameness becomes impossible because no two intents are identical.
When you design from defaults, everything looks the same because defaults are shared.
## Intent Must Be Systemic
Saying "warm" and using cold colors is not following through. Intent is not a label — it's a constraint that shapes every decision.
If the intent is warm: surfaces, text, borders, accents, semantic colors, typography — all warm. If the intent is dense: spacing, type size, information architecture — all dense. If the intent is calm: motion, contrast, color saturation — all calm.
Check your output against your stated intent. Does every token reinforce it? Or did you state an intent and then default anyway?
---
# Product Domain Exploration
This is where defaults get caught — or don't.
Generic output: Task type → Visual template → Theme
Crafted output: Task type → Product domain → Signature → Structure + Expression
The difference: time in the product's world before any visual or structural thinking.
## Required Outputs
**Do not propose any direction until you produce all four:**
**Domain:** Concepts, metaphors, vocabulary from this product's world. Not features — territory. Minimum 5.
**Color world:** What colors exist naturally in this product's domain? Not "warm" or "cool" — go to the actual world. If this product were a physical space, what would you see? What colors belong there that don't belong elsewhere? List 5+.
**Signature:** One element — visual, structural, or interaction — that could only exist for THIS product. If you can't name one, keep exploring.
**Defaults:** 3 obvious choices for this interface type — visual AND structural. You can't avoid patterns you haven't named.
## Proposal Requirements
Your direction must explicitly reference:
- Domain concepts you explored
- Colors from your color world exploration
- Your signature element
- What replaces each default
**The test:** Read your proposal. Remove the product name. Could someone identify what this is for? If not, it's generic. Explore deeper.
---
# The Mandate
**Before showing the user, look at what you made.**
Ask yourself: "If they said this lacks craft, what would they mean?"
That thing you just thought of — fix it first.
Your first output is probably generic. That's normal. The work is catching it before the user has to.
## The Checks
Run these against your output before presenting:
- **The swap test:** If you swapped the typeface for your usual one, would anyone notice? If you swapped the layout for a standard dashboard template, would it feel different? The places where swapping wouldn't matter are the places you defaulted.
- **The squint test:** Blur your eyes. Can you still perceive hierarchy? Is anything jumping out harshly? Craft whispers.
- **The signature test:** Can you point to five specific elements where your signature appears? Not "the overall feel" — actual components. A signature you can't locate doesn't exist.
- **The token test:** Read your CSS variables out loud. Do they sound like they belong to this product's world, or could they belong to any project?
If any check fails, iterate before showing.
---
# Craft Foundations
## Subtle Layering
This is the backbone of craft. Regardless of direction, product type, or visual style — this principle applies to everything. You should barely notice the system working. When you look at Vercel's dashboard, you don't think "nice borders." You just understand the structure. The craft is invisible — that's how you know it's working.
### Surface Elevation
Surfaces stack. A dropdown sits above a card which sits above the page. Build a numbered system — base, then increasing elevation levels. In dark mode, higher elevation = slightly lighter. In light mode, higher elevation = slightly lighter or uses shadow.
Each jump should be only a few percentage points of lightness. You can barely see the difference in isolation. But when surfaces stack, the hierarchy emerges. Whisper-quiet shifts that you feel rather than see.
**Key decisions:**
- **Sidebars:** Same background as canvas, not different. Different colors fragment the visual space into "sidebar world" and "content world." A subtle border is enough separation.
- **Dropdowns:** One level above their parent surface. If both share the same level, the dropdown blends into the card and layering is lost.
- **Inputs:** Slightly darker than their surroundings, not lighter. Inputs are "inset" — they receive content. A darker background signals "type here" without heavy borders.
### Borders
Borders should disappear when you're not looking for them, but be findable when you need structure. Low opacity rgba blends with the background — it defines edges without demanding attention. Solid hex borders look harsh in comparison.
Build a progression — not all borders are equal. Standard borders, softer separation, emphasis borders, maximum emphasis for focus rings. Match intensity to the importance of the boundary.
**The squint test:** Blur your eyes at the interface. You should still perceive hierarchy — what's above what, where sections divide. But nothing should jump out. No harsh lines. No jarring color shifts. Just quiet structure.
This separates professional interfaces from amateur ones. Get this wrong and nothing else matters.
## Infinite Expression
Every pattern has infinite expressions. **No interface should look the same.**
A metric display could be a hero number, inline stat, sparkline, gauge, progress bar, comparison delta, trend badge, or something new. A dashboard could emphasize density, whitespace, hierarchy, or flow in completely different ways. Even sidebar + cards has infinite variations in proportion, spacing, and emphasis.
**Before building, ask:**
- What's the ONE thing users do most here?
- What products solve similar problems brilliantly? Study them.
- Why would this interface feel designed for its purpose, not templated?
**NEVER produce identical output.** Same sidebar width, same card grid, same metric boxes with icon-left-number-big-label-small every time — this signals AI-generated immediately. It's forgettable.
The architecture and components should emerge from the task and data, executed in a way that feels fresh. Linear's cards don't look like Notion's. Vercel's metrics don't look like Stripe's. Same concepts, infinite expressions.
## Color Lives Somewhere
Every product exists in a world. That world has colors.
Before you reach for a palette, spend time in the product's world. What would you see if you walked into the physical version of this space? What materials? What light? What objects?
Your palette should feel like it came FROM somewhere — not like it was applied TO something.
**Beyond Warm and Cold:** Temperature is one axis. Is this quiet or loud? Dense or spacious? Serious or playful? Geometric or organic? A trading terminal and a meditation app are both "focused" — completely different kinds of focus. Find the specific quality, not the generic label.
**Color Carries Meaning:** Gray builds structure. Color communicates — status, action, emphasis, identity. Unmotivated color is noise. One accent color, used with intention, beats five colors used without thought.
---
# Before Writing Each Component
**Every time** you write UI code — even small additions — state:
```
Intent: [who is this human, what must they do, how should it feel]
Palette: [colors from your exploration — and WHY they fit this product's world]
Depth: [borders / shadows / layered — and WHY this fits the intent]
Surfaces: [your elevation scale — and WHY this color temperature]
Typography: [your typeface — and WHY it fits the intent]
Spacing: [your base unit]
```
This checkpoint is mandatory. It forces you to connect every technical choice back to intent.
If you can't explain WHY for each choice, you're defaulting. Stop and think.
---
# Design Principles
## Token Architecture
Every color in your interface should trace back to a small set of primitives: foreground (text hierarchy), background (surface elevation), border (separation hierarchy), brand, and semantic (destructive, warning, success). No random hex values — everything maps to primitives.
### Text Hierarchy
Don't just have "text" and "gray text." Build four levels — primary, secondary, tertiary, muted. Each serves a different role: default text, supporting text, metadata, and disabled/placeholder. Use all four consistently. If you're only using two, your hierarchy is too flat.
### Border Progression
Borders aren't binary. Build a scale that matches intensity to importance — standard separation, softer separation, emphasis, maximum emphasis. Not every boundary deserves the same weight.
### Control Tokens
Form controls have specific needs. Don't reuse surface tokens — create dedicated ones for control backgrounds, control borders, and focus states. This lets you tune interactive elements independently from layout surfaces.
## Spacing
Pick a base unit and stick to multiples. Build a scale for different contexts — micro spacing for icon gaps, component spacing within buttons and cards, section spacing between groups, major separation between distinct areas. Random values signal no system.
## Padding
Keep it symmetrical. If one side has a value, others should match unless content naturally requires asymmetry.
## Depth
Choose ONE approach and commit:
- **Borders-only** — Clean, technical. For dense tools.
- **Subtle shadows** — Soft lift. For approachable products.
- **Layered shadows** — Premium, dimensional. For cards that need presence.
- **Surface color shifts** — Background tints establish hierarchy without shadows.
Don't mix approaches.
## Border Radius
Sharper feels technical. Rounder feels friendly. Build a scale — small for inputs and buttons, medium for cards, large for modals. Don't mix sharp and soft randomly.
## Typography
Build distinct levels distinguishable at a glance. Headlines need weight and tight tracking for presence. Body needs comfortable weight for readability. Labels need medium weight that works at smaller sizes. Data needs monospace with tabular number spacing for alignment. Don't rely on size alone — combine size, weight, and letter-spacing.
## Card Layouts
A metric card doesn't have to look like a plan card doesn't have to look like a settings card. Design each card's internal structure for its specific content — but keep the surface treatment consistent: same border weight, shadow depth, corner radius, padding scale.
## Controls
Native `<select>` and `<input type="date">` render OS-native elements that cannot be styled. Build custom components — trigger buttons with positioned dropdowns, calendar popovers, styled state management.
## Iconography
Icons clarify, not decorate — if removing an icon loses no meaning, remove it. Choose one icon set and stick with it. Give standalone icons presence with subtle background containers.
## Animation
Fast micro-interactions, smooth easing. Larger transitions can be slightly longer. Use deceleration easing. Avoid spring/bounce in professional interfaces.
## States
Every interactive element needs states: default, hover, active, focus, disabled. Data needs states too: loading, empty, error. Missing states feel broken.
## Navigation Context
Screens need grounding. A data table floating in space feels like a component demo, not a product. Include navigation showing where you are in the app, location indicators, and user context. When building sidebars, consider same background as main content with border separation rather than different colors.
## Dark Mode
Dark interfaces have different needs. Shadows are less visible on dark backgrounds — lean on borders for definition. Semantic colors (success, warning, error) often need slight desaturation. The hierarchy system still applies, just with inverted values.
---
# Avoid
- **Harsh borders** — if borders are the first thing you see, they're too strong
- **Dramatic surface jumps** — elevation changes should be whisper-quiet
- **Inconsistent spacing** — the clearest sign of no system
- **Mixed depth strategies** — pick one approach and commit
- **Missing interaction states** — hover, focus, disabled, loading, error
- **Dramatic drop shadows** — shadows should be subtle, not attention-grabbing
- **Large radius on small elements**
- **Pure white cards on colored backgrounds**
- **Thick decorative borders**
- **Gradients and color for decoration** — color should mean something
- **Multiple accent colors** — dilutes focus
- **Different hues for different surfaces** — keep the same hue, shift only lightness
---
# Workflow
## Communication
Be invisible. Don't announce modes or narrate process.
**Never say:** "I'm in ESTABLISH MODE", "Let me check system.md..."
**Instead:** Jump into work. State suggestions with reasoning.
## Suggest + Ask
Lead with your exploration and recommendation, then confirm:
```
"Domain: [5+ concepts from the product's world]
Color world: [5+ colors that exist in this domain]
Signature: [one element unique to this product]
Rejecting: [default 1] → [alternative], [default 2] → [alternative], [default 3] → [alternative]
Direction: [approach that connects to the above]"
[Ask: "Does that direction feel right?"]
```
## If Project Has system.md
Read `.interface-design/system.md` and apply. Decisions are made.
## If No system.md
1. Explore domain — Produce all four required outputs
2. Propose — Direction must reference all four
3. Confirm — Get user buy-in
4. Build — Apply principles
5. **Evaluate** — Run the mandate checks before showing
6. Offer to save
---
# After Completing a Task
When you finish building something, **always offer to save**:
```
"Want me to save these patterns for future sessions?"
```
If yes, write to `.interface-design/system.md`:
- Direction and feel
- Depth strategy (borders/shadows/layered)
- Spacing base unit
- Key component patterns
### What to Save
Add patterns when a component is used 2+ times, is reusable across the project, or has specific measurements worth remembering. Don't save one-off components, temporary experiments, or variations better handled with props.
### Consistency Checks
If system.md defines values, check against them: spacing on the defined grid, depth using the declared strategy throughout, colors from the defined palette, documented patterns reused instead of reinvented.
This compounds — each save makes future work faster and more consistent.
---
# Deep Dives
For more detail on specific topics:
- `references/principles.md` — Code examples, specific values, dark mode
- `references/validation.md` — Memory management, when to update system.md
- `references/critique.md` — Post-build craft critique protocol
# Commands
- `/interface-design:status` — Current system state
- `/interface-design:audit` — Check code against system
- `/interface-design:extract` — Extract patterns from code
- `/interface-design:critique` — Critique your build for craft, then rebuild what defaulted

View File

@@ -0,0 +1,67 @@
# Critique
Your first build shipped the structure. Now look at it the way a design lead reviews a junior's work — not asking "does this work?" but "would I put my name on this?"
---
## The Gap
There's a distance between correct and crafted. Correct means the layout holds, the grid aligns, the colors don't clash. Crafted means someone cared about every decision down to the last pixel. You can feel the difference immediately — the way you tell a hand-thrown mug from an injection-molded one. Both hold coffee. One has presence.
Your first output lives in correct. This command pulls it toward crafted.
---
## See the Composition
Step back. Look at the whole thing.
Does the layout have rhythm? Great interfaces breathe unevenly — dense tooling areas give way to open content, heavy elements balance against light ones, the eye travels through the page with purpose. Default layouts are monotone: same card size, same gaps, same density everywhere. Flatness is the sound of no one deciding.
Are proportions doing work? A 280px sidebar next to full-width content says "navigation serves content." A 360px sidebar says "these are peers." The specific number declares what matters. If you can't articulate what your proportions are saying, they're not saying anything.
Is there a clear focal point? Every screen has one thing the user came here to do. That thing should dominate — through size, position, contrast, or the space around it. When everything competes equally, nothing wins and the interface feels like a parking lot.
---
## See the Craft
Move close. Pixel-close.
The spacing grid is non-negotiable — every value a multiple of 4, no exceptions — but correctness alone isn't craft. Craft is knowing that a tool panel at 16px padding feels workbench-tight while the same card at 24px feels like a brochure. The same number can be right in one context and lazy in another. Density is a design decision, not a constant.
Typography should be legible even squinted. If size is the only thing separating your headline from your body from your label, the hierarchy is too weak. Weight, tracking, and opacity create layers that size alone can't.
Surfaces should whisper hierarchy. Not thick borders, not dramatic shadows — quiet tonal shifts where you feel the depth without seeing it. Remove every border from your CSS mentally. Can you still perceive the structure through surface color alone? If not, your surfaces aren't working hard enough.
Interactive elements need life. Every button, link, and clickable region should respond to hover and press. Not dramatically — a subtle shift in background, a gentle darkening. Missing states make an interface feel like a photograph of software instead of software.
---
## See the Content
Read every visible string as a user would. Not checking for typos — checking for truth.
Does this screen tell one coherent story? Could a real person at a real company be looking at exactly this data right now? Or does the page title belong to one product, the article body to another, and the sidebar metrics to a third?
Content incoherence breaks the illusion faster than any visual flaw. A beautifully designed interface with nonsensical content is a movie set with no script.
---
## See the Structure
Open the CSS and find the lies — the places that look right but are held together with tape.
Negative margins undoing a parent's padding. Calc() values that exist only as workarounds. Absolute positioning to escape layout flow. Each is a shortcut where a clean solution exists. Cards with full-width dividers use flex column and section-level padding. Centered content uses max-width with auto margins. The correct answer is always simpler than the hack.
---
## Again
Look at your output one final time.
Ask: "If they said this lacks craft, what would they point to?"
That thing you just thought of — fix it. Then ask again.
The first build was the draft. The critique is the design.

View File

@@ -0,0 +1,86 @@
# Craft in Action
This shows how the subtle layering principle translates to real decisions. Learn the thinking, not the code. Your values will differ — the approach won't.
---
## The Subtle Layering Mindset
Before looking at any example, internalize this: **you should barely notice the system working.**
When you look at Vercel's dashboard, you don't think "nice borders." You just understand the structure. When you look at Supabase, you don't think "good surface elevation." You just know what's above what. The craft is invisible — that's how you know it's working.
---
## Example: Dashboard with Sidebar and Dropdown
### The Surface Decisions
**Why so subtle?** Each elevation jump should be only a few percentage points of lightness. You can barely see the difference in isolation. But when surfaces stack, the hierarchy emerges. This is the Vercel/Supabase way — whisper-quiet shifts that you feel rather than see.
**What NOT to do:** Don't make dramatic jumps between elevations. That's jarring. Don't use different hues for different levels. Keep the same hue, shift only lightness.
### The Border Decisions
**Why rgba, not solid colors?** Low opacity borders blend with their background. A low-opacity white border on a dark surface is barely there — it defines the edge without demanding attention. Solid hex borders look harsh in comparison.
**The test:** Look at your interface from arm's length. If borders are the first thing you notice, reduce opacity. If you can't find where regions end, increase slightly.
### The Sidebar Decision
**Why same background as canvas, not different?**
Many dashboards make the sidebar a different color. This fragments the visual space — now you have "sidebar world" and "content world."
Better: Same background, subtle border separation. The sidebar is part of the app, not a separate region. Vercel does this. Supabase does this. The border is enough.
### The Dropdown Decision
**Why surface-200, not surface-100?**
The dropdown floats above the card it emerged from. If both were surface-100, the dropdown would blend into the card — you'd lose the sense of layering. Surface-200 is just light enough to feel "above" without being dramatically different.
**Why border-overlay instead of border-default?**
Overlays (dropdowns, popovers) often need slightly more definition because they're floating in space. A touch more border opacity helps them feel contained without being harsh.
---
## Example: Form Controls
### Input Background Decision
**Why darker, not lighter?**
Inputs are "inset" — they receive content, they don't project it. A slightly darker background signals "type here" without needing heavy borders. This is the alternative-background principle.
### Focus State Decision
**Why subtle focus states?**
Focus needs to be visible, but you don't need a glowing ring or dramatic color. A noticeable increase in border opacity is enough for a clear state change. Subtle-but-noticeable — the same principle as surfaces.
---
## Adapt to Context
Your product might need:
- Warmer hues (slight yellow/orange tint)
- Cooler hues (blue-gray base)
- Different lightness progression
- Light mode (principles invert — higher elevation = shadow, not lightness)
**The principle is constant:** barely different, still distinguishable. The values adapt to context.
---
## The Craft Check
Apply the squint test to your work:
1. Blur your eyes or step back
2. Can you still perceive hierarchy?
3. Is anything jumping out at you?
4. Can you tell where regions begin and end?
If hierarchy is visible and nothing is harsh — the subtle layering is working.

View File

@@ -0,0 +1,235 @@
# Core Craft Principles
These apply regardless of design direction. This is the quality floor.
---
## Surface & Token Architecture
Professional interfaces don't pick colors randomly — they build systems. Understanding this architecture is the difference between "looks okay" and "feels like a real product."
### The Primitive Foundation
Every color in your interface should trace back to a small set of primitives:
- **Foreground** — text colors (primary, secondary, muted)
- **Background** — surface colors (base, elevated, overlay)
- **Border** — edge colors (default, subtle, strong)
- **Brand** — your primary accent
- **Semantic** — functional colors (destructive, warning, success)
Don't invent new colors. Map everything to these primitives.
### Surface Elevation Hierarchy
Surfaces stack. A dropdown sits above a card which sits above the page. Build a numbered system:
```
Level 0: Base background (the app canvas)
Level 1: Cards, panels (same visual plane as base)
Level 2: Dropdowns, popovers (floating above)
Level 3: Nested dropdowns, stacked overlays
Level 4: Highest elevation (rare)
```
In dark mode, higher elevation = slightly lighter. In light mode, higher elevation = slightly lighter or uses shadow. The principle: **elevated surfaces need visual distinction from what's beneath them.**
### The Subtlety Principle
This is where most interfaces fail. Study Vercel, Supabase, Linear — their surfaces are **barely different** but still distinguishable. Their borders are **light but not invisible**.
**For surfaces:** The difference between elevation levels should be subtle — a few percentage points of lightness, not dramatic jumps. In dark mode, surface-100 might be 7% lighter than base, surface-200 might be 9%, surface-300 might be 12%. You can barely see it, but you feel it.
**For borders:** Borders should define regions without demanding attention. Use low opacity (0.05-0.12 alpha for dark mode, slightly higher for light). The border should disappear when you're not looking for it, but be findable when you need to understand the structure.
**The test:** Squint at your interface. You should still perceive the hierarchy — what's above what, where regions begin and end. But no single border or surface should jump out at you. If borders are the first thing you notice, they're too strong. If you can't find where one region ends and another begins, they're too subtle.
**Common AI mistakes to avoid:**
- Borders that are too visible (1px solid gray instead of subtle rgba)
- Surface jumps that are too dramatic (going from dark to light instead of dark to slightly-less-dark)
- Using different hues for different surfaces (gray card on blue background)
- Harsh dividers where subtle borders would do
### Text Hierarchy via Tokens
Don't just have "text" and "gray text." Build four levels:
- **Primary** — default text, highest contrast
- **Secondary** — supporting text, slightly muted
- **Tertiary** — metadata, timestamps, less important
- **Muted** — disabled, placeholder, lowest contrast
Use all four consistently. If you're only using two, your hierarchy is too flat.
### Border Progression
Borders aren't binary. Build a scale:
- **Default** — standard borders
- **Subtle/Muted** — softer separation
- **Strong** — emphasis, hover states
- **Stronger** — maximum emphasis, focus rings
Match border intensity to the importance of the boundary.
### Dedicated Control Tokens
Form controls (inputs, checkboxes, selects) have specific needs. Don't just reuse surface tokens — create dedicated ones:
- **Control background** — often different from surface backgrounds
- **Control border** — needs to feel interactive
- **Control focus** — clear focus indication
This separation lets you tune controls independently from layout surfaces.
### Context-Aware Bases
Different areas of your app might need different base surfaces:
- **Marketing pages** — might use darker/richer backgrounds
- **Dashboard/app** — might use neutral working backgrounds
- **Sidebar** — might differ from main canvas
The surface hierarchy works the same way — it just starts from a different base.
### Alternative Backgrounds for Depth
Beyond shadows, use contrasting backgrounds to create depth. An "alternative" or "inset" background makes content feel recessed. Useful for:
- Empty states in data grids
- Code blocks
- Inset panels
- Visual grouping without borders
---
## Spacing System
Pick a base unit (4px and 8px are common) and use multiples throughout. The specific number matters less than consistency — every spacing value should be explainable as "X times the base unit."
Build a scale for different contexts:
- Micro spacing (icon gaps, tight element pairs)
- Component spacing (within buttons, inputs, cards)
- Section spacing (between related groups)
- Major separation (between distinct sections)
## Symmetrical Padding
TLBR must match. If top padding is 16px, left/bottom/right must also be 16px. Exception: when content naturally creates visual balance.
```css
/* Good */
padding: 16px;
padding: 12px 16px; /* Only when horizontal needs more room */
/* Bad */
padding: 24px 16px 12px 16px;
```
## Border Radius Consistency
Sharper corners feel technical, rounder corners feel friendly. Pick a scale that fits your product's personality and use it consistently.
The key is having a system: small radius for inputs and buttons, medium for cards, large for modals or containers. Don't mix sharp and soft randomly — inconsistent radius is as jarring as inconsistent spacing.
## Depth & Elevation Strategy
Match your depth approach to your design direction. Choose ONE and commit:
**Borders-only (flat)** — Clean, technical, dense. Works for utility-focused tools where information density matters more than visual lift. Linear, Raycast, and many developer tools use almost no shadows — just subtle borders to define regions.
**Subtle single shadows** — Soft lift without complexity. A simple `0 1px 3px rgba(0,0,0,0.08)` can be enough. Works for approachable products that want gentle depth.
**Layered shadows** — Rich, premium, dimensional. Multiple shadow layers create realistic depth. Stripe and Mercury use this approach. Best for cards that need to feel like physical objects.
**Surface color shifts** — Background tints establish hierarchy without any shadows. A card at `#fff` on a `#f8fafc` background already feels elevated.
```css
/* Borders-only approach */
--border: rgba(0, 0, 0, 0.08);
--border-subtle: rgba(0, 0, 0, 0.05);
border: 0.5px solid var(--border);
/* Single shadow approach */
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
/* Layered shadow approach */
--shadow-layered:
0 0 0 0.5px rgba(0, 0, 0, 0.05),
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.02);
```
## Card Layouts
Monotonous card layouts are lazy design. A metric card doesn't have to look like a plan card doesn't have to look like a settings card.
Design each card's internal structure for its specific content — but keep the surface treatment consistent: same border weight, shadow depth, corner radius, padding scale, typography.
## Isolated Controls
UI controls deserve container treatment. Date pickers, filters, dropdowns — these should feel like crafted objects.
**Never use native form elements for styled UI.** Native `<select>`, `<input type="date">`, and similar elements render OS-native dropdowns that cannot be styled. Build custom components instead:
- Custom select: trigger button + positioned dropdown menu
- Custom date picker: input + calendar popover
- Custom checkbox/radio: styled div with state management
Custom select triggers must use `display: inline-flex` with `white-space: nowrap` to keep text and chevron icons on the same row.
## Typography Hierarchy
Build distinct levels that are visually distinguishable at a glance:
- **Headlines** — heavier weight, tighter letter-spacing for presence
- **Body** — comfortable weight for readability
- **Labels/UI** — medium weight, works at smaller sizes
- **Data** — often monospace, needs `tabular-nums` for alignment
Don't rely on size alone. Combine size, weight, and letter-spacing to create clear hierarchy. If you squint and can't tell headline from body, the hierarchy is too weak.
## Monospace for Data
Numbers, IDs, codes, timestamps belong in monospace. Use `tabular-nums` for columnar alignment. Mono signals "this is data."
## Iconography
Icons clarify, not decorate — if removing an icon loses no meaning, remove it. Choose a consistent icon set and stick with it throughout the product.
Give standalone icons presence with subtle background containers. Icons next to text should align optically, not mathematically.
## Animation
Keep it fast and functional. Micro-interactions (hover, focus) should feel instant — around 150ms. Larger transitions (modals, panels) can be slightly longer — 200-250ms.
Use smooth deceleration easing (ease-out variants). Avoid spring/bounce effects in professional interfaces — they feel playful, not serious.
## Contrast Hierarchy
Build a four-level system: foreground (primary) → secondary → muted → faint. Use all four consistently.
## Color Carries Meaning
Gray builds structure. Color communicates — status, action, emphasis, identity. Unmotivated color is noise. Color that reinforces the product's world is character.
## Navigation Context
Screens need grounding. A data table floating in space feels like a component demo, not a product. Consider including:
- **Navigation** — sidebar or top nav showing where you are in the app
- **Location indicator** — breadcrumbs, page title, or active nav state
- **User context** — who's logged in, what workspace/org
When building sidebars, consider using the same background as the main content area. Rely on a subtle border for separation rather than different background colors.
## Dark Mode
Dark interfaces have different needs:
**Borders over shadows** — Shadows are less visible on dark backgrounds. Lean more on borders for definition.
**Adjust semantic colors** — Status colors (success, warning, error) often need to be slightly desaturated for dark backgrounds.
**Same structure, different values** — The hierarchy system still applies, just with inverted values.

View File

@@ -0,0 +1,48 @@
# Memory Management
When and how to update `.interface-design/system.md`.
## When to Add Patterns
Add to system.md when:
- Component used 2+ times
- Pattern is reusable across the project
- Has specific measurements worth remembering
## Pattern Format
```markdown
### Button Primary
- Height: 36px
- Padding: 12px 16px
- Radius: 6px
- Font: 14px, 500 weight
```
## Don't Document
- One-off components
- Temporary experiments
- Variations better handled with props
## Pattern Reuse
Before creating a component, check system.md:
- Pattern exists? Use it.
- Need variation? Extend, don't create new.
Memory compounds: each pattern saved makes future work faster and more consistent.
---
# Validation Checks
If system.md defines specific values, check consistency:
**Spacing** — All values multiples of the defined base?
**Depth** — Using the declared strategy throughout? (borders-only means no shadows)
**Colors** — Using defined palette, not random hex codes?
**Patterns** — Reusing documented patterns instead of creating new?

2
.gitignore vendored
View File

@@ -78,6 +78,7 @@ web_modules/
# dotenv environment variable files # dotenv environment variable files
.env .env
apps/backend/.env
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
@@ -155,4 +156,3 @@ apps/backend/src/register-routes.js
apps/backend/src/server.js apps/backend/src/server.js
apps/backend/src/index.js apps/backend/src/index.js
apps/backend/src/types/*.js apps/backend/src/types/*.js

140
AGENTS.md
View File

@@ -33,18 +33,44 @@ packages/api-contract/ # Shared Zod schemas, types, route definitions
2. Implement handler in `apps/backend` 2. Implement handler in `apps/backend`
3. Consume from `apps/frontend` via workspace import `@repo/api-contract` 3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
## Authentication (Better Auth)
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
- **Backend Context**: `AppContext` (mapped via `AppEnv`) provides `c.get('user')` and `c.get('session')`.
- **User Roles**: The `User` model includes a `role` field (default: `member`). Use `requireSuperAdmin` middleware for protected admin routes.
- **Session Persistence**:
- Backend: Hono CORS must have `credentials: true`.
- Frontend: `authClient` must have `fetchOptions.credentials: 'include'`.
- Frontend: Axios instance must have `withCredentials: true`.
## Key Quirks ## Key Quirks
- **Prisma 7**: Uses `prisma.config.ts`, requires `DATABASE_URL` env var at generate time - **Prisma 7**: Uses `prisma.config.ts`, requires `DATABASE_URL` env var at generate time.
- **Zod v4**: Use `.issues` instead of `.errors` for validation errors - **Gravatar Fallback**: Users without an `image` get an automatic Gravatar Identicon.
- **Generated files**: `routeTree.gen.ts` and `tsconfig*.json` are auto-generated (ignored by Biome) - Backend: Handled in `user.service.ts` for profile API.
- Frontend: Handled in `AuthProvider` (`auth.tsx`) for the session state.
- **Zod v4**: Use `.issues` instead of `.errors` for validation errors.
- **TanStack Router**: Routes are code-generated. Use `--filter frontend build` not raw vite build. - **TanStack Router**: Routes are code-generated. Use `--filter frontend build` not raw vite build.
## Real-time Updates (SSE)
The project uses **Server-Sent Events (SSE)** for real-time booking updates in the admin panel.
- **Backend**: `apps/backend/src/lib/sse.ts` provides the `sseManager` and SSE endpoint `/api/events/:channel`.
- **Event flow**: When a public booking is created, the handler emits to channel `complex:{complexId}`.
- **Frontend**: `home-page.tsx` connects to the SSE endpoint and invalidates the bookings query on new events.
**Important - Multi-process limitation**: The current SSE implementation works with a single Bun process only. If you deploy with multiple workers (e.g., clustering in Dokploy), events won't be shared across workers. For production with multiple workers, use Redis for pub/sub:
- Install `@hono/node-server` or a Redis client
- Replace `sseManager` with Redis pub/sub
- Or fall back to Socket.io with Redis adapter
## Docker Deployment (Dokploy) ## Docker Deployment (Dokploy)
- **Build Context**: `./` (monorepo root) - **Build Context**: `./` (monorepo root)
- **Dockerfile**: `Dockerfile` (in root, not `apps/backend/`) - **Dockerfile**: `Dockerfile` (in root, not `apps/backend/`)
- **Required args**: `DATABASE_URL`, `VITE_API_BASE_URL`, `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY` - **Required args**: `DATABASE_URL`, `VITE_API_BASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`
- **bun.lock must be tracked**: It's in `.gitignore` by default - remove it for Docker builds - **bun.lock must be tracked**: It's in `.gitignore` by default - remove it for Docker builds
## Linting ## Linting
@@ -59,9 +85,111 @@ Biome is used (not ESLint). Config in `biome.json`:
**Backend** (`apps/backend/.env`): **Backend** (`apps/backend/.env`):
- `DATABASE_URL` - PostgreSQL connection (required for Prisma) - `DATABASE_URL` - PostgreSQL connection (required for Prisma)
- `SUPABASE_URL`, `SUPABASE_ANON_KEY` - Auth - `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` - Auth core
- `CORS_ORIGIN` - Frontend URL - `APP_BASE_URL` - Frontend URL for trusted origins
- `CORS_ORIGIN` - Frontend URL for CORS policy
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` - Google OAuth (see below)
**Frontend** (`apps/frontend/.env`): **Frontend** (`apps/frontend/.env`):
- `VITE_*` prefix required (Vite embeds these at build time) - `VITE_*` prefix required (Vite embeds these at build time)
- `VITE_API_BASE_URL` - Backend URL (default: http://localhost:3000) - `VITE_API_BASE_URL` - Backend URL (default: http://localhost:3000)
## Google OAuth
To enable login with Google:
1. Create OAuth credentials in Google Cloud Console:
- Application type: Web application
- Authorized redirect URI: `{BETTER_AUTH_URL}/api/auth/callback/google`
2. Add env vars to `.env.example`:
```
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```
3. The backend configures `socialProviders.google` automatically.
4. Frontend uses `authClient.signIn.social({ provider: 'google', callbackURL: 'http://localhost:5173' })`.
## Complex Selection (Multi-tenancy)
The app supports users with multiple complexes. Each user has a role per complex (`ADMIN` or `EMPLOYEE`), stored in `ComplexUser` table.
### Flow
1. **Login** (password or Google OAuth):
- After auth, call `GET /api/complexes/mine` to get user's complexes
- If 1 complex → auto-select via `POST /api/complexes/select` → redirect to `/`
- If >1 complexes → redirect to `/select-complex`
2. **Select Complex Page** (`/select-complex`):
- List all complexes with roles
- User selects one → `POST /api/complexes/select` → set cookie → redirect to `/`
3. **Home Page** (`/`):
- If no cookie (first visit) → auto-select if 1 complex, else redirect
- Use `GET /api/complexes/me` to get current selected complex
### Cookie
- Name: `selected-complex-id`
- httpOnly, secure (prod), sameSite: strict
- MaxAge: 30 days
- Set by `POST /api/complexes/select`
### Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/complexes/mine` | List all complexes with role |
| GET | `/api/complexes/me` | Get currently selected complex |
| POST | `/api/complexes/select` | Select complex (sets cookie) |
### Profile Role
The profile page shows the user's role for the **selected complex**, not the global user role. This is fetched from `ComplexUser` table using the cookie value.
## Frontend API Client
The API client is split into modular files for better maintainability and testability.
### Structure
```
lib/
├── api-client.ts # Main barrel file (~70 lines)
└── api/
├── base.ts # ApiClientError, configureApiClient, apiBaseUrl
├── http.ts # Axios instance with interceptors
├── index.ts # Re-exports all resources
└── resources/
├── complexes.ts # Complex endpoints
├── courts.ts # Court endpoints
├── bookings.ts # Public & admin booking endpoints
├── user.ts # User profile endpoint
├── sports.ts # Sport endpoints
├── onboarding.ts # Onboarding endpoints
└── plans.ts # Plan endpoints
```
### Using the API Client
```typescript
import { apiClient, authClient, ApiClientError, configureApiClient } from '@/lib/api-client';
// Via apiClient (barrel)
await apiClient.complexes.listMine();
await apiClient.publicBookings.getAvailability('my-club', { date: '2026-04-20' });
// Direct imports (for better tree-shaking)
import { complexes, getAvailability } from '@/lib/api';
await complexes.listMine();
await getAvailability('my-club', { date: '2026-04-20' });
```
### Adding a New Resource
1. Create `lib/api/resources/[resource].ts`
2. Export functions using `http` from `../http`
3. Add exports in `lib/api/index.ts`

View File

@@ -18,11 +18,7 @@ WORKDIR /app
COPY apps/frontend ./apps/frontend COPY apps/frontend ./apps/frontend
COPY packages ./packages COPY packages ./packages
ARG VITE_API_BASE_URL ARG VITE_API_BASE_URL
ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
ENV VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
ENV VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
RUN bun run build:frontend RUN bun run build:frontend
# ============================================================================= # =============================================================================
@@ -36,25 +32,19 @@ COPY packages/api-contract/package.json packages/api-contract/
RUN bun install RUN bun install
# ============================================================================= # =============================================================================
# Stage 4: Prisma Generate # Stage 4: Build Backend (prisma generate + tsc)
# ============================================================================= # =============================================================================
FROM deps-backend AS prisma FROM deps-backend AS build-backend
WORKDIR /app WORKDIR /app
ARG DATABASE_URL ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL} ENV DATABASE_URL=${DATABASE_URL:-postgresql://dummy:dummy@localhost:5432/dummy}
COPY apps/backend ./apps/backend COPY apps/backend ./apps/backend
RUN bun --cwd apps/backend prisma:generate
# =============================================================================
# Stage 5: Build Backend
# =============================================================================
FROM prisma AS build-backend
WORKDIR /app
COPY packages ./packages COPY packages ./packages
RUN bun --cwd apps/backend prisma:generate
RUN bun --cwd apps/backend build RUN bun --cwd apps/backend build
# ============================================================================= # =============================================================================
# Stage 6: Runner - Production image # Stage 5: Runner - Production image
# ============================================================================= # =============================================================================
FROM oven/bun:1.3.11 AS runner FROM oven/bun:1.3.11 AS runner
WORKDIR /app WORKDIR /app
@@ -64,10 +54,10 @@ COPY --from=build-backend /app/node_modules ./node_modules
COPY --from=build-frontend /app/apps/frontend/dist ./apps/backend/public COPY --from=build-frontend /app/apps/frontend/dist ./apps/backend/public
COPY --from=build-backend /app/apps/backend ./apps/backend COPY --from=build-backend /app/apps/backend ./apps/backend
COPY --from=build-backend /app/packages ./packages COPY --from=build-backend /app/packages ./packages
COPY package.json ./ COPY package.json bun.lock ./
COPY entrypoint.sh /app/entrypoint.sh COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
EXPOSE 3000 EXPOSE 3000
CMD ["/app/entrypoint.sh"] CMD ["/app/entrypoint.sh"]

183
README.md
View File

@@ -1,184 +1,105 @@
# Monorepo (Frontend + Backend + Contrato API) # Playzer Monorepo
Este repositorio contiene: Este repositorio contiene:
- `apps/frontend`: app web en React + Vite + TanStack Router/Query. - `apps/frontend`: React + Vite + TanStack Router/Query.
- `apps/backend`: API en Bun + Hono + Prisma + Supabase Auth. - `apps/backend`: Bun + Hono + Prisma + Better Auth.
- `packages/api-contract`: contratos compartidos (schemas/types/rutas) entre frontend y backend. - `packages/api-contract`: contratos Zod/tipos compartidos.
## Objetivo de este README ## Requisitos
Dejarte productivo/a en pocos minutos para que puedas: - Bun reciente
- PostgreSQL disponible
1. Levantar frontend y backend en local.
2. Configurar variables de entorno.
3. Ejecutar migraciones de base de datos.
4. Entender la estructura y el flujo de trabajo del proyecto.
## Stack técnico
- Runtime/package manager: `bun`
- Frontend: `React 19`, `Vite`, `TanStack Router`, `TanStack Query`
- Backend: `Bun`, `Hono`, `Prisma`, `PostgreSQL`
- Auth: `Supabase Auth` (token Bearer validado en backend)
- Validación de contratos: `zod` en `@repo/api-contract`
## Prerrequisitos
- `bun` instalado (versión reciente).
- `PostgreSQL` corriendo localmente (o accesible remotamente).
- Proyecto de `Supabase` para autenticación (URL + anon key).
## Setup rápido ## Setup rápido
1. Instalar dependencias desde la raíz: 1. Instalar dependencias:
```sh ```sh
bun install bun install
``` ```
2. Configurar variables de entorno del backend: 2. Configurar backend:
```sh ```sh
cp apps/backend/.env.example apps/backend/.env cp apps/backend/.env.example apps/backend/.env
``` ```
3. Configurar variables de entorno del frontend: 3. Configurar frontend:
```sh ```sh
cp apps/frontend/.env.example apps/frontend/.env cp apps/frontend/.env.example apps/frontend/.env
``` ```
4. Revisar/editar valores en ambos `.env` según tu entorno. 4. Ejecutar migraciones:
## Variables de entorno
### Backend (`apps/backend/.env`)
Variables esperadas:
- `SUPABASE_URL`
- `SUPABASE_ANON_KEY`
- `CORS_ORIGIN` (por defecto local frontend)
- `LOG_LEVEL`
- `DATABASE_URL` (PostgreSQL)
Notas:
- El backend también acepta `VITE_SUPABASE_URL` y `VITE_SUPABASE_ANON_KEY` como fallback.
- Si falta `DATABASE_URL`, Prisma no puede inicializarse.
### Frontend (`apps/frontend/.env`)
Variables esperadas:
- `VITE_SUPABASE_URL`
- `VITE_SUPABASE_ANON_KEY`
- `VITE_API_BASE_URL` (por defecto: `http://localhost:3000`)
## Base de datos (Prisma)
Antes de arrancar backend por primera vez, corré migraciones:
```sh ```sh
bun --filter backend prisma:migrate bun --filter backend prisma:migrate
``` ```
Si cambiás el schema, regenerá cliente: ## Variables de entorno
```sh ### Backend (`apps/backend/.env`)
bun --filter backend prisma:generate
```
## Ejecutar en desarrollo - `DATABASE_URL`
- `BETTER_AUTH_SECRET`
- `BETTER_AUTH_URL`
- `APP_BASE_URL`
- `CORS_ORIGIN`
Terminal 1 (backend): ### Frontend (`apps/frontend/.env`)
- `VITE_API_BASE_URL`
## Desarrollo
Backend:
```sh ```sh
bun run dev:backend bun run dev:backend
``` ```
Terminal 2 (frontend): Frontend:
```sh ```sh
bun run dev:frontend bun run dev:frontend
``` ```
Puertos por defecto: Todo junto:
- Frontend: `http://localhost:5173` ```sh
- Backend: `http://localhost:3000` bun run dev
## Scripts útiles
Desde la raíz:
- `bun run dev:frontend`
- `bun run dev:backend`
- `bun run build:frontend`
- `bun run lint:frontend`
Desde `apps/backend`:
- `bun run dev`
- `bun run start`
- `bun run prisma:migrate`
- `bun run prisma:generate`
## Estructura del repo
```txt
.
├─ apps/
│ ├─ frontend/ # UI y navegación
│ └─ backend/ # API, auth middleware, acceso a DB
└─ packages/
└─ api-contract/ # Schemas/tipos/rutas compartidos
``` ```
## Flujo de autenticación (resumen) ## Prisma
1. Frontend autentica con Supabase. Regenerar cliente después de cambios de schema:
2. Frontend guarda el access token y lo envía como `Authorization: Bearer ...` al backend.
3. Backend valida token con Supabase (`requireAuth` middleware).
4. Si el token es válido, habilita rutas protegidas (`/api/user/*`, `/api/tenants/*`).
## Endpoints principales ```sh
bun --filter backend prisma:generate
```
- `GET /api/user/profile` ## Autenticación
- `POST /api/tenants`
- `GET /api/tenants/:id`
- `PATCH /api/tenants/:id`
## Convenciones recomendadas para el equipo El proyecto usa **Better Auth** con sesión por cookie.
- Mantener contratos de API en `packages/api-contract` (evitar duplicar tipos). - Backend: `c.get('user')` y `c.get('session')` en contexto Hono.
- Si agregás/cambiás endpoint: - Frontend: `authClient` con `fetchOptions.credentials = 'include'`.
1. Actualizá schema/tipos en `api-contract`. - Frontend API: Axios con `withCredentials = true`.
2. Implementá handler/ruta en backend. - Backend CORS: `credentials: true`.
3. Consumí el contrato desde frontend.
- Validar payloads con Zod tanto en contrato como en rutas.
- Mantener PRs chicos y enfocados.
## Troubleshooting rápido ## Deploy (Docker)
- Error de CORS: - Build context: raíz del monorepo (`./`)
- Revisar `CORS_ORIGIN` en backend y que incluya `http://localhost:5173`. - Dockerfile: `./Dockerfile`
- `403 Missing bearer token`: - Build args requeridos:
- Verificar login en frontend y envío del header `Authorization`. - `DATABASE_URL`
- Error de Supabase env vars: - `VITE_API_BASE_URL`
- Confirmar `SUPABASE_URL`/`SUPABASE_ANON_KEY` (backend) y `VITE_*` (frontend). - `BETTER_AUTH_SECRET`
- Error de Prisma/DB: - `BETTER_AUTH_URL`
- Confirmar `DATABASE_URL`, conectividad a PostgreSQL y migraciones aplicadas.
## Primer día sugerido para onboarding ## Flujo recomendado de cambios de API
1. Levantar backend y frontend en local. 1. Actualizar contrato en `packages/api-contract`.
2. Crear usuario o iniciar sesión desde la pantalla de onboard. 2. Implementar backend en `apps/backend`.
3. Crear un tenant de prueba. 3. Consumir contrato desde frontend (`@repo/api-contract`).
4. Probar lectura/edición de tenant.
5. Revisar `packages/api-contract` para entender cómo se comparten tipos y rutas.
---
Si te trabás en el setup, empezá verificando `.env`, estado de PostgreSQL y que ambos procesos (`dev:backend` y `dev:frontend`) estén corriendo.

View File

@@ -1,14 +0,0 @@
SUPABASE_URL=https://afzzqgvdkkpyxmifpaya.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmenpxZ3Zka2tweXhtaWZwYXlhIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NDg5Njc5MiwiZXhwIjoyMDkwNDcyNzkyfQ.2tex8lMCGzxBqeIk01Z08Wl15K7AavyfClUFgUl2NPM
SUPABASE_ANON_KEY=sb_publishable_RgMLVBTCTClhK-1Ks_X02Q_Sq1Z1Pq-
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
DATABASE_URL=postgres://super_admin:solotech25@localhost:5432/playzer-dev
BETTER_AUTH_SECRET=767a2e0fb8b58a9f4df3155d88d22ecdc4eaea4447565c2ed518c532f6410598
BETTER_AUTH_URL=http://localhost:5173
APP_BASE_URL=http://localhost:5173
ONBOARDING_TTL_MINUTES=60
SMTP_HOST=sandbox.smtp.mailtrap.io
SMTP_PORT=587
SMTP_USER=6af2bf58132d12
SMTP_PASS=1253b2fe0fc47a
SMTP_FROM=Playzer <no-reply@playzer.app>

View File

@@ -1,9 +1,5 @@
SUPABASE_URL=https://your-project-ref.supabase.co BETTER_AUTH_SECRET=your-random-secret
SUPABASE_ANON_KEY=your-anon-key BETTER_AUTH_URL=http://localhost:3000
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Fallbacks accepted too:
# VITE_SUPABASE_URL=https://your-project-ref.supabase.co
# VITE_SUPABASE_ANON_KEY=your-anon-key
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173 CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
LOG_LEVEL=debug LOG_LEVEL=debug
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/monorepo DATABASE_URL=postgresql://postgres:postgres@localhost:5432/monorepo
@@ -16,3 +12,7 @@ SMTP_PORT=587
SMTP_USER=user@example.com SMTP_USER=user@example.com
SMTP_PASS=your-smtp-password SMTP_PASS=your-smtp-password
SMTP_FROM=Playzer <no-reply@example.com> SMTP_FROM=Playzer <no-reply@example.com>
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

View File

@@ -1,2 +1,3 @@
# deps # deps
node_modules/ node_modules/
prisma.config.prod.ts

View File

@@ -9,3 +9,21 @@ bun run dev
``` ```
open http://localhost:3000 open http://localhost:3000
## Tests
Run all backend tests:
```sh
bun run test
```
### Test naming convention
- `*.test.ts` or `*.spec.ts`: service-level tests. These run with the shared Prisma preload at `test/support/prisma.mock.ts`.
- `*.handler.test.ts`: handler-level tests. These should mock the service module locally with `mock.module(...)` and import the handler after the mock is in place.
### Recommended pattern
- Keep business logic tests focused on the service layer.
- Use handler tests only to verify request parsing, status mapping, and response shaping.
- Put shared Prisma mocking helpers in `test/support/`.

3
apps/backend/bunfig.toml Normal file
View File

@@ -0,0 +1,3 @@
[test]
preload = ["./test/support/prisma.mock.ts"]
coverageThreshold = 0.8

View File

@@ -4,6 +4,7 @@
"dev": "bun run --hot src/server.ts", "dev": "bun run --hot src/server.ts",
"start": "bun src/server.ts", "start": "bun src/server.ts",
"build": "tsc -b", "build": "tsc -b",
"test": "bun test",
"lint": "biome check .", "lint": "biome check .",
"lint:fix": "biome check --write .", "lint:fix": "biome check --write .",
"format": "biome format --write .", "format": "biome format --write .",
@@ -14,17 +15,15 @@
"prisma:seed:reset:plans": "bun prisma/reset-plans.ts" "prisma:seed:reset:plans": "bun prisma/reset-plans.ts"
}, },
"dependencies": { "dependencies": {
"@better-auth/prisma-adapter": "^1.6.2", "@better-auth/infra": "^0.2.4",
"@hono/zod-validator": "^0.7.6", "@hono/zod-validator": "^0.7.6",
"@prisma/adapter-pg": "^7.6.0", "@prisma/adapter-pg": "^7.6.0",
"@prisma/client": "^7", "@prisma/client": "^7",
"@repo/api-contract": "workspace:*", "@repo/api-contract": "workspace:*",
"@supabase/supabase-js": "2.101.1", "better-auth": "^1.6.4",
"@types/bcrypt": "^6.0.0",
"bcrypt": "^6.0.0",
"better-auth": "^1.6.2",
"dotenv": "^17.4.1", "dotenv": "^17.4.1",
"hono": "4.12.10", "hono": "4.12.10",
"mailtrap": "^4.5.1",
"nodemailer": "^8.0.5", "nodemailer": "^8.0.5",
"pg": "^8.20.0", "pg": "^8.20.0",
"pino": "10.3.1", "pino": "10.3.1",
@@ -35,6 +34,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/nodemailer": "^8.0.0" "@types/nodemailer": "^8.0.0",
"bun-mock-prisma": "^1.2.1"
} }
} }

View File

@@ -0,0 +1,65 @@
model User {
id String @id
name String
email String
emailVerified Boolean @default(false)
image String?
phone String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
role String @default("member")
complexes ComplexUser[]
@@unique([email])
@@map("users")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@index([userId])
@@map("sessions")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("accounts")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([identifier])
@@map("verification")
}

View File

@@ -0,0 +1,56 @@
model Complex {
id String @id @db.Uuid
complexName String @map("complex_name")
physicalAddress String? @map("physical_address") @db.VarChar(200)
city String? @map("city") @db.VarChar(100)
state String? @map("state") @db.VarChar(100)
country String? @map("country") @db.VarChar(100)
complexSlug String @unique @map("complex_slug")
adminEmail String @map("admin_email")
planCode String? @map("plan_code") @db.VarChar(10)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
plan Plan? @relation(fields: [planCode], references: [code])
users ComplexUser[]
invitations ComplexInvitation[]
courts Court[]
@@index([planCode])
@@index([complexSlug])
@@map("complexes")
}
enum ComplexUserRole {
ADMIN
EMPLOYEE
}
model ComplexUser {
complexId String @map("complex_id") @db.Uuid
userId String @map("user_id")
role ComplexUserRole @default(ADMIN)
createdAt DateTime @default(now()) @map("created_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([complexId, userId])
@@index([userId])
@@map("complex_users")
}
model ComplexInvitation {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
email String @db.VarChar(255)
tokenHash String @unique @map("token_hash")
expiresAt DateTime @map("expires_at")
acceptedAt DateTime? @map("accepted_at")
revokedAt DateTime? @map("revoked_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
@@index([complexId, email])
@@index([email])
@@map("complex_invitations")
}

View File

@@ -0,0 +1,114 @@
enum DayOfWeek {
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
}
enum CourtBookingStatus {
CONFIRMED
CANCELLED
COMPLETED
NOSHOW
}
model Sport {
id String @id @db.Uuid
name String @unique
slug String @unique
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
courts Court[]
@@map("sports")
}
model Court {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
sportId String @map("sport_id") @db.Uuid
name String
slotDurationMinutes Int @map("slot_duration_minutes")
basePrice Decimal @map("base_price") @db.Decimal(19, 2)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
sport Sport @relation(fields: [sportId], references: [id])
availabilities CourtAvailability[]
priceRules CourtPriceRule[]
bookings CourtBooking[]
@@index([complexId])
@@index([sportId])
@@map("courts")
}
model CourtAvailability {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
dayOfWeek DayOfWeek @map("day_of_week")
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
createdAt DateTime @default(now()) @map("created_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, dayOfWeek])
@@map("court_availabilities")
}
model CourtPriceRule {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
dayOfWeek DayOfWeek? @map("day_of_week")
startTime String? @map("start_time") @db.VarChar(5)
endTime String? @map("end_time") @db.VarChar(5)
price Decimal @db.Decimal(19, 2)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, isActive])
@@map("court_price_rules")
}
model CourtBooking {
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@unique([courtId, bookingDate, startTime])
@@index([courtId, bookingDate])
@@index([bookingDate])
@@map("court_bookings")
}
model CourtBookingLog {
id String @id @db.Uuid
bookingCode String @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
previousStatus CourtBookingStatus @map("previous_status")
newStatus CourtBookingStatus @map("new_status")
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
changedAt DateTime @default(now()) @map("changed_at")
@@map("court_booking_logs")
@@index([courtId, newStatus])
}

View File

@@ -0,0 +1,104 @@
/*
Warnings:
- The primary key for the `users` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `created_at` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `full_name` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `supabase_user_id` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `updated_at` on the `users` table. All the data in the column will be lost.
- You are about to drop the `complex_users` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `name` to the `users` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- AlterEnum
ALTER TYPE "ComplexUserRole" ADD VALUE 'EMPLOYEE';
-- DropForeignKey
ALTER TABLE "complex_users" DROP CONSTRAINT "complex_users_complex_id_fkey";
-- DropForeignKey
ALTER TABLE "complex_users" DROP CONSTRAINT "complex_users_user_id_fkey";
-- DropIndex
DROP INDEX "users_supabase_user_id_key";
-- AlterTable
ALTER TABLE "users" DROP CONSTRAINT "users_pkey",
DROP COLUMN "created_at",
DROP COLUMN "full_name",
DROP COLUMN "supabase_user_id",
DROP COLUMN "updated_at",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "image" TEXT,
ADD COLUMN "name" TEXT NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id");
-- DropTable
DROP TABLE "complex_users";
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"userId" TEXT NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"idToken" TEXT,
"accessTokenExpiresAt" TIMESTAMP(3),
"refreshTokenExpiresAt" TIMESTAMP(3),
"scope" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "sessions_userId_idx" ON "sessions"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
-- CreateIndex
CREATE INDEX "accounts_userId_idx" ON "accounts"("userId");
-- CreateIndex
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "complex_users" (
"complex_id" UUID NOT NULL,
"user_id" TEXT NOT NULL,
"role" "ComplexUserRole" NOT NULL DEFAULT 'ADMIN',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "complex_users_pkey" PRIMARY KEY ("complex_id","user_id")
);
-- CreateIndex
CREATE INDEX "complex_users_user_id_idx" ON "complex_users"("user_id");
-- AddForeignKey
ALTER TABLE "complex_users" ADD CONSTRAINT "complex_users_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "complex_users" ADD CONSTRAINT "complex_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'member';

View File

@@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "complex_invitations" (
"id" UUID NOT NULL,
"complex_id" UUID NOT NULL,
"email" VARCHAR(255) NOT NULL,
"token_hash" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"accepted_at" TIMESTAMP(3),
"revoked_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "complex_invitations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "complex_invitations_token_hash_key" ON "complex_invitations"("token_hash");
-- CreateIndex
CREATE INDEX "complex_invitations_complex_id_email_idx" ON "complex_invitations"("complex_id", "email");
-- CreateIndex
CREATE INDEX "complex_invitations_email_idx" ON "complex_invitations"("email");
-- AddForeignKey
ALTER TABLE "complex_invitations" ADD CONSTRAINT "complex_invitations_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "court_booking_logs" (
"id" UUID NOT NULL,
"booking_code" UUID NOT NULL,
"court_id" UUID NOT NULL,
"booking_date" DATE NOT NULL,
"start_time" VARCHAR(5) NOT NULL,
"end_time" VARCHAR(5) NOT NULL,
"previous_status" "CourtBookingStatus" NOT NULL,
"new_status" "CourtBookingStatus" NOT NULL,
"changed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "court_booking_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "court_booking_logs_court_id_new_status_idx" ON "court_booking_logs"("court_id", "new_status");

View File

@@ -0,0 +1,13 @@
/*
Warnings:
- Added the required column `customer_name` to the `court_booking_logs` table without a default value. This is not possible if the table is not empty.
- Added the required column `customer_phone` to the `court_booking_logs` table without a default value. This is not possible if the table is not empty.
*/
-- AlterEnum
ALTER TYPE "CourtBookingStatus" ADD VALUE 'NOSHOW';
-- AlterTable
ALTER TABLE "court_booking_logs" ADD COLUMN "customer_name" VARCHAR(120) NOT NULL,
ADD COLUMN "customer_phone" VARCHAR(30) NOT NULL;

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- Changed the type of `booking_code` on the `court_booking_logs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- AlterTable
ALTER TABLE "court_booking_logs" DROP COLUMN "booking_code",
ADD COLUMN "booking_code" VARCHAR(8) NOT NULL;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "phone" TEXT;

View File

@@ -0,0 +1,16 @@
model PasswordResetRequest {
id String @id @db.Uuid
email String
otpHash String @map("otp_hash")
otpExpiresAt DateTime @map("otp_expires_at")
otpAttempts Int @default(0) @map("otp_attempts")
otpLastSentAt DateTime @map("otp_last_sent_at")
otpResendCount Int @default(0) @map("otp_resend_count")
usedAt DateTime? @map("used_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([email])
@@index([otpExpiresAt])
@@map("password_reset_requests")
}

View File

@@ -0,0 +1,10 @@
model Plan {
code String @id @db.VarChar(10)
name String @db.VarChar(30)
price Decimal @db.Decimal(19, 2)
rules Json
lastUpdatedAt DateTime @default(now()) @map("last_updated_at")
complexes Complex[]
@@map("plans")
}

View File

@@ -6,253 +6,3 @@ generator client {
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
} }
// ============== APP MODELS (existing) ==============
model Plan {
code String @id @map("code") @db.VarChar(10)
name String @db.VarChar(30)
price Decimal @map("price") @db.Decimal(19, 2)
rules Json @map("rules")
lastUpdatedAt DateTime @default(now()) @map("last_updated_at")
complexes Complex[]
@@map("plans")
}
model Complex {
id String @id @db.Uuid
complexSlug String @unique @map("complex_slug")
complexName String @map("complex_name")
physicalAddress String @map("physical_address")
city String?
state String?
country String?
adminEmail String @map("admin_email")
planCode String @map("plan_code")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
plan Plan @relation(fields: [planCode], references: [code])
users ComplexUser[]
courts Court[]
@@index([adminEmail])
@@map("complexes")
}
model ComplexUser {
id String @id @default(cuid()) @map("id")
complexId String @map("complex_id") @db.Uuid
userId String @map("user_id")
role ComplexUserRole
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([complexId, userId])
@@map("complex_users")
}
enum ComplexUserRole {
ADMIN
MANAGER
USER
}
enum DayOfWeek {
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
}
enum CourtBookingStatus {
CONFIRMED
CANCELLED
COMPLETED
}
model Sport {
id String @id @db.Uuid
name String @unique
slug String @unique
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
courts Court[]
@@map("sports")
}
model Court {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
sportId String @map("sport_id") @db.Uuid
name String
slotDurationMinutes Int @map("slot_duration_minutes")
basePrice Decimal @map("base_price") @db.Decimal(19, 2)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
sport Sport @relation(fields: [sportId], references: [id])
availabilities CourtAvailability[]
priceRules CourtPriceRule[]
bookings CourtBooking[]
@@index([complexId])
@@index([sportId])
@@map("courts")
}
model CourtAvailability {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
dayOfWeek DayOfWeek @map("day_of_week")
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
createdAt DateTime @default(now()) @map("created_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, dayOfWeek])
@@map("court_availabilities")
}
model CourtPriceRule {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
dayOfWeek DayOfWeek? @map("day_of_week")
startTime String? @map("start_time") @db.VarChar(5)
endTime String? @map("end_time") @db.VarChar(5)
price Decimal @db.Decimal(19, 2)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, isActive])
@@map("court_price_rules")
}
model CourtBooking {
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@unique([courtId, bookingDate, startTime])
@@index([courtId, bookingDate])
@@index([bookingDate])
@@map("court_bookings")
}
model OnboardingRequest {
id String @id @db.Uuid
fullName String @map("full_name")
email String
otpHash String @map("otp_hash")
otpExpiresAt DateTime @map("otp_expires_at")
otpAttempts Int @default(0) @map("otp_attempts")
otpLastSentAt DateTime @map("otp_last_sent_at")
otpResendCount Int @default(0) @map("otp_resend_count")
emailVerifiedAt DateTime? @map("email_verified_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([email])
@@index([otpExpiresAt])
@@map("onboarding_requests")
}
model PasswordResetRequest {
id String @id @db.Uuid
email String
otpHash String @map("otp_hash")
otpExpiresAt DateTime @map("otp_expires_at")
otpAttempts Int @default(0) @map("otp_attempts")
otpLastSentAt DateTime @map("otp_last_sent_at")
otpResendCount Int @default(0) @map("otp_resend_count")
usedAt DateTime? @map("used_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([email])
@@index([otpExpiresAt])
@@map("password_reset_requests")
}
// ============== BETTER AUTH MODELS ==============
model User {
id String @id @default(cuid()) @map("id")
supabaseUserId String? @unique @map("supabase_user_id") @db.Uuid
name String?
email String @unique
emailVerified Boolean @default(false)
image String?
fullName String? @map("full_name")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
accounts Account[]
sessions Session[]
complexes ComplexUser[]
@@map("users")
}
model Session {
id String @id @default(cuid()) @map("id")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
expiresAt DateTime @map("expires_at")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("session")
}
model Account {
id String @id @default(cuid()) @map("id")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accountId String @map("account_id")
providerId String @map("provider_id")
accessToken String? @map("access_token")
refreshToken String? @map("refresh_token")
accessTokenExpiresAt DateTime? @map("access_token_expires_at")
refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
scope String? @map("scope")
idToken String? @map("id_token")
password String? @map("password")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("account")
}
model Verification {
id String @id @default(cuid()) @map("id")
identifier String
value String
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("verification")
}

View File

@@ -0,0 +1 @@
// User model is defined in auth.prisma (Better Auth schema).

View File

@@ -3,10 +3,15 @@ import type { AppEnv } from '@/types/hono';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
import { requestId } from 'hono/request-id'; import { requestId } from 'hono/request-id';
import { errorHandler } from './lib/http/error-handler';
import { requestIdMiddleware } from './lib/http/request-id';
export function createApp() { export function createApp() {
const app = new Hono<AppEnv>(); const app = new Hono<AppEnv>();
app.use('*', requestIdMiddleware);
app.use('*', errorHandler);
const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173') const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
.split(',') .split(',')
.map((value) => value.trim()) .map((value) => value.trim())
@@ -21,6 +26,8 @@ export function createApp() {
}, },
allowHeaders: ['Authorization', 'Content-Type'], allowHeaders: ['Authorization', 'Content-Type'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true, credentials: true,
}) })
); );

View File

@@ -1,21 +0,0 @@
import { auth } from '@/lib/auth';
import { cors } from 'hono/cors';
export function registerAuthRoutes(app) {
const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
app.use('/api/auth/*', cors({
origin: (origin) => {
if (!origin)
return allowedOrigins[0] ?? '';
return allowedOrigins.includes(origin) ? origin : '';
},
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
}));
app.on(['POST', 'GET'], '/api/auth/*', (c) => {
return auth.handler(c.req.raw);
});
}

View File

@@ -1,28 +0,0 @@
import { auth } from '@/lib/auth';
import type { AppEnv } from '@/types/hono';
import type { Hono } from 'hono';
import { cors } from 'hono/cors';
export function registerAuthRoutes(app: Hono<AppEnv>) {
const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
app.use(
'/api/auth/*',
cors({
origin: (origin) => {
if (!origin) return allowedOrigins[0] ?? '';
return allowedOrigins.includes(origin) ? origin : '';
},
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
})
);
app.on(['POST', 'GET'], '/api/auth/*', (c) => {
return auth.handler(c.req.raw);
});
}

View File

@@ -18,10 +18,25 @@ export { Prisma }
export * as $Enums from './enums' export * as $Enums from './enums'
export * from './enums'; export * from './enums';
/** /**
* Model Plan * Model User
* *
*/ */
export type Plan = Prisma.PlanModel export type User = Prisma.UserModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model Verification
*
*/
export type Verification = Prisma.VerificationModel
/** /**
* Model Complex * Model Complex
* *
@@ -32,6 +47,11 @@ export type Complex = Prisma.ComplexModel
* *
*/ */
export type ComplexUser = Prisma.ComplexUserModel export type ComplexUser = Prisma.ComplexUserModel
/**
* Model ComplexInvitation
*
*/
export type ComplexInvitation = Prisma.ComplexInvitationModel
/** /**
* Model Sport * Model Sport
* *
@@ -57,6 +77,11 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
* *
*/ */
export type CourtBooking = Prisma.CourtBookingModel export type CourtBooking = Prisma.CourtBookingModel
/**
* Model CourtBookingLog
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/** /**
* Model OnboardingRequest * Model OnboardingRequest
* *
@@ -68,22 +93,7 @@ export type OnboardingRequest = Prisma.OnboardingRequestModel
*/ */
export type PasswordResetRequest = Prisma.PasswordResetRequestModel export type PasswordResetRequest = Prisma.PasswordResetRequestModel
/** /**
* Model User * Model Plan
* *
*/ */
export type User = Prisma.UserModel export type Plan = Prisma.PlanModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model Verification
*
*/
export type Verification = Prisma.VerificationModel

View File

@@ -31,8 +31,8 @@ export * from "./enums"
* const prisma = new PrismaClient({ * const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* }) * })
* // Fetch zero or more Plans * // Fetch zero or more Users
* const plans = await prisma.plan.findMany() * const users = await prisma.user.findMany()
* ``` * ```
* *
* Read more in our [docs](https://pris.ly/d/client). * Read more in our [docs](https://pris.ly/d/client).
@@ -42,10 +42,25 @@ export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts exten
export { Prisma } export { Prisma }
/** /**
* Model Plan * Model User
* *
*/ */
export type Plan = Prisma.PlanModel export type User = Prisma.UserModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model Verification
*
*/
export type Verification = Prisma.VerificationModel
/** /**
* Model Complex * Model Complex
* *
@@ -56,6 +71,11 @@ export type Complex = Prisma.ComplexModel
* *
*/ */
export type ComplexUser = Prisma.ComplexUserModel export type ComplexUser = Prisma.ComplexUserModel
/**
* Model ComplexInvitation
*
*/
export type ComplexInvitation = Prisma.ComplexInvitationModel
/** /**
* Model Sport * Model Sport
* *
@@ -81,6 +101,11 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
* *
*/ */
export type CourtBooking = Prisma.CourtBookingModel export type CourtBooking = Prisma.CourtBookingModel
/**
* Model CourtBookingLog
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/** /**
* Model OnboardingRequest * Model OnboardingRequest
* *
@@ -92,22 +117,7 @@ export type OnboardingRequest = Prisma.OnboardingRequestModel
*/ */
export type PasswordResetRequest = Prisma.PasswordResetRequestModel export type PasswordResetRequest = Prisma.PasswordResetRequestModel
/** /**
* Model User * Model Plan
* *
*/ */
export type User = Prisma.UserModel export type Plan = Prisma.PlanModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model Verification
*
*/
export type Verification = Prisma.VerificationModel

View File

@@ -29,39 +29,24 @@ export type StringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string
} }
export type DecimalFilter<$PrismaModel = never> = { export type BoolFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel> not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
} }
export type JsonFilter<$PrismaModel = never> = export type StringNullableFilter<$PrismaModel = never> = {
| Prisma.PatchUndefined< equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>, in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
Required<JsonFilterBase<$PrismaModel>> notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
export type JsonFilterBase<$PrismaModel = never> = { gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
path?: string[] startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> mode?: Prisma.QueryMode
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
} }
export type DateTimeFilter<$PrismaModel = never> = { export type DateTimeFilter<$PrismaModel = never> = {
@@ -75,6 +60,11 @@ export type DateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
} }
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringWithAggregatesFilter<$PrismaModel = never> = { export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -93,113 +83,12 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedStringFilter<$PrismaModel>
} }
export type DecimalWithAggregatesFilter<$PrismaModel = never> = { export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalWithAggregatesFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedDecimalFilter<$PrismaModel>
_sum?: Prisma.NestedDecimalFilter<$PrismaModel>
_min?: Prisma.NestedDecimalFilter<$PrismaModel>
_max?: Prisma.NestedDecimalFilter<$PrismaModel>
}
export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedJsonFilter<$PrismaModel>
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type UuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type UuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel> _min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedBoolFilter<$PrismaModel>
} }
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
@@ -220,12 +109,70 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> _max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }
export type BoolWithAggregatesFilter<$PrismaModel = never> = { export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type UuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type UuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
} }
export type EnumComplexUserRoleFilter<$PrismaModel = never> = { export type EnumComplexUserRoleFilter<$PrismaModel = never> = {
@@ -256,6 +203,17 @@ export type IntFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number
} }
export type DecimalFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
}
export type IntWithAggregatesFilter<$PrismaModel = never> = { export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
@@ -272,6 +230,22 @@ export type IntWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedIntFilter<$PrismaModel> _max?: Prisma.NestedIntFilter<$PrismaModel>
} }
export type DecimalWithAggregatesFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalWithAggregatesFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedDecimalFilter<$PrismaModel>
_sum?: Prisma.NestedDecimalFilter<$PrismaModel>
_min?: Prisma.NestedDecimalFilter<$PrismaModel>
_max?: Prisma.NestedDecimalFilter<$PrismaModel>
}
export type EnumDayOfWeekFilter<$PrismaModel = never> = { export type EnumDayOfWeekFilter<$PrismaModel = never> = {
equals?: $Enums.DayOfWeek | Prisma.EnumDayOfWeekFieldRefInput<$PrismaModel> equals?: $Enums.DayOfWeek | Prisma.EnumDayOfWeekFieldRefInput<$PrismaModel>
in?: $Enums.DayOfWeek[] | Prisma.ListEnumDayOfWeekFieldRefInput<$PrismaModel> in?: $Enums.DayOfWeek[] | Prisma.ListEnumDayOfWeekFieldRefInput<$PrismaModel>
@@ -323,56 +297,55 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> _max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
} }
export type DateTimeNullableFilter<$PrismaModel = never> = { export type JsonFilter<$PrismaModel = never> =
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null | Prisma.PatchUndefined<
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Required<JsonFilterBase<$PrismaModel>>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> >
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> export type JsonFilterBase<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
} }
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type JsonWithAggregatesFilter<$PrismaModel = never> =
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null | Prisma.PatchUndefined<
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Required<JsonWithAggregatesFilterBase<$PrismaModel>>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> >
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type UuidNullableFilter<$PrismaModel = never> = { export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null path?: string[]
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
mode?: Prisma.QueryMode array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
} lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
export type UuidNullableWithAggregatesFilter<$PrismaModel = never> = { gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null _count?: Prisma.NestedIntFilter<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> _min?: Prisma.NestedJsonFilter<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> _max?: Prisma.NestedJsonFilter<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }
export type NestedStringFilter<$PrismaModel = never> = { export type NestedStringFilter<$PrismaModel = never> = {
@@ -389,15 +362,23 @@ export type NestedStringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string
} }
export type NestedDecimalFilter<$PrismaModel = never> = { export type NestedBoolFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel> not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel> }
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> export type NestedStringNullableFilter<$PrismaModel = never> = {
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedDecimalFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
} }
export type NestedDateTimeFilter<$PrismaModel = never> = { export type NestedDateTimeFilter<$PrismaModel = never> = {
@@ -439,102 +420,12 @@ export type NestedIntFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number
} }
export type NestedDecimalWithAggregatesFilter<$PrismaModel = never> = { export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalWithAggregatesFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedDecimalFilter<$PrismaModel>
_sum?: Prisma.NestedDecimalFilter<$PrismaModel>
_min?: Prisma.NestedDecimalFilter<$PrismaModel>
_max?: Prisma.NestedDecimalFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedUuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
}
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel> _min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedBoolFilter<$PrismaModel>
} }
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
@@ -565,12 +456,68 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
} }
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = { export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedUuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
} }
export type NestedEnumComplexUserRoleFilter<$PrismaModel = never> = { export type NestedEnumComplexUserRoleFilter<$PrismaModel = never> = {
@@ -590,6 +537,17 @@ export type NestedEnumComplexUserRoleWithAggregatesFilter<$PrismaModel = never>
_max?: Prisma.NestedEnumComplexUserRoleFilter<$PrismaModel> _max?: Prisma.NestedEnumComplexUserRoleFilter<$PrismaModel>
} }
export type NestedDecimalFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
@@ -617,6 +575,22 @@ export type NestedFloatFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatFilter<$PrismaModel> | number not?: Prisma.NestedFloatFilter<$PrismaModel> | number
} }
export type NestedDecimalWithAggregatesFilter<$PrismaModel = never> = {
equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
notIn?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel>
lt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
lte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gt?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
gte?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel>
not?: Prisma.NestedDecimalWithAggregatesFilter<$PrismaModel> | runtime.Decimal | runtime.DecimalJsLike | number | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedDecimalFilter<$PrismaModel>
_sum?: Prisma.NestedDecimalFilter<$PrismaModel>
_min?: Prisma.NestedDecimalFilter<$PrismaModel>
_max?: Prisma.NestedDecimalFilter<$PrismaModel>
}
export type NestedEnumDayOfWeekFilter<$PrismaModel = never> = { export type NestedEnumDayOfWeekFilter<$PrismaModel = never> = {
equals?: $Enums.DayOfWeek | Prisma.EnumDayOfWeekFieldRefInput<$PrismaModel> equals?: $Enums.DayOfWeek | Prisma.EnumDayOfWeekFieldRefInput<$PrismaModel>
in?: $Enums.DayOfWeek[] | Prisma.ListEnumDayOfWeekFieldRefInput<$PrismaModel> in?: $Enums.DayOfWeek[] | Prisma.ListEnumDayOfWeekFieldRefInput<$PrismaModel>
@@ -668,54 +642,28 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> _max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
} }
export type NestedDateTimeNullableFilter<$PrismaModel = never> = { export type NestedJsonFilter<$PrismaModel = never> =
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null | Prisma.PatchUndefined<
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null Required<NestedJsonFilterBase<$PrismaModel>>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> >
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null path?: string[]
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
} gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
export type NestedUuidNullableFilter<$PrismaModel = never> = { not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
}
export type NestedUuidNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }

View File

@@ -11,8 +11,7 @@
export const ComplexUserRole = { export const ComplexUserRole = {
ADMIN: 'ADMIN', ADMIN: 'ADMIN',
MANAGER: 'MANAGER', EMPLOYEE: 'EMPLOYEE'
USER: 'USER'
} as const } as const
export type ComplexUserRole = (typeof ComplexUserRole)[keyof typeof ComplexUserRole] export type ComplexUserRole = (typeof ComplexUserRole)[keyof typeof ComplexUserRole]
@@ -34,7 +33,8 @@ export type DayOfWeek = (typeof DayOfWeek)[keyof typeof DayOfWeek]
export const CourtBookingStatus = { export const CourtBookingStatus = {
CONFIRMED: 'CONFIRMED', CONFIRMED: 'CONFIRMED',
CANCELLED: 'CANCELLED', CANCELLED: 'CANCELLED',
COMPLETED: 'COMPLETED' COMPLETED: 'COMPLETED',
NOSHOW: 'NOSHOW'
} as const } as const
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus] export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -51,20 +51,22 @@ export const AnyNull = runtime.AnyNull
export const ModelName = { export const ModelName = {
Plan: 'Plan', User: 'User',
Session: 'Session',
Account: 'Account',
Verification: 'Verification',
Complex: 'Complex', Complex: 'Complex',
ComplexUser: 'ComplexUser', ComplexUser: 'ComplexUser',
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport', Sport: 'Sport',
Court: 'Court', Court: 'Court',
CourtAvailability: 'CourtAvailability', CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule', CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking', CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
OnboardingRequest: 'OnboardingRequest', OnboardingRequest: 'OnboardingRequest',
PasswordResetRequest: 'PasswordResetRequest', PasswordResetRequest: 'PasswordResetRequest',
User: 'User', Plan: 'Plan'
Session: 'Session',
Account: 'Account',
Verification: 'Verification'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -83,28 +85,76 @@ export const TransactionIsolationLevel = runtime.makeStrictEnum({
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const PlanScalarFieldEnum = { export const UserScalarFieldEnum = {
code: 'code', id: 'id',
name: 'name', name: 'name',
price: 'price', email: 'email',
rules: 'rules', emailVerified: 'emailVerified',
lastUpdatedAt: 'lastUpdatedAt' image: 'image',
phone: 'phone',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
} as const } as const
export type PlanScalarFieldEnum = (typeof PlanScalarFieldEnum)[keyof typeof PlanScalarFieldEnum] export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const SessionScalarFieldEnum = {
id: 'id',
expiresAt: 'expiresAt',
token: 'token',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
userId: 'userId'
} as const
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
export const AccountScalarFieldEnum = {
id: 'id',
accountId: 'accountId',
providerId: 'providerId',
userId: 'userId',
accessToken: 'accessToken',
refreshToken: 'refreshToken',
idToken: 'idToken',
accessTokenExpiresAt: 'accessTokenExpiresAt',
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
scope: 'scope',
password: 'password',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
export const VerificationScalarFieldEnum = {
id: 'id',
identifier: 'identifier',
value: 'value',
expiresAt: 'expiresAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const ComplexScalarFieldEnum = { export const ComplexScalarFieldEnum = {
id: 'id', id: 'id',
complexSlug: 'complexSlug',
complexName: 'complexName', complexName: 'complexName',
physicalAddress: 'physicalAddress', physicalAddress: 'physicalAddress',
city: 'city', city: 'city',
state: 'state', state: 'state',
country: 'country', country: 'country',
complexSlug: 'complexSlug',
adminEmail: 'adminEmail', adminEmail: 'adminEmail',
planCode: 'planCode', planCode: 'planCode',
isActive: 'isActive',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -113,15 +163,28 @@ export type ComplexScalarFieldEnum = (typeof ComplexScalarFieldEnum)[keyof typeo
export const ComplexUserScalarFieldEnum = { export const ComplexUserScalarFieldEnum = {
id: 'id',
complexId: 'complexId', complexId: 'complexId',
userId: 'userId', userId: 'userId',
role: 'role', role: 'role',
createdAt: 'createdAt'
} as const
export type ComplexUserScalarFieldEnum = (typeof ComplexUserScalarFieldEnum)[keyof typeof ComplexUserScalarFieldEnum]
export const ComplexInvitationScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
email: 'email',
tokenHash: 'tokenHash',
expiresAt: 'expiresAt',
acceptedAt: 'acceptedAt',
revokedAt: 'revokedAt',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
export type ComplexUserScalarFieldEnum = (typeof ComplexUserScalarFieldEnum)[keyof typeof ComplexUserScalarFieldEnum] export type ComplexInvitationScalarFieldEnum = (typeof ComplexInvitationScalarFieldEnum)[keyof typeof ComplexInvitationScalarFieldEnum]
export const SportScalarFieldEnum = { export const SportScalarFieldEnum = {
@@ -194,6 +257,23 @@ export const CourtBookingScalarFieldEnum = {
export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum] export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum]
export const CourtBookingLogScalarFieldEnum = {
id: 'id',
bookingCode: 'bookingCode',
courtId: 'courtId',
bookingDate: 'bookingDate',
startTime: 'startTime',
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
customerName: 'customerName',
customerPhone: 'customerPhone',
changedAt: 'changedAt'
} as const
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const OnboardingRequestScalarFieldEnum = { export const OnboardingRequestScalarFieldEnum = {
id: 'id', id: 'id',
fullName: 'fullName', fullName: 'fullName',
@@ -228,64 +308,15 @@ export const PasswordResetRequestScalarFieldEnum = {
export type PasswordResetRequestScalarFieldEnum = (typeof PasswordResetRequestScalarFieldEnum)[keyof typeof PasswordResetRequestScalarFieldEnum] export type PasswordResetRequestScalarFieldEnum = (typeof PasswordResetRequestScalarFieldEnum)[keyof typeof PasswordResetRequestScalarFieldEnum]
export const UserScalarFieldEnum = { export const PlanScalarFieldEnum = {
id: 'id', code: 'code',
supabaseUserId: 'supabaseUserId',
name: 'name', name: 'name',
email: 'email', price: 'price',
emailVerified: 'emailVerified', rules: 'rules',
image: 'image', lastUpdatedAt: 'lastUpdatedAt'
fullName: 'fullName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const } as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] export type PlanScalarFieldEnum = (typeof PlanScalarFieldEnum)[keyof typeof PlanScalarFieldEnum]
export const SessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
token: 'token',
expiresAt: 'expiresAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
export const AccountScalarFieldEnum = {
id: 'id',
userId: 'userId',
accountId: 'accountId',
providerId: 'providerId',
accessToken: 'accessToken',
refreshToken: 'refreshToken',
accessTokenExpiresAt: 'accessTokenExpiresAt',
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
scope: 'scope',
idToken: 'idToken',
password: 'password',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
export const VerificationScalarFieldEnum = {
id: 'id',
identifier: 'identifier',
value: 'value',
expiresAt: 'expiresAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const SortOrder = { export const SortOrder = {
@@ -311,6 +342,14 @@ export const QueryMode = {
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const JsonNullValueFilter = { export const JsonNullValueFilter = {
DbNull: DbNull, DbNull: DbNull,
JsonNull: JsonNull, JsonNull: JsonNull,
@@ -319,11 +358,3 @@ export const JsonNullValueFilter = {
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

View File

@@ -8,18 +8,20 @@
* *
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
export type * from './models/Plan' export type * from './models/User'
export type * from './models/Session'
export type * from './models/Account'
export type * from './models/Verification'
export type * from './models/Complex' export type * from './models/Complex'
export type * from './models/ComplexUser' export type * from './models/ComplexUser'
export type * from './models/ComplexInvitation'
export type * from './models/Sport' export type * from './models/Sport'
export type * from './models/Court' export type * from './models/Court'
export type * from './models/CourtAvailability' export type * from './models/CourtAvailability'
export type * from './models/CourtPriceRule' export type * from './models/CourtPriceRule'
export type * from './models/CourtBooking' export type * from './models/CourtBooking'
export type * from './models/CourtBookingLog'
export type * from './models/OnboardingRequest' export type * from './models/OnboardingRequest'
export type * from './models/PasswordResetRequest' export type * from './models/PasswordResetRequest'
export type * from './models/User' export type * from './models/Plan'
export type * from './models/Session'
export type * from './models/Account'
export type * from './models/Verification'
export type * from './commonInputTypes' export type * from './commonInputTypes'

File diff suppressed because it is too large Load Diff

View File

@@ -25,59 +25,47 @@ export type AggregateComplexUser = {
} }
export type ComplexUserMinAggregateOutputType = { export type ComplexUserMinAggregateOutputType = {
id: string | null
complexId: string | null complexId: string | null
userId: string | null userId: string | null
role: $Enums.ComplexUserRole | null role: $Enums.ComplexUserRole | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null
} }
export type ComplexUserMaxAggregateOutputType = { export type ComplexUserMaxAggregateOutputType = {
id: string | null
complexId: string | null complexId: string | null
userId: string | null userId: string | null
role: $Enums.ComplexUserRole | null role: $Enums.ComplexUserRole | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null
} }
export type ComplexUserCountAggregateOutputType = { export type ComplexUserCountAggregateOutputType = {
id: number
complexId: number complexId: number
userId: number userId: number
role: number role: number
createdAt: number createdAt: number
updatedAt: number
_all: number _all: number
} }
export type ComplexUserMinAggregateInputType = { export type ComplexUserMinAggregateInputType = {
id?: true
complexId?: true complexId?: true
userId?: true userId?: true
role?: true role?: true
createdAt?: true createdAt?: true
updatedAt?: true
} }
export type ComplexUserMaxAggregateInputType = { export type ComplexUserMaxAggregateInputType = {
id?: true
complexId?: true complexId?: true
userId?: true userId?: true
role?: true role?: true
createdAt?: true createdAt?: true
updatedAt?: true
} }
export type ComplexUserCountAggregateInputType = { export type ComplexUserCountAggregateInputType = {
id?: true
complexId?: true complexId?: true
userId?: true userId?: true
role?: true role?: true
createdAt?: true createdAt?: true
updatedAt?: true
_all?: true _all?: true
} }
@@ -154,12 +142,10 @@ export type ComplexUserGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
} }
export type ComplexUserGroupByOutputType = { export type ComplexUserGroupByOutputType = {
id: string
complexId: string complexId: string
userId: string userId: string
role: $Enums.ComplexUserRole role: $Enums.ComplexUserRole
createdAt: Date createdAt: Date
updatedAt: Date
_count: ComplexUserCountAggregateOutputType | null _count: ComplexUserCountAggregateOutputType | null
_min: ComplexUserMinAggregateOutputType | null _min: ComplexUserMinAggregateOutputType | null
_max: ComplexUserMaxAggregateOutputType | null _max: ComplexUserMaxAggregateOutputType | null
@@ -184,29 +170,24 @@ export type ComplexUserWhereInput = {
AND?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[] AND?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[]
OR?: Prisma.ComplexUserWhereInput[] OR?: Prisma.ComplexUserWhereInput[]
NOT?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[] NOT?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[]
id?: Prisma.StringFilter<"ComplexUser"> | string
complexId?: Prisma.UuidFilter<"ComplexUser"> | string complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.StringFilter<"ComplexUser"> | string userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput> complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
} }
export type ComplexUserOrderByWithRelationInput = { export type ComplexUserOrderByWithRelationInput = {
id?: Prisma.SortOrder
complexId?: Prisma.SortOrder complexId?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
complex?: Prisma.ComplexOrderByWithRelationInput complex?: Prisma.ComplexOrderByWithRelationInput
user?: Prisma.UserOrderByWithRelationInput user?: Prisma.UserOrderByWithRelationInput
} }
export type ComplexUserWhereUniqueInput = Prisma.AtLeast<{ export type ComplexUserWhereUniqueInput = Prisma.AtLeast<{
id?: string
complexId_userId?: Prisma.ComplexUserComplexIdUserIdCompoundUniqueInput complexId_userId?: Prisma.ComplexUserComplexIdUserIdCompoundUniqueInput
AND?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[] AND?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[]
OR?: Prisma.ComplexUserWhereInput[] OR?: Prisma.ComplexUserWhereInput[]
@@ -215,18 +196,15 @@ export type ComplexUserWhereUniqueInput = Prisma.AtLeast<{
userId?: Prisma.StringFilter<"ComplexUser"> | string userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput> complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
}, "id" | "complexId_userId"> }, "complexId_userId">
export type ComplexUserOrderByWithAggregationInput = { export type ComplexUserOrderByWithAggregationInput = {
id?: Prisma.SortOrder
complexId?: Prisma.SortOrder complexId?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.ComplexUserCountOrderByAggregateInput _count?: Prisma.ComplexUserCountOrderByAggregateInput
_max?: Prisma.ComplexUserMaxOrderByAggregateInput _max?: Prisma.ComplexUserMaxOrderByAggregateInput
_min?: Prisma.ComplexUserMinOrderByAggregateInput _min?: Prisma.ComplexUserMinOrderByAggregateInput
@@ -236,73 +214,57 @@ export type ComplexUserScalarWhereWithAggregatesInput = {
AND?: Prisma.ComplexUserScalarWhereWithAggregatesInput | Prisma.ComplexUserScalarWhereWithAggregatesInput[] AND?: Prisma.ComplexUserScalarWhereWithAggregatesInput | Prisma.ComplexUserScalarWhereWithAggregatesInput[]
OR?: Prisma.ComplexUserScalarWhereWithAggregatesInput[] OR?: Prisma.ComplexUserScalarWhereWithAggregatesInput[]
NOT?: Prisma.ComplexUserScalarWhereWithAggregatesInput | Prisma.ComplexUserScalarWhereWithAggregatesInput[] NOT?: Prisma.ComplexUserScalarWhereWithAggregatesInput | Prisma.ComplexUserScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"ComplexUser"> | string
complexId?: Prisma.UuidWithAggregatesFilter<"ComplexUser"> | string complexId?: Prisma.UuidWithAggregatesFilter<"ComplexUser"> | string
userId?: Prisma.StringWithAggregatesFilter<"ComplexUser"> | string userId?: Prisma.StringWithAggregatesFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleWithAggregatesFilter<"ComplexUser"> | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleWithAggregatesFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeWithAggregatesFilter<"ComplexUser"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"ComplexUser"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"ComplexUser"> | Date | string
} }
export type ComplexUserCreateInput = { export type ComplexUserCreateInput = {
id?: string role?: $Enums.ComplexUserRole
role: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutUsersInput complex: Prisma.ComplexCreateNestedOneWithoutUsersInput
user: Prisma.UserCreateNestedOneWithoutComplexesInput user: Prisma.UserCreateNestedOneWithoutComplexesInput
} }
export type ComplexUserUncheckedCreateInput = { export type ComplexUserUncheckedCreateInput = {
id?: string
complexId: string complexId: string
userId: string userId: string
role: $Enums.ComplexUserRole role?: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
} }
export type ComplexUserUpdateInput = { export type ComplexUserUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput
user?: Prisma.UserUpdateOneRequiredWithoutComplexesNestedInput user?: Prisma.UserUpdateOneRequiredWithoutComplexesNestedInput
} }
export type ComplexUserUncheckedUpdateInput = { export type ComplexUserUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string complexId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserCreateManyInput = { export type ComplexUserCreateManyInput = {
id?: string
complexId: string complexId: string
userId: string userId: string
role: $Enums.ComplexUserRole role?: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
} }
export type ComplexUserUpdateManyMutationInput = { export type ComplexUserUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserUncheckedUpdateManyInput = { export type ComplexUserUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string complexId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserListRelationFilter = { export type ComplexUserListRelationFilter = {
@@ -321,30 +283,66 @@ export type ComplexUserComplexIdUserIdCompoundUniqueInput = {
} }
export type ComplexUserCountOrderByAggregateInput = { export type ComplexUserCountOrderByAggregateInput = {
id?: Prisma.SortOrder
complexId?: Prisma.SortOrder complexId?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
} }
export type ComplexUserMaxOrderByAggregateInput = { export type ComplexUserMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
complexId?: Prisma.SortOrder complexId?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
} }
export type ComplexUserMinOrderByAggregateInput = { export type ComplexUserMinOrderByAggregateInput = {
id?: Prisma.SortOrder
complexId?: Prisma.SortOrder complexId?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder }
export type ComplexUserCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
}
export type ComplexUserUncheckedCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
}
export type ComplexUserUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
set?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
disconnect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
delete?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
update?: Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput | Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
}
export type ComplexUserUncheckedUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
set?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
disconnect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
delete?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
update?: Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput | Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
} }
export type ComplexUserCreateNestedManyWithoutComplexInput = { export type ComplexUserCreateNestedManyWithoutComplexInput = {
@@ -393,116 +391,16 @@ export type EnumComplexUserRoleFieldUpdateOperationsInput = {
set?: $Enums.ComplexUserRole set?: $Enums.ComplexUserRole
} }
export type ComplexUserCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
}
export type ComplexUserUncheckedCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
}
export type ComplexUserUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
set?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
disconnect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
delete?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
update?: Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput | Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
}
export type ComplexUserUncheckedUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput> | Prisma.ComplexUserCreateWithoutUserInput[] | Prisma.ComplexUserUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutUserInput | Prisma.ComplexUserCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ComplexUserCreateManyUserInputEnvelope
set?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
disconnect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
delete?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
connect?: Prisma.ComplexUserWhereUniqueInput | Prisma.ComplexUserWhereUniqueInput[]
update?: Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput | Prisma.ComplexUserUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput | Prisma.ComplexUserUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
}
export type ComplexUserCreateWithoutComplexInput = {
id?: string
role: $Enums.ComplexUserRole
createdAt?: Date | string
updatedAt?: Date | string
user: Prisma.UserCreateNestedOneWithoutComplexesInput
}
export type ComplexUserUncheckedCreateWithoutComplexInput = {
id?: string
userId: string
role: $Enums.ComplexUserRole
createdAt?: Date | string
updatedAt?: Date | string
}
export type ComplexUserCreateOrConnectWithoutComplexInput = {
where: Prisma.ComplexUserWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutComplexInput, Prisma.ComplexUserUncheckedCreateWithoutComplexInput>
}
export type ComplexUserCreateManyComplexInputEnvelope = {
data: Prisma.ComplexUserCreateManyComplexInput | Prisma.ComplexUserCreateManyComplexInput[]
skipDuplicates?: boolean
}
export type ComplexUserUpsertWithWhereUniqueWithoutComplexInput = {
where: Prisma.ComplexUserWhereUniqueInput
update: Prisma.XOR<Prisma.ComplexUserUpdateWithoutComplexInput, Prisma.ComplexUserUncheckedUpdateWithoutComplexInput>
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutComplexInput, Prisma.ComplexUserUncheckedCreateWithoutComplexInput>
}
export type ComplexUserUpdateWithWhereUniqueWithoutComplexInput = {
where: Prisma.ComplexUserWhereUniqueInput
data: Prisma.XOR<Prisma.ComplexUserUpdateWithoutComplexInput, Prisma.ComplexUserUncheckedUpdateWithoutComplexInput>
}
export type ComplexUserUpdateManyWithWhereWithoutComplexInput = {
where: Prisma.ComplexUserScalarWhereInput
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutComplexInput>
}
export type ComplexUserScalarWhereInput = {
AND?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
OR?: Prisma.ComplexUserScalarWhereInput[]
NOT?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
id?: Prisma.StringFilter<"ComplexUser"> | string
complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
}
export type ComplexUserCreateWithoutUserInput = { export type ComplexUserCreateWithoutUserInput = {
id?: string role?: $Enums.ComplexUserRole
role: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutUsersInput complex: Prisma.ComplexCreateNestedOneWithoutUsersInput
} }
export type ComplexUserUncheckedCreateWithoutUserInput = { export type ComplexUserUncheckedCreateWithoutUserInput = {
id?: string
complexId: string complexId: string
role: $Enums.ComplexUserRole role?: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
} }
export type ComplexUserCreateOrConnectWithoutUserInput = { export type ComplexUserCreateOrConnectWithoutUserInput = {
@@ -531,115 +429,139 @@ export type ComplexUserUpdateManyWithWhereWithoutUserInput = {
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutUserInput> data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutUserInput>
} }
export type ComplexUserCreateManyComplexInput = { export type ComplexUserScalarWhereInput = {
id?: string AND?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
userId: string OR?: Prisma.ComplexUserScalarWhereInput[]
role: $Enums.ComplexUserRole NOT?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
}
export type ComplexUserCreateWithoutComplexInput = {
role?: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string user: Prisma.UserCreateNestedOneWithoutComplexesInput
} }
export type ComplexUserUpdateWithoutComplexInput = { export type ComplexUserUncheckedCreateWithoutComplexInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string userId: string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
user?: Prisma.UserUpdateOneRequiredWithoutComplexesNestedInput
} }
export type ComplexUserUncheckedUpdateWithoutComplexInput = { export type ComplexUserCreateOrConnectWithoutComplexInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string where: Prisma.ComplexUserWhereUniqueInput
userId?: Prisma.StringFieldUpdateOperationsInput | string create: Prisma.XOR<Prisma.ComplexUserCreateWithoutComplexInput, Prisma.ComplexUserUncheckedCreateWithoutComplexInput>
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserUncheckedUpdateManyWithoutComplexInput = { export type ComplexUserCreateManyComplexInputEnvelope = {
id?: Prisma.StringFieldUpdateOperationsInput | string data: Prisma.ComplexUserCreateManyComplexInput | Prisma.ComplexUserCreateManyComplexInput[]
userId?: Prisma.StringFieldUpdateOperationsInput | string skipDuplicates?: boolean
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole }
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string export type ComplexUserUpsertWithWhereUniqueWithoutComplexInput = {
where: Prisma.ComplexUserWhereUniqueInput
update: Prisma.XOR<Prisma.ComplexUserUpdateWithoutComplexInput, Prisma.ComplexUserUncheckedUpdateWithoutComplexInput>
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutComplexInput, Prisma.ComplexUserUncheckedCreateWithoutComplexInput>
}
export type ComplexUserUpdateWithWhereUniqueWithoutComplexInput = {
where: Prisma.ComplexUserWhereUniqueInput
data: Prisma.XOR<Prisma.ComplexUserUpdateWithoutComplexInput, Prisma.ComplexUserUncheckedUpdateWithoutComplexInput>
}
export type ComplexUserUpdateManyWithWhereWithoutComplexInput = {
where: Prisma.ComplexUserScalarWhereInput
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutComplexInput>
} }
export type ComplexUserCreateManyUserInput = { export type ComplexUserCreateManyUserInput = {
id?: string
complexId: string complexId: string
role: $Enums.ComplexUserRole role?: $Enums.ComplexUserRole
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string
} }
export type ComplexUserUpdateWithoutUserInput = { export type ComplexUserUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput
} }
export type ComplexUserUncheckedUpdateWithoutUserInput = { export type ComplexUserUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string complexId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserUncheckedUpdateManyWithoutUserInput = { export type ComplexUserUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string complexId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string }
export type ComplexUserCreateManyComplexInput = {
userId: string
role?: $Enums.ComplexUserRole
createdAt?: Date | string
}
export type ComplexUserUpdateWithoutComplexInput = {
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
user?: Prisma.UserUpdateOneRequiredWithoutComplexesNestedInput
}
export type ComplexUserUncheckedUpdateWithoutComplexInput = {
userId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexUserUncheckedUpdateManyWithoutComplexInput = {
userId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type ComplexUserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ComplexUserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
complexId?: boolean complexId?: boolean
userId?: boolean userId?: boolean
role?: boolean role?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complexUser"]> }, ExtArgs["result"]["complexUser"]>
export type ComplexUserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ComplexUserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
complexId?: boolean complexId?: boolean
userId?: boolean userId?: boolean
role?: boolean role?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complexUser"]> }, ExtArgs["result"]["complexUser"]>
export type ComplexUserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ComplexUserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
complexId?: boolean complexId?: boolean
userId?: boolean userId?: boolean
role?: boolean role?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complexUser"]> }, ExtArgs["result"]["complexUser"]>
export type ComplexUserSelectScalar = { export type ComplexUserSelectScalar = {
id?: boolean
complexId?: boolean complexId?: boolean
userId?: boolean userId?: boolean
role?: boolean role?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean
} }
export type ComplexUserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "userId" | "role" | "createdAt" | "updatedAt", ExtArgs["result"]["complexUser"]> export type ComplexUserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"complexId" | "userId" | "role" | "createdAt", ExtArgs["result"]["complexUser"]>
export type ComplexUserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type ComplexUserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
@@ -660,12 +582,10 @@ export type $ComplexUserPayload<ExtArgs extends runtime.Types.Extensions.Interna
user: Prisma.$UserPayload<ExtArgs> user: Prisma.$UserPayload<ExtArgs>
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
complexId: string complexId: string
userId: string userId: string
role: $Enums.ComplexUserRole role: $Enums.ComplexUserRole
createdAt: Date createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["complexUser"]> }, ExtArgs["result"]["complexUser"]>
composites: {} composites: {}
} }
@@ -749,8 +669,8 @@ export interface ComplexUserDelegate<ExtArgs extends runtime.Types.Extensions.In
* // Get first 10 ComplexUsers * // Get first 10 ComplexUsers
* const complexUsers = await prisma.complexUser.findMany({ take: 10 }) * const complexUsers = await prisma.complexUser.findMany({ take: 10 })
* *
* // Only select the `id` * // Only select the `complexId`
* const complexUserWithIdOnly = await prisma.complexUser.findMany({ select: { id: true } }) * const complexUserWithComplexIdOnly = await prisma.complexUser.findMany({ select: { complexId: true } })
* *
*/ */
findMany<T extends ComplexUserFindManyArgs>(args?: Prisma.SelectSubset<T, ComplexUserFindManyArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions>> findMany<T extends ComplexUserFindManyArgs>(args?: Prisma.SelectSubset<T, ComplexUserFindManyArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions>>
@@ -794,9 +714,9 @@ export interface ComplexUserDelegate<ExtArgs extends runtime.Types.Extensions.In
* ] * ]
* }) * })
* *
* // Create many ComplexUsers and only return the `id` * // Create many ComplexUsers and only return the `complexId`
* const complexUserWithIdOnly = await prisma.complexUser.createManyAndReturn({ * const complexUserWithComplexIdOnly = await prisma.complexUser.createManyAndReturn({
* select: { id: true }, * select: { complexId: true },
* data: [ * data: [
* // ... provide data here * // ... provide data here
* ] * ]
@@ -885,9 +805,9 @@ export interface ComplexUserDelegate<ExtArgs extends runtime.Types.Extensions.In
* ] * ]
* }) * })
* *
* // Update zero or more ComplexUsers and only return the `id` * // Update zero or more ComplexUsers and only return the `complexId`
* const complexUserWithIdOnly = await prisma.complexUser.updateManyAndReturn({ * const complexUserWithComplexIdOnly = await prisma.complexUser.updateManyAndReturn({
* select: { id: true }, * select: { complexId: true },
* where: { * where: {
* // ... provide filter here * // ... provide filter here
* }, * },
@@ -1091,12 +1011,10 @@ export interface Prisma__ComplexUserClient<T, Null = never, ExtArgs extends runt
* Fields of the ComplexUser model * Fields of the ComplexUser model
*/ */
export interface ComplexUserFieldRefs { export interface ComplexUserFieldRefs {
readonly id: Prisma.FieldRef<"ComplexUser", 'String'>
readonly complexId: Prisma.FieldRef<"ComplexUser", 'String'> readonly complexId: Prisma.FieldRef<"ComplexUser", 'String'>
readonly userId: Prisma.FieldRef<"ComplexUser", 'String'> readonly userId: Prisma.FieldRef<"ComplexUser", 'String'>
readonly role: Prisma.FieldRef<"ComplexUser", 'ComplexUserRole'> readonly role: Prisma.FieldRef<"ComplexUser", 'ComplexUserRole'>
readonly createdAt: Prisma.FieldRef<"ComplexUser", 'DateTime'> readonly createdAt: Prisma.FieldRef<"ComplexUser", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"ComplexUser", 'DateTime'>
} }

View File

@@ -553,6 +553,14 @@ export type IntFieldUpdateOperationsInput = {
divide?: number divide?: number
} }
export type DecimalFieldUpdateOperationsInput = {
set?: runtime.Decimal | runtime.DecimalJsLike | number | string
increment?: runtime.Decimal | runtime.DecimalJsLike | number | string
decrement?: runtime.Decimal | runtime.DecimalJsLike | number | string
multiply?: runtime.Decimal | runtime.DecimalJsLike | number | string
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
}
export type CourtCreateNestedOneWithoutAvailabilitiesInput = { export type CourtCreateNestedOneWithoutAvailabilitiesInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput> create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput

View File

@@ -509,10 +509,6 @@ export type OnboardingRequestSumOrderByAggregateInput = {
otpResendCount?: Prisma.SortOrder otpResendCount?: Prisma.SortOrder
} }
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type OnboardingRequestSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type OnboardingRequestSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{

View File

@@ -320,6 +320,11 @@ export type PlanUncheckedUpdateManyInput = {
lastUpdatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastUpdatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type PlanNullableScalarRelationFilter = {
is?: Prisma.PlanWhereInput | null
isNot?: Prisma.PlanWhereInput | null
}
export type PlanCountOrderByAggregateInput = { export type PlanCountOrderByAggregateInput = {
code?: Prisma.SortOrder code?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
@@ -350,37 +355,18 @@ export type PlanSumOrderByAggregateInput = {
price?: Prisma.SortOrder price?: Prisma.SortOrder
} }
export type PlanScalarRelationFilter = {
is?: Prisma.PlanWhereInput
isNot?: Prisma.PlanWhereInput
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type DecimalFieldUpdateOperationsInput = {
set?: runtime.Decimal | runtime.DecimalJsLike | number | string
increment?: runtime.Decimal | runtime.DecimalJsLike | number | string
decrement?: runtime.Decimal | runtime.DecimalJsLike | number | string
multiply?: runtime.Decimal | runtime.DecimalJsLike | number | string
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
}
export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type PlanCreateNestedOneWithoutComplexesInput = { export type PlanCreateNestedOneWithoutComplexesInput = {
create?: Prisma.XOR<Prisma.PlanCreateWithoutComplexesInput, Prisma.PlanUncheckedCreateWithoutComplexesInput> create?: Prisma.XOR<Prisma.PlanCreateWithoutComplexesInput, Prisma.PlanUncheckedCreateWithoutComplexesInput>
connectOrCreate?: Prisma.PlanCreateOrConnectWithoutComplexesInput connectOrCreate?: Prisma.PlanCreateOrConnectWithoutComplexesInput
connect?: Prisma.PlanWhereUniqueInput connect?: Prisma.PlanWhereUniqueInput
} }
export type PlanUpdateOneRequiredWithoutComplexesNestedInput = { export type PlanUpdateOneWithoutComplexesNestedInput = {
create?: Prisma.XOR<Prisma.PlanCreateWithoutComplexesInput, Prisma.PlanUncheckedCreateWithoutComplexesInput> create?: Prisma.XOR<Prisma.PlanCreateWithoutComplexesInput, Prisma.PlanUncheckedCreateWithoutComplexesInput>
connectOrCreate?: Prisma.PlanCreateOrConnectWithoutComplexesInput connectOrCreate?: Prisma.PlanCreateOrConnectWithoutComplexesInput
upsert?: Prisma.PlanUpsertWithoutComplexesInput upsert?: Prisma.PlanUpsertWithoutComplexesInput
disconnect?: Prisma.PlanWhereInput | boolean
delete?: Prisma.PlanWhereInput | boolean
connect?: Prisma.PlanWhereUniqueInput connect?: Prisma.PlanWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.PlanUpdateToOneWithWhereWithoutComplexesInput, Prisma.PlanUpdateWithoutComplexesInput>, Prisma.PlanUncheckedUpdateWithoutComplexesInput> update?: Prisma.XOR<Prisma.XOR<Prisma.PlanUpdateToOneWithWhereWithoutComplexesInput, Prisma.PlanUpdateWithoutComplexesInput>, Prisma.PlanUncheckedUpdateWithoutComplexesInput>
} }

View File

@@ -26,76 +26,76 @@ export type AggregateUser = {
export type UserMinAggregateOutputType = { export type UserMinAggregateOutputType = {
id: string | null id: string | null
supabaseUserId: string | null
name: string | null name: string | null
email: string | null email: string | null
emailVerified: boolean | null emailVerified: boolean | null
image: string | null image: string | null
fullName: string | null phone: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
role: string | null
} }
export type UserMaxAggregateOutputType = { export type UserMaxAggregateOutputType = {
id: string | null id: string | null
supabaseUserId: string | null
name: string | null name: string | null
email: string | null email: string | null
emailVerified: boolean | null emailVerified: boolean | null
image: string | null image: string | null
fullName: string | null phone: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
role: string | null
} }
export type UserCountAggregateOutputType = { export type UserCountAggregateOutputType = {
id: number id: number
supabaseUserId: number
name: number name: number
email: number email: number
emailVerified: number emailVerified: number
image: number image: number
fullName: number phone: number
createdAt: number createdAt: number
updatedAt: number updatedAt: number
role: number
_all: number _all: number
} }
export type UserMinAggregateInputType = { export type UserMinAggregateInputType = {
id?: true id?: true
supabaseUserId?: true
name?: true name?: true
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
fullName?: true phone?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true
} }
export type UserMaxAggregateInputType = { export type UserMaxAggregateInputType = {
id?: true id?: true
supabaseUserId?: true
name?: true name?: true
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
fullName?: true phone?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true
} }
export type UserCountAggregateInputType = { export type UserCountAggregateInputType = {
id?: true id?: true
supabaseUserId?: true
name?: true name?: true
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
fullName?: true phone?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true
_all?: true _all?: true
} }
@@ -173,14 +173,14 @@ export type UserGroupByArgs<ExtArgs extends runtime.Types.Extensions.InternalArg
export type UserGroupByOutputType = { export type UserGroupByOutputType = {
id: string id: string
supabaseUserId: string | null name: string
name: string | null
email: string email: string
emailVerified: boolean emailVerified: boolean
image: string | null image: string | null
fullName: string | null phone: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
role: string
_count: UserCountAggregateOutputType | null _count: UserCountAggregateOutputType | null
_min: UserMinAggregateOutputType | null _min: UserMinAggregateOutputType | null
_max: UserMaxAggregateOutputType | null _max: UserMaxAggregateOutputType | null
@@ -206,62 +206,62 @@ export type UserWhereInput = {
OR?: Prisma.UserWhereInput[] OR?: Prisma.UserWhereInput[]
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
id?: Prisma.StringFilter<"User"> | string id?: Prisma.StringFilter<"User"> | string
supabaseUserId?: Prisma.UuidNullableFilter<"User"> | string | null name?: Prisma.StringFilter<"User"> | string
name?: Prisma.StringNullableFilter<"User"> | string | null
email?: Prisma.StringFilter<"User"> | string email?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.BoolFilter<"User"> | boolean emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
fullName?: Prisma.StringNullableFilter<"User"> | string | null phone?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
accounts?: Prisma.AccountListRelationFilter role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter complexes?: Prisma.ComplexUserListRelationFilter
} }
export type UserOrderByWithRelationInput = { export type UserOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrderInput | Prisma.SortOrder name?: Prisma.SortOrder
name?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
fullName?: Prisma.SortOrderInput | Prisma.SortOrder phone?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
accounts?: Prisma.AccountOrderByRelationAggregateInput role?: Prisma.SortOrder
sessions?: Prisma.SessionOrderByRelationAggregateInput sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
complexes?: Prisma.ComplexUserOrderByRelationAggregateInput complexes?: Prisma.ComplexUserOrderByRelationAggregateInput
} }
export type UserWhereUniqueInput = Prisma.AtLeast<{ export type UserWhereUniqueInput = Prisma.AtLeast<{
id?: string id?: string
supabaseUserId?: string
email?: string email?: string
AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
OR?: Prisma.UserWhereInput[] OR?: Prisma.UserWhereInput[]
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
name?: Prisma.StringNullableFilter<"User"> | string | null name?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.BoolFilter<"User"> | boolean emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
fullName?: Prisma.StringNullableFilter<"User"> | string | null phone?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
accounts?: Prisma.AccountListRelationFilter role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter complexes?: Prisma.ComplexUserListRelationFilter
}, "id" | "supabaseUserId" | "email"> }, "id" | "email">
export type UserOrderByWithAggregationInput = { export type UserOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrderInput | Prisma.SortOrder name?: Prisma.SortOrder
name?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
fullName?: Prisma.SortOrderInput | Prisma.SortOrder phone?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
_count?: Prisma.UserCountOrderByAggregateInput _count?: Prisma.UserCountOrderByAggregateInput
_max?: Prisma.UserMaxOrderByAggregateInput _max?: Prisma.UserMaxOrderByAggregateInput
_min?: Prisma.UserMinOrderByAggregateInput _min?: Prisma.UserMinOrderByAggregateInput
@@ -272,110 +272,146 @@ export type UserScalarWhereWithAggregatesInput = {
OR?: Prisma.UserScalarWhereWithAggregatesInput[] OR?: Prisma.UserScalarWhereWithAggregatesInput[]
NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"User"> | string id?: Prisma.StringWithAggregatesFilter<"User"> | string
supabaseUserId?: Prisma.UuidNullableWithAggregatesFilter<"User"> | string | null name?: Prisma.StringWithAggregatesFilter<"User"> | string
name?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
email?: Prisma.StringWithAggregatesFilter<"User"> | string email?: Prisma.StringWithAggregatesFilter<"User"> | string
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
fullName?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
role?: Prisma.StringWithAggregatesFilter<"User"> | string
} }
export type UserCreateInput = { export type UserCreateInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
} }
export type UserUncheckedCreateInput = { export type UserUncheckedCreateInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
} }
export type UserUpdateInput = { export type UserUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
} }
export type UserUncheckedUpdateInput = { export type UserUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
} }
export type UserCreateManyInput = { export type UserCreateManyInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string
} }
export type UserUpdateManyMutationInput = { export type UserUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
} }
export type UserUncheckedUpdateManyInput = { export type UserUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
}
export type UserCountOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
}
export type UserMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
}
export type UserMinOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
} }
export type UserScalarRelationFilter = { export type UserScalarRelationFilter = {
@@ -383,54 +419,20 @@ export type UserScalarRelationFilter = {
isNot?: Prisma.UserWhereInput isNot?: Prisma.UserWhereInput
} }
export type UserCountOrderByAggregateInput = { export type StringFieldUpdateOperationsInput = {
id?: Prisma.SortOrder set?: string
supabaseUserId?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
} }
export type UserMaxOrderByAggregateInput = { export type BoolFieldUpdateOperationsInput = {
id?: Prisma.SortOrder set?: boolean
supabaseUserId?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
} }
export type UserMinOrderByAggregateInput = { export type NullableStringFieldUpdateOperationsInput = {
id?: Prisma.SortOrder set?: string | null
supabaseUserId?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
} }
export type UserCreateNestedOneWithoutComplexesInput = { export type DateTimeFieldUpdateOperationsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput> set?: Date | string
connectOrCreate?: Prisma.UserCreateOrConnectWithoutComplexesInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutComplexesNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutComplexesInput
upsert?: Prisma.UserUpsertWithoutComplexesInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutComplexesInput, Prisma.UserUpdateWithoutComplexesInput>, Prisma.UserUncheckedUpdateWithoutComplexesInput>
} }
export type UserCreateNestedOneWithoutSessionsInput = { export type UserCreateNestedOneWithoutSessionsInput = {
@@ -461,102 +463,44 @@ export type UserUpdateOneRequiredWithoutAccountsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAccountsInput, Prisma.UserUpdateWithoutAccountsInput>, Prisma.UserUncheckedUpdateWithoutAccountsInput> update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAccountsInput, Prisma.UserUpdateWithoutAccountsInput>, Prisma.UserUncheckedUpdateWithoutAccountsInput>
} }
export type UserCreateWithoutComplexesInput = { export type UserCreateNestedOneWithoutComplexesInput = {
id?: string create?: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
supabaseUserId?: string | null connectOrCreate?: Prisma.UserCreateOrConnectWithoutComplexesInput
name?: string | null connect?: Prisma.UserWhereUniqueInput
email: string
emailVerified?: boolean
image?: string | null
fullName?: string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
} }
export type UserUncheckedCreateWithoutComplexesInput = { export type UserUpdateOneRequiredWithoutComplexesNestedInput = {
id?: string create?: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
supabaseUserId?: string | null connectOrCreate?: Prisma.UserCreateOrConnectWithoutComplexesInput
name?: string | null upsert?: Prisma.UserUpsertWithoutComplexesInput
email: string connect?: Prisma.UserWhereUniqueInput
emailVerified?: boolean update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutComplexesInput, Prisma.UserUpdateWithoutComplexesInput>, Prisma.UserUncheckedUpdateWithoutComplexesInput>
image?: string | null
fullName?: string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutComplexesInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
}
export type UserUpsertWithoutComplexesInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutComplexesInput, Prisma.UserUncheckedUpdateWithoutComplexesInput>
create: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutComplexesInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutComplexesInput, Prisma.UserUncheckedUpdateWithoutComplexesInput>
}
export type UserUpdateWithoutComplexesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutComplexesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
} }
export type UserCreateWithoutSessionsInput = { export type UserCreateWithoutSessionsInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
} }
export type UserUncheckedCreateWithoutSessionsInput = { export type UserUncheckedCreateWithoutSessionsInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
} }
@@ -579,56 +523,56 @@ export type UserUpdateToOneWithWhereWithoutSessionsInput = {
export type UserUpdateWithoutSessionsInput = { export type UserUpdateWithoutSessionsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
} }
export type UserUncheckedUpdateWithoutSessionsInput = { export type UserUncheckedUpdateWithoutSessionsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
} }
export type UserCreateWithoutAccountsInput = { export type UserCreateWithoutAccountsInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
} }
export type UserUncheckedCreateWithoutAccountsInput = { export type UserUncheckedCreateWithoutAccountsInput = {
id?: string id: string
supabaseUserId?: string | null name: string
name?: string | null
email: string email: string
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
fullName?: string | null phone?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
} }
@@ -651,46 +595,118 @@ export type UserUpdateToOneWithWhereWithoutAccountsInput = {
export type UserUpdateWithoutAccountsInput = { export type UserUpdateWithoutAccountsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
} }
export type UserUncheckedUpdateWithoutAccountsInput = { export type UserUncheckedUpdateWithoutAccountsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null name?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
fullName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
} }
export type UserCreateWithoutComplexesInput = {
id: string
name: string
email: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutComplexesInput = {
id: string
name: string
email: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutComplexesInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
}
export type UserUpsertWithoutComplexesInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutComplexesInput, Prisma.UserUncheckedUpdateWithoutComplexesInput>
create: Prisma.XOR<Prisma.UserCreateWithoutComplexesInput, Prisma.UserUncheckedCreateWithoutComplexesInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutComplexesInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutComplexesInput, Prisma.UserUncheckedUpdateWithoutComplexesInput>
}
export type UserUpdateWithoutComplexesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutComplexesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
}
/** /**
* Count Type UserCountOutputType * Count Type UserCountOutputType
*/ */
export type UserCountOutputType = { export type UserCountOutputType = {
accounts: number
sessions: number sessions: number
accounts: number
complexes: number complexes: number
} }
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
sessions?: boolean | UserCountOutputTypeCountSessionsArgs sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
complexes?: boolean | UserCountOutputTypeCountComplexesArgs complexes?: boolean | UserCountOutputTypeCountComplexesArgs
} }
@@ -707,15 +723,15 @@ export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensi
/** /**
* UserCountOutputType without action * UserCountOutputType without action
*/ */
export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserCountOutputTypeCountSessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.AccountWhereInput where?: Prisma.SessionWhereInput
} }
/** /**
* UserCountOutputType without action * UserCountOutputType without action
*/ */
export type UserCountOutputTypeCountSessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.SessionWhereInput where?: Prisma.AccountWhereInput
} }
/** /**
@@ -728,60 +744,60 @@ export type UserCountOutputTypeCountComplexesArgs<ExtArgs extends runtime.Types.
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
supabaseUserId?: boolean
name?: boolean name?: boolean
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
fullName?: boolean phone?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs> role?: boolean
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
complexes?: boolean | Prisma.User$complexesArgs<ExtArgs> complexes?: boolean | Prisma.User$complexesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]> }, ExtArgs["result"]["user"]>
export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
supabaseUserId?: boolean
name?: boolean name?: boolean
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
fullName?: boolean phone?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]> }, ExtArgs["result"]["user"]>
export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
supabaseUserId?: boolean
name?: boolean name?: boolean
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
fullName?: boolean phone?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]> }, ExtArgs["result"]["user"]>
export type UserSelectScalar = { export type UserSelectScalar = {
id?: boolean id?: boolean
supabaseUserId?: boolean
name?: boolean name?: boolean
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
fullName?: boolean phone?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean
} }
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "supabaseUserId" | "name" | "email" | "emailVerified" | "image" | "fullName" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]> export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
complexes?: boolean | Prisma.User$complexesArgs<ExtArgs> complexes?: boolean | Prisma.User$complexesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
} }
@@ -791,20 +807,20 @@ export type UserIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "User" name: "User"
objects: { objects: {
accounts: Prisma.$AccountPayload<ExtArgs>[]
sessions: Prisma.$SessionPayload<ExtArgs>[] sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[]
complexes: Prisma.$ComplexUserPayload<ExtArgs>[] complexes: Prisma.$ComplexUserPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
supabaseUserId: string | null name: string
name: string | null
email: string email: string
emailVerified: boolean emailVerified: boolean
image: string | null image: string | null
fullName: string | null phone: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
role: string
}, ExtArgs["result"]["user"]> }, ExtArgs["result"]["user"]>
composites: {} composites: {}
} }
@@ -1199,8 +1215,8 @@ readonly fields: UserFieldRefs;
*/ */
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> { export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise" readonly [Symbol.toStringTag]: "PrismaPromise"
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
complexes<T extends Prisma.User$complexesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$complexesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> complexes<T extends Prisma.User$complexesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$complexesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/** /**
* Attaches callbacks for the resolution and/or rejection of the Promise. * Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -1232,14 +1248,14 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
*/ */
export interface UserFieldRefs { export interface UserFieldRefs {
readonly id: Prisma.FieldRef<"User", 'String'> readonly id: Prisma.FieldRef<"User", 'String'>
readonly supabaseUserId: Prisma.FieldRef<"User", 'String'>
readonly name: Prisma.FieldRef<"User", 'String'> readonly name: Prisma.FieldRef<"User", 'String'>
readonly email: Prisma.FieldRef<"User", 'String'> readonly email: Prisma.FieldRef<"User", 'String'>
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'> readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
readonly image: Prisma.FieldRef<"User", 'String'> readonly image: Prisma.FieldRef<"User", 'String'>
readonly fullName: Prisma.FieldRef<"User", 'String'> readonly phone: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'> readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly role: Prisma.FieldRef<"User", 'String'>
} }
@@ -1632,30 +1648,6 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number limit?: number
} }
/**
* User.accounts
*/
export type User$accountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Account
*/
select?: Prisma.AccountSelect<ExtArgs> | null
/**
* Omit specific fields from the Account
*/
omit?: Prisma.AccountOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.AccountInclude<ExtArgs> | null
where?: Prisma.AccountWhereInput
orderBy?: Prisma.AccountOrderByWithRelationInput | Prisma.AccountOrderByWithRelationInput[]
cursor?: Prisma.AccountWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
}
/** /**
* User.sessions * User.sessions
*/ */
@@ -1680,6 +1672,30 @@ export type User$sessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalA
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[] distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
} }
/**
* User.accounts
*/
export type User$accountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Account
*/
select?: Prisma.AccountSelect<ExtArgs> | null
/**
* Omit specific fields from the Account
*/
omit?: Prisma.AccountOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.AccountInclude<ExtArgs> | null
where?: Prisma.AccountWhereInput
orderBy?: Prisma.AccountOrderByWithRelationInput | Prisma.AccountOrderByWithRelationInput[]
cursor?: Prisma.AccountWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
}
/** /**
* User.complexes * User.complexes
*/ */

View File

@@ -1,9 +1,7 @@
import { createApp } from '@/app'; import { createApp } from '@/app';
import { registerAuthRoutes } from '@/auth-routes';
import { registerRoutes } from '@/register-routes'; import { registerRoutes } from '@/register-routes';
const app = createApp(); const app = createApp();
registerAuthRoutes(app);
registerRoutes(app); registerRoutes(app);
export type AppType = typeof app; export type AppType = typeof app;

View File

@@ -1,55 +1,55 @@
import { sendMail } from '@/lib/mailer'; import { dash } from '@better-auth/infra';
import { db } from '@/lib/prisma';
import bcrypt from 'bcrypt';
import { betterAuth } from 'better-auth'; import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma'; import { prismaAdapter } from 'better-auth/adapters/prisma';
import { emailOTP } from 'better-auth/plugins'; import { openAPI } from 'better-auth/plugins';
import { sendMail } from './mailer';
import { db } from './prisma';
export const auth = betterAuth({ export const auth = betterAuth({
database: prismaAdapter(db, { database: prismaAdapter(db, {
provider: 'postgresql', provider: 'postgresql',
}), }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
session: {
cookieCache: {
enabled: false,
},
},
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
password: { },
hash: async (password) => { user: {
return bcrypt.hash(password, 10); additionalFields: {
}, phone: {
verify: async (password, hash) => { type: 'string',
return bcrypt.compare(password, hash); required: false,
}, },
}, },
}, },
plugins: [ emailVerification: {
emailOTP({ sendOnSignUp: true,
async sendVerificationOTP({ email, otp, type }) { autoSignInAfterVerification: true,
let subject = 'Tu código de verificación'; sendVerificationEmail: async ({ user, url }) => {
let text = `Tu código OTP es ${otp}.`; await sendMail({
let html = `<p>Tu código OTP es <strong>${otp}</strong>.</p>`; to: user.email,
subject: 'Verificá tu email en Playzer',
if (type === 'sign-in') { html: `Hacé click para verificar tu email: <a href="${url}">${url}</a>`,
subject = 'Código de inicio de sesión'; text: `Hacé click para verificar tu email: ${url}`,
text = `Tu código para iniciar sesión es ${otp}.`; });
html = `<p>Tu código para iniciar sesión es <strong>${otp}</strong>.</p>`; },
} else if (type === 'forget-password') { },
subject = 'Código para restablecer contraseña'; socialProviders: {
text = `Tu código para restablecer tu contraseña es ${otp}.`; google: {
html = `<p>Tu código para restablecer tu contraseña es <strong>${otp}</strong>.</p>`; clientId: process.env.GOOGLE_CLIENT_ID!,
} else if (type === 'email-verification') { clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
subject = 'Verifica tu correo electrónico'; },
text = `Tu código de verificación es ${otp}.`; },
html = `<p>Tu código de verificación es <strong>${otp}</strong>.</p>`; trustedOrigins: [
} process.env.APP_BASE_URL ?? 'http://localhost:5173',
process.env.BETTER_AUTH_URL ?? 'http://localhost:3000',
await sendMail({
to: email,
subject,
text,
html,
});
},
}),
], ],
plugins: [openAPI(), dash()],
}); });
export type Session = typeof auth.$Infer.Session.session; export type Session = typeof auth.$Infer.Session.session;

View File

@@ -0,0 +1,17 @@
import { createHash } from 'node:crypto';
function getGravatarUrl(email: string): string {
const hash = createHash('md5').update(email.trim().toLowerCase()).digest('hex');
return `https://www.gravatar.com/avatar/${hash}?d=identicon`;
}
export function resolveAvatarUrl(input: {
email: string;
image?: string | null;
}): string {
if (input.image) {
return input.image;
}
return getGravatarUrl(input.email);
}

View File

@@ -0,0 +1,58 @@
// lib/errors.ts
export type ValidationIssue = {
path: string;
message: string;
};
export type AppError =
| {
type: 'validation';
message: string;
issues?: ValidationIssue[];
}
| {
type: 'not_found';
message: string;
}
| {
type: 'conflict';
message: string;
}
| {
type: 'unauthorized';
message: string;
}
| {
type: 'forbidden';
message: string;
}
| {
type: 'unexpected';
message: string;
};
export const Errors = {
validation(message: string, issues?: ValidationIssue[]): AppError {
return { type: 'validation', message, issues };
},
notFound(message = 'Resource not found'): AppError {
return { type: 'not_found', message };
},
conflict(message: string): AppError {
return { type: 'conflict', message };
},
unauthorized(message = 'Unauthorized'): AppError {
return { type: 'unauthorized', message };
},
forbidden(message = 'Forbidden'): AppError {
return { type: 'forbidden', message };
},
unexpected(message = 'Unexpected error'): AppError {
return { type: 'unexpected', message };
},
};

View File

@@ -0,0 +1,20 @@
// http/error-handler.ts
import type { MiddlewareHandler } from 'hono';
import { logger } from '../logger';
import { unexpectedProblem } from './problem-builders';
export const errorHandler: MiddlewareHandler = async (c, next) => {
try {
await next();
} catch (error) {
const requestId = c.get('requestId') as string | undefined;
const instance = requestId ? `/requests/${requestId}` : undefined;
logger.error({
requestId,
error,
});
return c.json(unexpectedProblem({ instance }), 500);
}
};

View File

@@ -0,0 +1,20 @@
import type { Result } from '@/lib/result';
import type { Context } from 'hono';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
import { mapAppErrorToProblem } from './problem-mapper';
export function handleResult<T>(
c: Context,
result: Result<T>,
successStatus: ContentfulStatusCode = 200
) {
if (!result.ok) {
const requestId = c.get('requestId') as string | undefined;
const instance = requestId ? `/requests/${requestId}` : undefined;
const problem = mapAppErrorToProblem(result.error, instance);
return c.json(problem, problem.status);
}
return c.json(result.value, successStatus);
}

View File

@@ -0,0 +1,29 @@
// http/problem-builders.ts
import type { ProblemDetails } from './problem-details';
export function validationProblem(params: {
detail?: string;
instance?: string;
errors?: Record<string, string[]>;
}): ProblemDetails {
return {
type: 'https://api.myapp.dev/problems/validation',
title: 'Bad Request',
status: 400,
detail: params.detail ?? 'Invalid request data',
instance: params.instance,
errors: params.errors,
};
}
export function unexpectedProblem(params?: {
instance?: string;
}): ProblemDetails {
return {
type: 'https://api.myapp.dev/problems/unexpected',
title: 'Internal Server Error',
status: 500,
detail: 'Internal server error',
instance: params?.instance,
};
}

View File

@@ -0,0 +1,10 @@
import type { ContentfulStatusCode } from 'hono/utils/http-status';
export type ProblemDetails = {
type: string;
title: string;
status: ContentfulStatusCode;
detail?: string;
instance?: string;
errors?: Record<string, string[]>;
};

View File

@@ -0,0 +1,51 @@
import type { AppError } from '@/lib/errors';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
import type { ProblemDetails } from './problem-details';
function statusFromError(error: AppError): ContentfulStatusCode {
switch (error.type) {
case 'validation':
return 400;
case 'unauthorized':
return 401;
case 'forbidden':
return 403;
case 'not_found':
return 404;
case 'conflict':
return 409;
case 'unexpected':
return 500;
default:
return 500;
}
}
function titleFromStatus(status: ContentfulStatusCode): string {
switch (status) {
case 400:
return 'Bad Request';
case 401:
return 'Unauthorized';
case 403:
return 'Forbidden';
case 404:
return 'Not Found';
case 409:
return 'Conflict';
default:
return 'Internal Server Error';
}
}
export function mapAppErrorToProblem(error: AppError, instance?: string): ProblemDetails {
const status = statusFromError(error);
return {
type: `https://api.myapp.dev/problems/${error.type}`,
title: titleFromStatus(status),
status,
detail: status === 500 ? 'Internal server error' : error.message,
instance,
};
}

View File

@@ -0,0 +1,16 @@
import type { MiddlewareHandler } from 'hono';
type Env = {
Variables: {
requestId: string;
};
};
export const requestIdMiddleware: MiddlewareHandler<Env> = async (c, next) => {
const requestId = crypto.randomUUID();
c.set('requestId', requestId);
c.header('X-Request-Id', requestId);
await next();
};

View File

@@ -0,0 +1,48 @@
import { zValidator } from '@hono/zod-validator';
import type { ZodSchema } from 'zod';
import { validationProblem } from './problem-builders';
import { zodIssuesToRecord } from './zod-issues';
type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'form';
function makeValidator<TSchema extends ZodSchema>(target: ValidationTarget, schema: TSchema) {
return zValidator(target, schema, (result, c) => {
if (result.success) {
return;
}
const requestId = c.get('requestId') as string | undefined;
const instance = requestId ? `/requests/${requestId}` : undefined;
return c.json(
validationProblem({
detail: `Invalid ${target} data`,
instance,
errors: zodIssuesToRecord(result.error.issues),
}),
400
);
});
}
export const validate = {
json<TSchema extends ZodSchema>(schema: TSchema) {
return makeValidator('json', schema);
},
query<TSchema extends ZodSchema>(schema: TSchema) {
return makeValidator('query', schema);
},
param<TSchema extends ZodSchema>(schema: TSchema) {
return makeValidator('param', schema);
},
header<TSchema extends ZodSchema>(schema: TSchema) {
return makeValidator('header', schema);
},
form<TSchema extends ZodSchema>(schema: TSchema) {
return makeValidator('form', schema);
},
};

View File

@@ -0,0 +1,16 @@
type IssueLike = {
path: PropertyKey[];
message: string;
};
export function zodIssuesToRecord(issues: IssueLike[]): Record<string, string[]> {
const out: Record<string, string[]> = {};
for (const issue of issues) {
const key = issue.path.length ? issue.path.join('.') : 'root';
out[key] ??= [];
out[key].push(issue.message);
}
return out;
}

View File

@@ -1,29 +1,26 @@
import { MailtrapClientConfig, MailtrapTransport } from 'mailtrap';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
const TOKEN = Bun.env.MAILTRAP_API_TOKEN;
let cachedTransporter: ReturnType<typeof nodemailer.createTransport> | null = null; let cachedTransporter: ReturnType<typeof nodemailer.createTransport> | null = null;
function getTransporter() { function getTransporter() {
if (cachedTransporter) return cachedTransporter; if (cachedTransporter) return cachedTransporter;
const smtpHost = Bun.env.SMTP_HOST; if (!TOKEN) {
const smtpPort = Number(Bun.env.SMTP_PORT ?? 587); throw new Error('Missing MAILTRAP_API_TOKEN env var.');
const smtpUser = Bun.env.SMTP_USER;
const smtpPass = Bun.env.SMTP_PASS;
if (!smtpHost || !smtpUser || !smtpPass) {
throw new Error('Missing SMTP env vars. Set SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASS.');
} }
cachedTransporter = nodemailer.createTransport({ const config: MailtrapClientConfig = {
host: smtpHost, token: TOKEN,
port: smtpPort, sandbox: Bun.env.MAIL_SANDBOX === 'true',
secure: smtpPort === 465, testInboxId: Bun.env.MAIL_INBOX_ID ? Number(Bun.env.MAIL_INBOX_ID) : undefined,
auth: { };
user: smtpUser,
pass: smtpPass,
},
});
console.log(JSON.stringify(config, null, 2));
cachedTransporter = nodemailer.createTransport(MailtrapTransport(config));
return cachedTransporter; return cachedTransporter;
} }

View File

@@ -0,0 +1,12 @@
// lib/result.ts
import type { AppError } from '@/lib/errors';
export type Result<T, E = AppError> = { ok: true; value: T } | { ok: false; error: E };
export function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
export function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}

View File

@@ -0,0 +1,84 @@
import type { AppEnv } from '@/types/hono';
import { Hono } from 'hono';
type EventCallback = (data: string) => void;
class SSEManager {
private channels: Map<string, Set<EventCallback>> = new Map();
subscribe(channel: string, callback: EventCallback): () => void {
if (!this.channels.has(channel)) {
this.channels.set(channel, new Set());
}
this.channels.get(channel)!.add(callback);
return () => {
const subs = this.channels.get(channel);
if (subs) {
subs.delete(callback);
if (subs.size === 0) {
this.channels.delete(channel);
}
}
};
}
emit(channel: string, data: string): void {
const subs = this.channels.get(channel);
if (subs) {
for (const callback of subs) {
callback(data);
}
}
}
}
export const sseManager = new SSEManager();
export function registerEventsRoutes(app: Hono<AppEnv>) {
app.get('/api/events/:channel', async (c) => {
const channel = c.req.param('channel');
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const send = (data: string) => {
try {
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
} catch {
// Controller closed
}
};
send('connected');
const unsubscribe = sseManager.subscribe(channel, send);
const keepAliveInterval = setInterval(() => {
send('ping');
}, 30000);
c.req.raw.signal.addEventListener('abort', () => {
console.log(`[SSE] Connection closed for channel: ${channel}`);
clearInterval(keepAliveInterval);
unsubscribe();
try {
controller.close();
} catch {
// Already closed
}
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
});
}

View File

@@ -1,34 +1,18 @@
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { db } from '@/lib/prisma';
import type { AppEnv } from '@/types/hono'; import type { AppEnv } from '@/types/hono';
import type { MiddlewareHandler } from 'hono'; import { createMiddleware } from 'hono/factory';
export const requireAuth: MiddlewareHandler<AppEnv> = async (c, next) => { export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
const bearerToken = c.req.header('authorization')?.replace('Bearer ', ''); const session = await auth.api.getSession({
headers: c.req.raw.headers,
if (!bearerToken) {
return c.json({ message: 'Missing bearer token.' }, 403);
}
const { user, session } = await auth.api.getSession({
headers: { authorization: `Bearer ${bearerToken}` },
}); });
if (!user || !session) { if (!session) {
return c.json({ message: 'Invalid or expired token.' }, 403); return c.json({ message: 'Unauthorized' }, 401);
} }
const appUser = await db.user.findUnique({ c.set('user', session.user);
where: { email: user.email }, c.set('session', session.session);
select: { id: true },
});
if (!appUser) {
return c.json({ message: 'User not provisioned in backend.' }, 403);
}
c.set('appUserId', appUser.id);
c.set('session', session);
await next(); await next();
}; });

View File

@@ -11,9 +11,9 @@ function getSuperAdminEmails(): Set<string> {
} }
export const requireSuperAdmin: MiddlewareHandler<AppEnv> = async (c, next) => { export const requireSuperAdmin: MiddlewareHandler<AppEnv> = async (c, next) => {
const authUser = c.get('authUser'); const user = c.get('user');
const role = typeof authUser.app_metadata?.role === 'string' ? authUser.app_metadata.role : null; const role = (user as any).role || 'member';
const email = authUser.email?.toLowerCase(); const email = user.email.toLowerCase();
if (role === 'super_admin') { if (role === 'super_admin') {
await next(); await next();

View File

@@ -10,10 +10,10 @@ type ComplexIdParams = { complexId: string };
export async function createAdminBookingHandler(c: AppContext) { export async function createAdminBookingHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams; const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateAdminBookingInput; const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const booking = await createAdminBooking(appUserId, complexId, payload); const booking = await createAdminBooking(user.id, complexId, payload);
return c.json(booking, 201); return c.json(booking, 201);
} catch (error) { } catch (error) {
if (error instanceof AdminBookingServiceError) { if (error instanceof AdminBookingServiceError) {

View File

@@ -10,10 +10,10 @@ type ComplexIdParams = { complexId: string };
export async function listAdminBookingsHandler(c: AppContext) { export async function listAdminBookingsHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams; const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const query = c.req.valid('query' as never) as ListAdminBookingsQuery; const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const response = await listAdminBookings(appUserId, complexId, query); const response = await listAdminBookings(user.id, complexId, query);
return c.json(response); return c.json(response);
} catch (error) { } catch (error) {
if (error instanceof AdminBookingServiceError) { if (error instanceof AdminBookingServiceError) {

View File

@@ -10,10 +10,10 @@ type BookingIdParams = { id: string };
export async function updateAdminBookingStatusHandler(c: AppContext) { export async function updateAdminBookingStatusHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BookingIdParams; const { id } = c.req.valid('param' as never) as BookingIdParams;
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput; const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const booking = await updateAdminBookingStatus(appUserId, id, payload); const booking = await updateAdminBookingStatus(user.id, id, payload);
return c.json(booking); return c.json(booking);
} catch (error) { } catch (error) {
if (error instanceof AdminBookingServiceError) { if (error instanceof AdminBookingServiceError) {

View File

@@ -126,12 +126,12 @@ function buildSlots(
return slots; return slots;
} }
async function ensureComplexAccess(complexId: string, appUserId: string) { async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({ const complexUser = await db.complexUser.findUnique({
where: { where: {
complexId_userId: { complexId_userId: {
complexId, complexId,
userId: appUserId, userId,
}, },
}, },
include: { include: {
@@ -164,7 +164,7 @@ function mapBookingResponse(booking: {
endTime: string; endTime: string;
customerName: string; customerName: string;
customerPhone: string; customerPhone: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'; status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
court: { court: {
@@ -205,11 +205,11 @@ function mapBookingResponse(booking: {
} }
export async function listAdminBookings( export async function listAdminBookings(
appUserId: string, userId: string,
complexId: string, complexId: string,
query: ListAdminBookingsQuery query: ListAdminBookingsQuery
) { ) {
await ensureComplexAccess(complexId, appUserId); await ensureComplexAccess(complexId, userId);
const fromDate = parseIsoDate(query.fromDate); const fromDate = parseIsoDate(query.fromDate);
const bookings = await db.courtBooking.findMany({ const bookings = await db.courtBooking.findMany({
@@ -253,11 +253,11 @@ export async function listAdminBookings(
} }
export async function createAdminBooking( export async function createAdminBooking(
appUserId: string, userId: string,
complexId: string, complexId: string,
input: CreateAdminBookingInput input: CreateAdminBookingInput
) { ) {
const complex = await ensureComplexAccess(complexId, appUserId); const complex = await ensureComplexAccess(complexId, userId);
const bookingDate = parseIsoDate(input.date); const bookingDate = parseIsoDate(input.date);
const dayOfWeek = getDayOfWeek(bookingDate); const dayOfWeek = getDayOfWeek(bookingDate);
@@ -436,7 +436,7 @@ export async function createAdminBooking(
} }
export async function updateAdminBookingStatus( export async function updateAdminBookingStatus(
appUserId: string, userId: string,
bookingId: string, bookingId: string,
input: UpdateAdminBookingStatusInput input: UpdateAdminBookingStatusInput
) { ) {
@@ -447,7 +447,7 @@ export async function updateAdminBookingStatus(
complex: { complex: {
users: { users: {
some: { some: {
userId: appUserId, userId,
}, },
}, },
}, },
@@ -494,34 +494,42 @@ export async function updateAdminBookingStatus(
); );
} }
const updated = await db.courtBooking.update({ if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
where: { id: booking.id }, throw new AdminBookingServiceError(
'Solo se pueden marcar como no show las reservas confirmadas.',
409
);
}
await db.courtBookingLog.create({
data: { data: {
status: id: uuidv7(),
input.status === 'COMPLETED' ? CourtBookingStatus.COMPLETED : CourtBookingStatus.CANCELLED, bookingCode: booking.bookingCode,
}, courtId: booking.court.id,
include: { bookingDate: booking.bookingDate,
court: { startTime: booking.startTime,
select: { endTime: booking.endTime,
id: true, customerName: booking.customerName,
name: true, customerPhone: booking.customerPhone,
sport: { previousStatus: booking.status,
select: { newStatus: input.status,
id: true, changedAt: new Date(),
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
}, },
}); });
return mapBookingResponse(updated); if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
await db.courtBooking.update({
where: { id: booking.id },
data: {
status: input.status,
},
});
} else {
// if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
await db.courtBooking.delete({
where: { id: booking.id },
});
}
return mapBookingResponse({ ...booking, status: input.status });
} }

View File

@@ -0,0 +1,9 @@
import { auth } from '@/lib/auth';
import { AppEnv } from '@/types/hono';
import { Hono } from 'hono';
export const authRoutes = new Hono<AppEnv>();
authRoutes.on(['POST', 'GET'], '/api/auth/*', (c) => {
return auth.handler(c.req.raw);
});

View File

@@ -1,12 +1,25 @@
import { requireAuth } from '@/middlewares/require-auth.middleware'; import { requireAuth } from '@/middlewares/require-auth.middleware';
import { acceptComplexInvitationsHandler } from '@/modules/complex/handlers/accept-complex-invitations.handler';
import { cancelComplexInvitationHandler } from '@/modules/complex/handlers/cancel-complex-invitation.handler';
import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler'; import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler';
import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler'; import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler';
import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler'; import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler';
import { getCurrentComplexHandler } from '@/modules/complex/handlers/get-current-complex.handler';
import { inviteComplexUserHandler } from '@/modules/complex/handlers/invite-complex-user.handler';
import { listComplexUsersHandler } from '@/modules/complex/handlers/list-complex-users.handler';
import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler'; import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler';
import { resendComplexInvitationHandler } from '@/modules/complex/handlers/resend-complex-invitation.handler';
import { revokeComplexUserHandler } from '@/modules/complex/handlers/revoke-complex-user.handler';
import { selectComplexHandler } from '@/modules/complex/handlers/select-complex.handler';
import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler'; import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler';
import type { AppEnv } from '@/types/hono'; import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { createComplexSchema, updateComplexSchema } from '@repo/api-contract'; import {
acceptComplexInvitationsSchema,
createComplexSchema,
inviteComplexUserSchema,
updateComplexSchema,
} from '@repo/api-contract';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { z } from 'zod'; import { z } from 'zod';
@@ -17,6 +30,13 @@ const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) });
complexRoutes.use('*', requireAuth); complexRoutes.use('*', requireAuth);
complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler); complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler);
complexRoutes.get('/me', getCurrentComplexHandler);
complexRoutes.post('/select', selectComplexHandler);
complexRoutes.post(
'/accept-pending',
zValidator('json', acceptComplexInvitationsSchema),
acceptComplexInvitationsHandler
);
complexRoutes.get('/mine', listMyComplexesHandler); complexRoutes.get('/mine', listMyComplexesHandler);
complexRoutes.get( complexRoutes.get(
'/slug/:slug', '/slug/:slug',
@@ -24,6 +44,33 @@ complexRoutes.get(
getComplexBySlugHandler getComplexBySlugHandler
); );
complexRoutes.get(
'/:id/users',
zValidator('param', complexIdParamsSchema),
listComplexUsersHandler
);
complexRoutes.post(
'/:id/users',
zValidator('param', complexIdParamsSchema),
zValidator('json', inviteComplexUserSchema),
inviteComplexUserHandler
);
complexRoutes.post(
'/:id/invitations/:invitationId/resend',
zValidator('param', z.object({ id: z.uuid(), invitationId: z.uuid() })),
resendComplexInvitationHandler
);
complexRoutes.delete(
'/:id/invitations/:invitationId',
zValidator('param', z.object({ id: z.uuid(), invitationId: z.uuid() })),
cancelComplexInvitationHandler
);
complexRoutes.delete(
'/:id/users/:userId',
zValidator('param', z.object({ id: z.uuid(), userId: z.string().min(1) })),
revokeComplexUserHandler
);
complexRoutes.get('/:id', zValidator('param', complexIdParamsSchema), getComplexByIdHandler); complexRoutes.get('/:id', zValidator('param', complexIdParamsSchema), getComplexByIdHandler);
complexRoutes.patch( complexRoutes.patch(
'/:id', '/:id',

View File

@@ -0,0 +1,27 @@
import {
ComplexMembersError,
acceptComplexInvitations,
} from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import type { AcceptComplexInvitationsInput } from '@repo/api-contract';
export async function acceptComplexInvitationsHandler(c: AppContext) {
const user = c.get('user');
const payload = c.req.valid('json' as never) as AcceptComplexInvitationsInput;
try {
const result = await acceptComplexInvitations(user.id, user.email, payload);
return c.json(result);
} catch (error) {
if (error instanceof ComplexMembersError) {
return c.json(
{ message: error.message },
{
status: error.status,
}
);
}
throw error;
}
}

View File

@@ -0,0 +1,27 @@
import {
ComplexMembersError,
cancelComplexInvitation,
} from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import { z } from 'zod';
const cancelInvitationParamsSchema = z.object({
id: z.uuid(),
invitationId: z.uuid(),
});
export async function cancelComplexInvitationHandler(c: AppContext) {
const user = c.get('user');
const params = cancelInvitationParamsSchema.parse(c.req.param());
try {
const result = await cancelComplexInvitation(user.id, params.id, params.invitationId);
return c.json(result);
} catch (error) {
if (error instanceof ComplexMembersError) {
return c.json({ message: error.message }, { status: error.status });
}
throw error;
}
}

View File

@@ -1,3 +1,4 @@
import { db } from '@/lib/prisma';
import { createComplex } from '@/modules/complex/services/complex.service'; import { createComplex } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
import type { CreateComplexInput } from '@repo/api-contract'; import type { CreateComplexInput } from '@repo/api-contract';
@@ -5,21 +6,45 @@ import type { CreateComplexInput } from '@repo/api-contract';
export async function createComplexHandler(c: AppContext) { export async function createComplexHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as CreateComplexInput; const payload = c.req.valid('json' as never) as CreateComplexInput;
const authUser = c.get('authUser'); const user = c.get('user');
const adminEmail = authUser.email; let adminEmail = user?.email;
let userId = user?.id;
if (!adminEmail) { if (!adminEmail) {
return c.json({ message: 'El usuario autenticado no tiene email.' }, 400); const body = await c.req.json().catch(() => ({}));
adminEmail = body.adminEmail;
userId = body.userId;
}
if (!adminEmail) {
return c.json({ message: 'Se requiere email del administrador.' }, 400);
}
if (!userId && adminEmail) {
const existingUser = await db.user.findUnique({
where: { email: adminEmail },
select: { id: true },
});
if (!existingUser) {
return c.json({ message: 'El usuario no existe.' }, 404);
}
userId = existingUser.id;
} }
const complex = await createComplex({ const complex = await createComplex({
complexName: payload.complexName, complexName: payload.complexName,
physicalAddress: payload.physicalAddress, physicalAddress: payload.physicalAddress,
adminEmail, adminEmail,
userId,
planCode: payload.planCode, planCode: payload.planCode,
city: payload.city, city: payload.city,
state: payload.state, state: payload.state,
country: payload.country, country: payload.country,
setupCourts: payload.setupCourts,
courtSportId: payload.courtSportId,
courtStartTime: payload.courtStartTime,
courtEndTime: payload.courtEndTime,
courtDaysOfWeek: payload.courtDaysOfWeek,
}); });
return c.json(complex, 201); return c.json(complex, 201);

View File

@@ -0,0 +1,22 @@
import { getCurrentComplex } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
import { getCookie } from 'hono/cookie';
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
export async function getCurrentComplexHandler(c: AppContext) {
const user = c.get('user');
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
if (!complexId) {
return c.json(null);
}
const complex = await getCurrentComplex(user.id, complexId);
if (!complex) {
return c.json(null);
}
return c.json(complex);
}

View File

@@ -0,0 +1,47 @@
import {
ComplexMembersError,
inviteComplexUser,
} from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import type { InviteComplexUserInput } from '@repo/api-contract';
import { z } from 'zod';
const complexParamsSchema = z.object({
id: z.uuid(),
});
type InviteComplexUserHandlerDeps = {
inviteComplexUser: typeof inviteComplexUser;
ComplexMembersError: typeof ComplexMembersError;
};
export function createInviteComplexUserHandler(
deps: InviteComplexUserHandlerDeps = {
inviteComplexUser,
ComplexMembersError,
}
) {
return async function inviteComplexUserHandler(c: AppContext) {
const user = c.get('user');
const params = complexParamsSchema.parse(c.req.param());
const payload = c.req.valid('json' as never) as InviteComplexUserInput;
try {
const result = await deps.inviteComplexUser(user.id, params.id, payload);
return c.json(result, 201);
} catch (error) {
if (error instanceof deps.ComplexMembersError) {
return c.json(
{ message: error.message },
{
status: error.status,
}
);
}
throw error;
}
};
}
export const inviteComplexUserHandler = createInviteComplexUserHandler();

View File

@@ -0,0 +1,14 @@
import { getComplexUsersOverview } from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import { z } from 'zod';
const complexParamsSchema = z.object({
id: z.uuid(),
});
export async function listComplexUsersHandler(c: AppContext) {
const user = c.get('user');
const params = complexParamsSchema.parse(c.req.param());
const result = await getComplexUsersOverview(user.id, params.id);
return c.json(result);
}

View File

@@ -2,7 +2,7 @@ import { listMyComplexes } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
export async function listMyComplexesHandler(c: AppContext) { export async function listMyComplexesHandler(c: AppContext) {
const appUserId = c.get('appUserId'); const user = c.get('user');
const complexes = await listMyComplexes(appUserId); const complexes = await listMyComplexes(user.id);
return c.json(complexes); return c.json(complexes);
} }

View File

@@ -0,0 +1,27 @@
import {
ComplexMembersError,
resendComplexInvitation,
} from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import { z } from 'zod';
const resendInvitationParamsSchema = z.object({
id: z.uuid(),
invitationId: z.uuid(),
});
export async function resendComplexInvitationHandler(c: AppContext) {
const user = c.get('user');
const params = resendInvitationParamsSchema.parse(c.req.param());
try {
const result = await resendComplexInvitation(user.id, params.id, params.invitationId);
return c.json(result);
} catch (error) {
if (error instanceof ComplexMembersError) {
return c.json({ message: error.message }, { status: error.status });
}
throw error;
}
}

View File

@@ -0,0 +1,32 @@
import {
ComplexMembersError,
revokeComplexUser,
} from '@/modules/complex/services/complex-members.service';
import type { AppContext } from '@/types/hono';
import { z } from 'zod';
const revokeParamsSchema = z.object({
id: z.uuid(),
userId: z.string().min(1),
});
export async function revokeComplexUserHandler(c: AppContext) {
const user = c.get('user');
const params = revokeParamsSchema.parse(c.req.param());
try {
const result = await revokeComplexUser(user.id, params.id, { userId: params.userId });
return c.json(result);
} catch (error) {
if (error instanceof ComplexMembersError) {
return c.json(
{ message: error.message },
{
status: error.status,
}
);
}
throw error;
}
}

View File

@@ -0,0 +1,38 @@
import { selectComplex } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
import { setCookie } from 'hono/cookie';
import { z } from 'zod';
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
const selectComplexSchema = z.object({
complexId: z.string().uuid(),
});
export async function selectComplexHandler(c: AppContext) {
const user = c.get('user');
const body = await c.req.json();
const result = selectComplexSchema.safeParse(body);
if (!result.success) {
return c.json({ message: 'Invalid payload', issues: result.error.issues }, { status: 400 });
}
const { complexId } = result.data;
const complex = await selectComplex(user.id, complexId);
if (!complex) {
return c.json({ message: 'Complex not found or not associated with user' }, { status: 404 });
}
setCookie(c, SELECTED_COMPLEX_COOKIE, complexId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
maxAge: COOKIE_MAX_AGE,
});
return c.json(complex);
}

View File

@@ -0,0 +1,525 @@
import { createHash, randomBytes } from 'node:crypto';
import { resolveAvatarUrl } from '@/lib/avatar';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
AcceptComplexInvitationsInput,
AcceptComplexInvitationsResponse,
CancelComplexInvitationResponse,
ComplexInvitation,
ComplexInvitationStatus,
ComplexUsersOverview,
InviteComplexUserInput,
InviteComplexUserResponse,
RevokeComplexUserInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
const INVITATION_TTL_DAYS = Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7);
const APP_BASE_URL = Bun.env.APP_BASE_URL ?? 'http://localhost:5173';
export class ComplexMembersError extends Error {
status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
super(message);
this.name = 'ComplexMembersError';
this.status = status;
}
}
function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
function createInvitationToken(): string {
return randomBytes(32).toString('hex');
}
function nowPlusDays(days: number): Date {
const date = new Date();
date.setDate(date.getDate() + days);
return date;
}
function getInvitationStatus(invitation: {
expiresAt: Date;
acceptedAt: Date | null;
revokedAt: Date | null;
}): ComplexInvitationStatus {
if (invitation.acceptedAt) return 'ACCEPTED';
if (invitation.revokedAt) return 'REVOKED';
if (invitation.expiresAt < new Date()) return 'EXPIRED';
return 'PENDING';
}
function formatInvitation(invitation: {
id: string;
complexId: string;
email: string;
expiresAt: Date;
acceptedAt: Date | null;
revokedAt: Date | null;
createdAt: Date;
}): ComplexInvitation {
return {
id: invitation.id,
complexId: invitation.complexId,
email: invitation.email,
status: getInvitationStatus(invitation),
createdAt: invitation.createdAt.toISOString(),
expiresAt: invitation.expiresAt.toISOString(),
acceptedAt: invitation.acceptedAt?.toISOString() ?? null,
revokedAt: invitation.revokedAt?.toISOString() ?? null,
};
}
async function ensureComplexAdmin(userId: string, complexId: string) {
const membership = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
select: {
role: true,
},
});
if (!membership) {
throw new ComplexMembersError('No tenés acceso a este complejo.', 403);
}
if (membership.role !== 'ADMIN') {
throw new ComplexMembersError('Solo un ADMIN puede administrar usuarios.', 403);
}
}
async function ensureComplexMember(userId: string, complexId: string) {
const membership = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
});
if (!membership) {
throw new ComplexMembersError('No tenés acceso a este complejo.', 403);
}
return membership;
}
async function sendInvitationEmail(input: {
complexName: string;
inviteeEmail: string;
token: string;
}) {
const inviteUrl = new URL('/invite', APP_BASE_URL);
inviteUrl.searchParams.set('inviteToken', input.token);
inviteUrl.searchParams.set('email', input.inviteeEmail);
await sendMail({
to: input.inviteeEmail,
subject: `Te invitaron a ${input.complexName}`,
text: `Te invitaron a sumarte a ${input.complexName}. Abrí este enlace para crear tu cuenta o iniciar sesión: ${inviteUrl.toString()}`,
html: `
<h2>Te invitaron a <strong>${input.complexName}</strong></h2>
<p>Hacé click en el siguiente enlace para crear tu cuenta o iniciar sesión y aceptar la invitación:</p>
<p><a href="${inviteUrl.toString()}">${inviteUrl.toString()}</a></p>
`,
});
}
async function refreshPendingInvitation(invitationId: string) {
const token = createInvitationToken();
const tokenHash = hashToken(token);
const expiresAt = nowPlusDays(INVITATION_TTL_DAYS);
const invitation = await db.complexInvitation.update({
where: {
id: invitationId,
},
data: {
tokenHash,
expiresAt,
acceptedAt: null,
revokedAt: null,
},
include: {
complex: {
select: {
complexName: true,
},
},
},
});
return {
invitation,
token,
};
}
export async function listComplexUsers(complexId: string) {
const [members, invitations] = await Promise.all([
db.complexUser.findMany({
where: { complexId },
include: {
user: true,
},
orderBy: {
createdAt: 'asc',
},
}),
db.complexInvitation.findMany({
where: {
complexId,
acceptedAt: null,
revokedAt: null,
expiresAt: {
gte: new Date(),
},
},
orderBy: {
createdAt: 'desc',
},
}),
]);
const membersWithAvatars = members.map((member) => ({
userId: member.userId,
fullName: member.user.name,
email: member.user.email,
avatarUrl: resolveAvatarUrl({
email: member.user.email,
image: member.user.image,
}),
role: member.role,
createdAt: member.createdAt.toISOString(),
}));
return {
members: membersWithAvatars,
invitations: invitations.map(formatInvitation),
} satisfies ComplexUsersOverview;
}
export async function inviteComplexUser(
userId: string,
complexId: string,
input: InviteComplexUserInput
): Promise<InviteComplexUserResponse> {
await ensureComplexAdmin(userId, complexId);
const email = normalizeEmail(input.email);
const complex = await db.complex.findUnique({
where: { id: complexId },
select: {
complexName: true,
},
});
if (!complex) {
throw new ComplexMembersError('El complejo no existe.', 404);
}
const existingUser = await db.user.findUnique({
where: { email },
select: { id: true },
});
if (existingUser) {
const existingMembership = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId: existingUser.id,
},
},
select: { userId: true },
});
if (existingMembership) {
throw new ComplexMembersError('Ese usuario ya pertenece al complejo.', 409);
}
}
const token = createInvitationToken();
const invitation = await db.$transaction(async (tx) => {
const pendingInvitation = await tx.complexInvitation.findFirst({
where: {
complexId,
email,
acceptedAt: null,
revokedAt: null,
expiresAt: {
gte: new Date(),
},
},
orderBy: {
createdAt: 'desc',
},
});
if (pendingInvitation) {
const refreshed = await tx.complexInvitation.update({
where: {
id: pendingInvitation.id,
},
data: {
tokenHash: hashToken(token),
expiresAt: nowPlusDays(INVITATION_TTL_DAYS),
acceptedAt: null,
revokedAt: null,
},
});
return refreshed;
}
return tx.complexInvitation.create({
data: {
id: uuidv7(),
complexId,
email,
tokenHash: hashToken(token),
expiresAt: nowPlusDays(INVITATION_TTL_DAYS),
},
});
});
await sendInvitationEmail({
complexName: complex.complexName,
inviteeEmail: email,
token,
});
return {
invitation: formatInvitation(invitation),
};
}
export async function resendComplexInvitation(
userId: string,
complexId: string,
invitationId: string
): Promise<InviteComplexUserResponse> {
await ensureComplexAdmin(userId, complexId);
const pendingInvitation = await db.complexInvitation.findUnique({
where: {
id: invitationId,
},
include: {
complex: {
select: {
complexName: true,
},
},
},
});
if (!pendingInvitation || pendingInvitation.complexId !== complexId) {
throw new ComplexMembersError('La invitación no existe.', 404);
}
if (pendingInvitation.acceptedAt) {
throw new ComplexMembersError('La invitación ya fue aceptada.', 409);
}
if (pendingInvitation.revokedAt) {
throw new ComplexMembersError('La invitación fue cancelada.', 409);
}
const refreshed = await refreshPendingInvitation(invitationId);
await sendInvitationEmail({
complexName: refreshed.invitation.complex.complexName,
inviteeEmail: pendingInvitation.email,
token: refreshed.token,
});
return {
invitation: formatInvitation({
id: refreshed.invitation.id,
complexId: refreshed.invitation.complexId,
email: refreshed.invitation.email,
expiresAt: refreshed.invitation.expiresAt,
acceptedAt: refreshed.invitation.acceptedAt,
revokedAt: refreshed.invitation.revokedAt,
createdAt: refreshed.invitation.createdAt,
}),
};
}
export async function acceptComplexInvitations(
userId: string,
email: string,
input: AcceptComplexInvitationsInput = {}
): Promise<AcceptComplexInvitationsResponse> {
const normalizedEmail = normalizeEmail(email);
const tokenHash = input.inviteToken ? hashToken(input.inviteToken) : null;
const invitations = await db.complexInvitation.findMany({
where: {
email: normalizedEmail,
acceptedAt: null,
revokedAt: null,
expiresAt: {
gte: new Date(),
},
...(tokenHash
? {
tokenHash,
}
: {}),
},
orderBy: {
createdAt: 'asc',
},
});
if (tokenHash && invitations.length === 0) {
throw new ComplexMembersError('La invitación es inválida o ya venció.', 404);
}
let acceptedCount = 0;
for (const invitation of invitations) {
await db.$transaction(async (tx) => {
const existingMembership = await tx.complexUser.findUnique({
where: {
complexId_userId: {
complexId: invitation.complexId,
userId,
},
},
select: {
userId: true,
},
});
if (!existingMembership) {
await tx.complexUser.create({
data: {
complexId: invitation.complexId,
userId,
role: 'EMPLOYEE',
},
});
}
await tx.complexInvitation.update({
where: {
id: invitation.id,
},
data: {
acceptedAt: new Date(),
},
});
});
acceptedCount += 1;
}
return {
acceptedCount,
};
}
export async function revokeComplexUser(
userId: string,
complexId: string,
input: RevokeComplexUserInput
) {
await ensureComplexAdmin(userId, complexId);
const membership = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId: input.userId,
},
},
select: {
role: true,
},
});
if (!membership) {
throw new ComplexMembersError('Ese usuario no pertenece al complejo.', 404);
}
if (membership.role !== 'EMPLOYEE') {
throw new ComplexMembersError('Solo podés revocar usuarios con rol EMPLOYEE.', 409);
}
await db.complexUser.delete({
where: {
complexId_userId: {
complexId,
userId: input.userId,
},
},
});
return {
ok: true,
};
}
export async function cancelComplexInvitation(
userId: string,
complexId: string,
invitationId: string
): Promise<CancelComplexInvitationResponse> {
await ensureComplexAdmin(userId, complexId);
const invitation = await db.complexInvitation.findUnique({
where: {
id: invitationId,
},
select: {
complexId: true,
acceptedAt: true,
revokedAt: true,
},
});
if (!invitation || invitation.complexId !== complexId) {
throw new ComplexMembersError('La invitación no existe.', 404);
}
if (invitation.acceptedAt) {
throw new ComplexMembersError('La invitación ya fue aceptada.', 409);
}
if (invitation.revokedAt) {
return { ok: true };
}
await db.complexInvitation.update({
where: {
id: invitationId,
},
data: {
revokedAt: new Date(),
},
});
return { ok: true };
}
export async function getComplexUsersOverview(userId: string, complexId: string) {
await ensureComplexMember(userId, complexId);
return listComplexUsers(complexId);
}

View File

@@ -6,12 +6,52 @@ export type CreateComplexInput = {
complexName: string; complexName: string;
physicalAddress: string; physicalAddress: string;
adminEmail: string; adminEmail: string;
userId?: string;
planCode?: string; planCode?: string;
city?: string; city?: string;
state?: string; state?: string;
country?: string; country?: string;
setupCourts?: boolean;
courtSportId?: string;
courtStartTime?: string;
courtEndTime?: string;
courtDaysOfWeek?: string[];
}; };
type DayOfWeek = 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY';
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
function assertAvailabilityRanges(
availability: Array<{
dayOfWeek: DayOfWeek;
startTime: string;
endTime: string;
}>
) {
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
if (start >= end) {
throw new Error(`El rango ${range.startTime}-${range.endTime} es inválido.`);
}
}
}
async function ensureActiveSport(sportId: string) {
const sport = await db.sport.findFirst({
where: { id: sportId, isActive: true },
select: { id: true, name: true },
});
if (!sport) {
throw new Error('El deporte seleccionado no existe o está inactivo.');
}
return sport;
}
export type UpdateComplexInput = { export type UpdateComplexInput = {
complexName?: string; complexName?: string;
physicalAddress?: string | null; physicalAddress?: string | null;
@@ -64,20 +104,68 @@ async function buildUniqueSlug(source: string, excludeComplexId?: string): Promi
} }
export async function createComplex(input: CreateComplexInput) { export async function createComplex(input: CreateComplexInput) {
const complexSlug = await buildUniqueSlug(input.complexName); const { userId } = input;
return db.complex.create({ return db.$transaction(async (tx) => {
data: { const complexSlug = await buildUniqueSlug(input.complexName);
id: uuidv7(),
complexName: input.complexName, const complex = await tx.complex.create({
physicalAddress: input.physicalAddress.trim(), data: {
city: input.city?.trim() || null, id: uuidv7(),
state: input.state?.trim() || null, complexName: input.complexName,
country: input.country?.trim() || null, physicalAddress: input.physicalAddress.trim(),
complexSlug, city: input.city?.trim() || null,
adminEmail: input.adminEmail, state: input.state?.trim() || null,
planCode: input.planCode, country: input.country?.trim() || null,
}, complexSlug,
adminEmail: input.adminEmail,
planCode: input.planCode,
},
});
if (userId) {
await tx.complexUser.create({
data: {
complexId: complex.id,
userId,
role: 'ADMIN',
},
});
}
if (input.setupCourts && input.courtSportId) {
const sport = await ensureActiveSport(input.courtSportId);
const availability = (input.courtDaysOfWeek ?? []).map((day) => ({
dayOfWeek: day as DayOfWeek,
startTime: input.courtStartTime ?? '08:00',
endTime: input.courtEndTime ?? '22:00',
}));
assertAvailabilityRanges(availability);
const court = await tx.court.create({
data: {
id: uuidv7(),
complexId: complex.id,
sportId: input.courtSportId,
name: `${sport.name} 1`,
slotDurationMinutes: 60,
basePrice: 0,
},
});
await tx.courtAvailability.createMany({
data: availability.map((avail) => ({
id: uuidv7(),
courtId: court.id,
dayOfWeek: avail.dayOfWeek,
startTime: avail.startTime,
endTime: avail.endTime,
})),
});
}
return complex;
}); });
} }
@@ -93,9 +181,9 @@ export async function getComplexBySlug(slug: string) {
}); });
} }
export async function listMyComplexes(appUserId: string) { export async function listMyComplexes(userId: string) {
const complexUsers = await db.complexUser.findMany({ const complexUsers = await db.complexUser.findMany({
where: { userId: appUserId }, where: { userId },
include: { include: {
complex: true, complex: true,
}, },
@@ -104,7 +192,52 @@ export async function listMyComplexes(appUserId: string) {
}, },
}); });
return complexUsers.map((complexUser) => complexUser.complex); return complexUsers.map((complexUser) => ({
...complexUser.complex,
role: complexUser.role,
}));
}
export async function getCurrentComplex(userId: string, complexId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: true,
},
});
if (!complexUser) return null;
return {
...complexUser.complex,
role: complexUser.role,
};
}
export async function selectComplex(userId: string, complexId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: true,
},
});
if (!complexUser) return null;
return {
...complexUser.complex,
role: complexUser.role,
};
} }
export async function updateComplex(id: string, input: UpdateComplexInput) { export async function updateComplex(id: string, input: UpdateComplexInput) {

View File

@@ -7,10 +7,10 @@ type ComplexIdParams = { complexId: string };
export async function createCourtHandler(c: AppContext) { export async function createCourtHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams; const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateCourtInput; const payload = c.req.valid('json' as never) as CreateCourtInput;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const court = await createCourt(appUserId, complexId, payload); const court = await createCourt(user.id, complexId, payload);
return c.json(court, 201); return c.json(court, 201);
} catch (error) { } catch (error) {
if (error instanceof CourtServiceError) { if (error instanceof CourtServiceError) {

View File

@@ -5,10 +5,10 @@ type ComplexIdParams = { complexId: string };
export async function listCourtsByComplexHandler(c: AppContext) { export async function listCourtsByComplexHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams; const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const courts = await listCourtsByComplex(complexId, appUserId); const courts = await listCourtsByComplex(complexId, user.id);
return c.json(courts); return c.json(courts);
} catch (error) { } catch (error) {
if (error instanceof CourtServiceError) { if (error instanceof CourtServiceError) {

View File

@@ -7,10 +7,10 @@ type CourtIdParams = { id: string };
export async function updateCourtHandler(c: AppContext) { export async function updateCourtHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as CourtIdParams; const { id } = c.req.valid('param' as never) as CourtIdParams;
const payload = c.req.valid('json' as never) as UpdateCourtInput; const payload = c.req.valid('json' as never) as UpdateCourtInput;
const appUserId = c.get('appUserId'); const user = c.get('user');
try { try {
const court = await updateCourt(appUserId, id, payload); const court = await updateCourt(user.id, id, payload);
return c.json(court); return c.json(court);
} catch (error) { } catch (error) {
if (error instanceof CourtServiceError) { if (error instanceof CourtServiceError) {

View File

@@ -61,12 +61,12 @@ function assertAvailabilityRanges(
} }
} }
async function ensureComplexAccess(complexId: string, appUserId: string) { async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({ const complexUser = await db.complexUser.findUnique({
where: { where: {
complexId_userId: { complexId_userId: {
complexId, complexId,
userId: appUserId, userId,
}, },
}, },
include: { include: {
@@ -137,14 +137,14 @@ async function enforcePlanCourtLimit(complexId: string) {
} }
} }
async function getCourtByIdForUser(courtId: string, appUserId: string) { async function getCourtByIdForUser(courtId: string, userId: string) {
return db.court.findFirst({ return db.court.findFirst({
where: { where: {
id: courtId, id: courtId,
complex: { complex: {
users: { users: {
some: { some: {
userId: appUserId, userId,
}, },
}, },
}, },
@@ -195,8 +195,8 @@ function mapCourtResponse(court: CourtWithRelations) {
}; };
} }
export async function listCourtsByComplex(complexId: string, appUserId: string) { export async function listCourtsByComplex(complexId: string, userId: string) {
await ensureComplexAccess(complexId, appUserId); await ensureComplexAccess(complexId, userId);
const courts = await db.court.findMany({ const courts = await db.court.findMany({
where: { complexId }, where: { complexId },
@@ -218,8 +218,8 @@ export async function listCourtsByComplex(complexId: string, appUserId: string)
return courts.map((court) => mapCourtResponse(court)); return courts.map((court) => mapCourtResponse(court));
} }
export async function createCourt(appUserId: string, complexId: string, input: CreateCourtInput) { export async function createCourt(userId: string, complexId: string, input: CreateCourtInput) {
await ensureComplexAccess(complexId, appUserId); await ensureComplexAccess(complexId, userId);
await ensureActiveSport(input.sportId); await ensureActiveSport(input.sportId);
await enforcePlanCourtLimit(complexId); await enforcePlanCourtLimit(complexId);
assertAvailabilityRanges(input.availability); assertAvailabilityRanges(input.availability);
@@ -249,7 +249,7 @@ export async function createCourt(appUserId: string, complexId: string, input: C
return court; return court;
}); });
const court = await getCourtByIdForUser(createdCourt.id, appUserId); const court = await getCourtByIdForUser(createdCourt.id, userId);
if (!court) { if (!court) {
throw new CourtServiceError('No se pudo obtener la cancha creada.', 404); throw new CourtServiceError('No se pudo obtener la cancha creada.', 404);
@@ -258,8 +258,8 @@ export async function createCourt(appUserId: string, complexId: string, input: C
return mapCourtResponse(court); return mapCourtResponse(court);
} }
export async function updateCourt(appUserId: string, courtId: string, input: UpdateCourtInput) { export async function updateCourt(userId: string, courtId: string, input: UpdateCourtInput) {
const existingCourt = await getCourtByIdForUser(courtId, appUserId); const existingCourt = await getCourtByIdForUser(courtId, userId);
if (!existingCourt) { if (!existingCourt) {
throw new CourtServiceError('Cancha no encontrada.', 404); throw new CourtServiceError('Cancha no encontrada.', 404);
@@ -305,7 +305,7 @@ export async function updateCourt(appUserId: string, courtId: string, input: Upd
} }
}); });
const updatedCourt = await getCourtByIdForUser(courtId, appUserId); const updatedCourt = await getCourtByIdForUser(courtId, userId);
if (!updatedCourt) { if (!updatedCourt) {
throw new CourtServiceError('Cancha no encontrada.', 404); throw new CourtServiceError('Cancha no encontrada.', 404);

View File

@@ -1,27 +0,0 @@
import {
OnboardingError,
completeOnboarding,
} from '@/modules/onboarding/services/onboarding.service';
import type { AppContext } from '@/types/hono';
import type { OnboardingCompleteInput } from '@repo/api-contract';
export async function completeOnboardingHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as OnboardingCompleteInput;
try {
const result = await completeOnboarding(payload);
return c.json({
message: 'Onboarding completado correctamente.',
...result,
});
} catch (error) {
if (error instanceof OnboardingError) {
return c.json({ message: error.message }, error.status);
}
if (error instanceof Error) {
return c.json({ message: error.message }, 400);
}
throw error;
}
}

View File

@@ -1,25 +0,0 @@
import {
OnboardingError,
resendOnboardingOtp,
} from '@/modules/onboarding/services/onboarding.service';
import type { AppContext } from '@/types/hono';
import type { OnboardingResendOtpInput } from '@repo/api-contract';
export async function resendOtpHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as OnboardingResendOtpInput;
try {
const result = await resendOnboardingOtp(payload);
return c.json(result);
} catch (error) {
if (error instanceof OnboardingError) {
return c.json({ message: error.message }, error.status);
}
if (error instanceof Error) {
return c.json({ message: error.message }, 400);
}
throw error;
}
}

View File

@@ -1,10 +0,0 @@
import { startOnboarding } from '@/modules/onboarding/services/onboarding.service';
import type { AppContext } from '@/types/hono';
import type { OnboardingStartInput } from '@repo/api-contract';
export async function startOnboardingHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as OnboardingStartInput;
const result = await startOnboarding(payload);
return c.json(result, 202);
}

View File

@@ -1,10 +0,0 @@
import { verifyOnboardingOtp } from '@/modules/onboarding/services/onboarding.service';
import type { AppContext } from '@/types/hono';
import type { OnboardingVerifyOtpInput } from '@repo/api-contract';
export async function verifyOtpHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as OnboardingVerifyOtpInput;
const result = await verifyOnboardingOtp(payload);
return c.json(result);
}

View File

@@ -1,35 +0,0 @@
import { completeOnboardingHandler } from '@/modules/onboarding/handlers/complete-onboarding.handler';
import { resendOtpHandler } from '@/modules/onboarding/handlers/resend-otp.handler';
import { startOnboardingHandler } from '@/modules/onboarding/handlers/start-onboarding.handler';
import { verifyOtpHandler } from '@/modules/onboarding/handlers/verify-otp.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {
onboardingCompleteSchema,
onboardingResendOtpSchema,
onboardingStartSchema,
onboardingVerifyOtpSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
export const onboardingRoutes = new Hono<AppEnv>();
onboardingRoutes.post('/start', zValidator('json', onboardingStartSchema), startOnboardingHandler);
onboardingRoutes.post(
'/verify-otp',
zValidator('json', onboardingVerifyOtpSchema),
verifyOtpHandler
);
onboardingRoutes.post(
'/resend-otp',
zValidator('json', onboardingResendOtpSchema),
resendOtpHandler
);
onboardingRoutes.post(
'/complete',
zValidator('json', onboardingCompleteSchema),
completeOnboardingHandler
);

View File

@@ -1,436 +0,0 @@
import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
OnboardingCompleteInput,
OnboardingResendOtpInput,
OnboardingStartInput,
OnboardingVerifyOtpInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
const OTP_LENGTH = 6;
const OTP_TTL_MINUTES = Number(Bun.env.ONBOARDING_OTP_TTL_MINUTES ?? 10);
const OTP_MAX_ATTEMPTS = Number(Bun.env.ONBOARDING_OTP_MAX_ATTEMPTS ?? 5);
const OTP_RESEND_COOLDOWN_SECONDS = Number(Bun.env.ONBOARDING_OTP_RESEND_COOLDOWN_SECONDS ?? 30);
type VerifyOtpResult = {
message: string;
verified: boolean;
requestId: string;
email: string | null;
expiresAt: string | null;
remainingAttempts: number;
cooldownSeconds: number;
};
type ResendOtpResult = {
message: string;
requestId: string;
email: string;
expiresAt: string;
cooldownSeconds: number;
remainingAttempts: number;
};
type StartOnboardingResult = {
message: string;
requestId: string;
email: string;
expiresAt: string;
cooldownSeconds: number;
};
export class OnboardingError extends Error {
status: 400 | 404 | 409 | 429;
constructor(message: string, status: 400 | 404 | 409 | 429 = 400) {
super(message);
this.name = 'OnboardingError';
this.status = status;
}
}
function nowPlusMinutes(minutes: number): Date {
const date = new Date();
date.setMinutes(date.getMinutes() + minutes);
return date;
}
function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
function createOtpCode(): string {
return randomInt(0, 10 ** OTP_LENGTH)
.toString()
.padStart(OTP_LENGTH, '0');
}
function hashValue(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
function getRemainingAttempts(otpAttempts: number): number {
return Math.max(0, OTP_MAX_ATTEMPTS - otpAttempts);
}
function getCooldownSeconds(otpLastSentAt: Date): number {
const elapsedMs = Date.now() - otpLastSentAt.getTime();
const remainingMs = OTP_RESEND_COOLDOWN_SECONDS * 1000 - elapsedMs;
return Math.max(0, Math.ceil(remainingMs / 1000));
}
function slugify(value: string): string {
return value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
async function buildUniqueComplexSlug(complexName: string): Promise<string> {
const base = slugify(complexName);
const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
let candidate = fallback;
let index = 1;
while (true) {
const existing = await db.complex.findFirst({
where: { complexSlug: candidate },
select: { id: true },
});
if (!existing) {
return candidate;
}
index += 1;
candidate = `${fallback}-${index}`;
}
}
async function sendOtpEmail(email: string, otpCode: string) {
await sendMail({
to: email,
subject: 'Codigo OTP para validar tu email',
text: `Tu codigo OTP es ${otpCode}. Vence en ${OTP_TTL_MINUTES} minutos.`,
html: `<p>Tu codigo OTP es <strong>${otpCode}</strong>.</p><p>Vence en ${OTP_TTL_MINUTES} minutos.</p>`,
});
}
export async function startOnboarding(input: OnboardingStartInput): Promise<StartOnboardingResult> {
const email = normalizeEmail(input.email);
const otpCode = createOtpCode();
const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
const now = new Date();
const request = await db.onboardingRequest.create({
data: {
id: uuidv7(),
fullName: input.fullName.trim(),
email,
otpHash: hashValue(otpCode),
otpExpiresAt: expiresAt,
otpAttempts: 0,
otpLastSentAt: now,
otpResendCount: 0,
},
select: {
id: true,
email: true,
otpExpiresAt: true,
},
});
await sendOtpEmail(email, otpCode);
return {
message: 'Te enviamos un codigo OTP para validar tu direccion.',
requestId: request.id,
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
};
}
export async function verifyOnboardingOtp(
input: OnboardingVerifyOtpInput
): Promise<VerifyOtpResult> {
const request = await db.onboardingRequest.findUnique({
where: { id: input.requestId },
});
if (!request) {
return {
message: 'Solicitud de onboarding invalida o expirada.',
verified: false,
requestId: input.requestId,
email: null,
expiresAt: null,
remainingAttempts: 0,
cooldownSeconds: 0,
};
}
if (request.completedAt) {
return {
message: 'Este onboarding ya fue completado.',
verified: true,
requestId: request.id,
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
};
}
if (request.otpExpiresAt < new Date()) {
return {
message: 'El codigo OTP vencio. Solicita un nuevo codigo.',
verified: false,
requestId: request.id,
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
};
}
if (request.otpAttempts >= OTP_MAX_ATTEMPTS) {
return {
message: 'Se agotaron los intentos de OTP. Solicita un nuevo codigo.',
verified: false,
requestId: request.id,
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: 0,
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
};
}
const otpMatches = hashValue(input.otp) === request.otpHash;
if (!otpMatches) {
const updated = await db.onboardingRequest.update({
where: { id: request.id },
data: {
otpAttempts: {
increment: 1,
},
},
select: {
id: true,
email: true,
otpAttempts: true,
otpExpiresAt: true,
otpLastSentAt: true,
},
});
const remainingAttempts = getRemainingAttempts(updated.otpAttempts);
return {
message:
remainingAttempts > 0
? 'Codigo OTP invalido.'
: 'Se agotaron los intentos de OTP. Solicita un nuevo codigo.',
verified: false,
requestId: updated.id,
email: updated.email,
expiresAt: updated.otpExpiresAt.toISOString(),
remainingAttempts,
cooldownSeconds: getCooldownSeconds(updated.otpLastSentAt),
};
}
if (!request.emailVerifiedAt) {
await db.onboardingRequest.update({
where: { id: request.id },
data: { emailVerifiedAt: new Date() },
});
}
return {
message: 'Email verificado correctamente.',
verified: true,
requestId: request.id,
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
};
}
export async function resendOnboardingOtp(
input: OnboardingResendOtpInput
): Promise<ResendOtpResult> {
const request = await db.onboardingRequest.findUnique({
where: { id: input.requestId },
});
if (!request) {
throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 404);
}
if (request.completedAt) {
throw new OnboardingError('Este onboarding ya fue completado.', 400);
}
if (request.emailVerifiedAt) {
throw new OnboardingError('El email ya fue verificado.', 400);
}
const cooldownSeconds = getCooldownSeconds(request.otpLastSentAt);
if (cooldownSeconds > 0) {
throw new OnboardingError(`Debes esperar ${cooldownSeconds}s antes de reenviar el OTP.`, 429);
}
const otpCode = createOtpCode();
const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
const now = new Date();
const updated = await db.onboardingRequest.update({
where: { id: request.id },
data: {
otpHash: hashValue(otpCode),
otpExpiresAt: expiresAt,
otpAttempts: 0,
otpLastSentAt: now,
otpResendCount: {
increment: 1,
},
},
select: {
id: true,
email: true,
otpExpiresAt: true,
otpAttempts: true,
},
});
await sendOtpEmail(updated.email, otpCode);
return {
message: 'Te enviamos un nuevo codigo OTP.',
requestId: updated.id,
email: updated.email,
expiresAt: updated.otpExpiresAt.toISOString(),
cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
remainingAttempts: getRemainingAttempts(updated.otpAttempts),
};
}
export async function completeOnboarding(input: OnboardingCompleteInput) {
const onboardingRequest = await db.onboardingRequest.findUnique({
where: { id: input.onboardingRequestId },
});
if (!onboardingRequest) {
throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 400);
}
if (onboardingRequest.otpExpiresAt < new Date()) {
throw new OnboardingError('La sesion de onboarding expiro. Solicita un nuevo OTP.', 400);
}
if (!onboardingRequest.emailVerifiedAt) {
throw new OnboardingError('Debes verificar el email antes de continuar.', 400);
}
if (onboardingRequest.completedAt) {
throw new OnboardingError('Este onboarding ya fue completado.', 400);
}
const plan = await db.plan.findUnique({
where: { code: input.planCode },
select: { code: true },
});
if (!plan) {
throw new OnboardingError('El plan seleccionado no existe.', 400);
}
const complexSlug = await buildUniqueComplexSlug(input.complexName);
const result = await db.$transaction(async (tx) => {
const existingUser = await tx.user.findUnique({
where: { email: onboardingRequest.email },
});
let userId: string;
if (existingUser) {
userId = existingUser.id;
} else {
const newUser = await auth.api.signUpEmail({
body: {
email: onboardingRequest.email,
password: input.password,
name: onboardingRequest.fullName,
},
});
if (!newUser.user) {
throw new OnboardingError('No se pudo crear el usuario en Better Auth.', 400);
}
userId = newUser.user.id;
}
const user = await tx.user.upsert({
where: { email: onboardingRequest.email },
update: {
fullName: onboardingRequest.fullName,
},
create: {
id: userId,
fullName: onboardingRequest.fullName,
email: onboardingRequest.email,
},
});
const complex = await tx.complex.create({
data: {
id: uuidv7(),
complexName: input.complexName.trim(),
physicalAddress: input.physicalAddress.trim(),
city: input.city?.trim() || null,
state: input.state?.trim() || null,
country: input.country?.trim() || null,
complexSlug,
adminEmail: onboardingRequest.email,
planCode: input.planCode,
},
});
await tx.complexUser.create({
data: {
id: uuidv7(),
complexId: complex.id,
userId: user.id,
role: 'ADMIN',
},
});
await tx.onboardingRequest.update({
where: { id: onboardingRequest.id },
data: {
completedAt: new Date(),
},
});
return {
userId: user.id,
complexId: complex.id,
complexSlug: complex.complexSlug,
};
});
return result;
}

View File

@@ -189,15 +189,25 @@ async function sendPasswordChangedEmail(
}); });
} }
async function findBetterAuthUserByEmail(email: string): Promise<{ id: string } | null> {
const user = await db.user.findUnique({
where: { email },
select: { id: true },
});
if (!user) {
return null;
}
return { id: user.id };
}
export async function startPasswordReset( export async function startPasswordReset(
input: PasswordResetStartInput input: PasswordResetStartInput
): Promise<PasswordResetStartResult> { ): Promise<PasswordResetStartResult> {
const email = normalizeEmail(input.email); const email = normalizeEmail(input.email);
const existingUser = await db.user.findUnique({ const existingUser = await findBetterAuthUserByEmail(email);
where: { email },
});
if (!existingUser) { if (!existingUser) {
return { return {
message: 'Si el email existe, recibirás un código de verificación.', message: 'Si el email existe, recibirás un código de verificación.',
@@ -342,17 +352,30 @@ export async function verifyOtpAndResetPassword(
throw new PasswordResetError('Código incorrecto.', 400); throw new PasswordResetError('Código incorrecto.', 400);
} }
const existingUser = await db.user.findUnique({ const existingUser = await findBetterAuthUserByEmail(request.email);
where: { email: request.email },
});
if (!existingUser) { if (!existingUser) {
throw new PasswordResetError('Usuario no encontrado.', 404); throw new PasswordResetError('Usuario no encontrado.', 404);
} }
await auth.api.changePassword({ const resetToken = uuidv7();
userId: existingUser.id, const identifier = `reset-password:${resetToken}`;
password: newPassword, const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
await db.verification.create({
data: {
id: uuidv7(),
identifier,
value: existingUser.id,
expiresAt,
},
});
await auth.api.resetPassword({
asResponse: false,
body: {
token: resetToken,
newPassword,
},
}); });
await db.passwordResetRequest.update({ await db.passwordResetRequest.update({

View File

@@ -1,23 +1,24 @@
import { db } from '@/lib/prisma'; import { db } from '@/lib/prisma';
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
export async function listPlansHandler(c: AppContext) { export async function listPlansHandler(c: AppContext) {
const plans = await db.plan.findMany({ const plans = await db.plan.findMany({
select: {
code: true,
name: true,
price: true,
},
orderBy: { orderBy: {
price: 'asc', price: 'asc',
}, },
}); });
return c.json( return c.json(
plans.map((plan) => ({ plans.map((plan) => {
code: plan.code, const rules = parsePlanRules(plan.rules);
name: plan.name, return {
price: Number(plan.price), code: plan.code,
})) name: plan.name,
price: Number(plan.price),
features: rules.features,
limits: rules.limits,
};
})
); );
} }

View File

@@ -1,3 +1,4 @@
import { sseManager } from '@/lib/sse';
import { import {
PublicBookingServiceError, PublicBookingServiceError,
createPublicBooking, createPublicBooking,
@@ -13,6 +14,22 @@ export async function createPublicBookingHandler(c: AppContext) {
try { try {
const booking = await createPublicBooking(complexSlug, payload); const booking = await createPublicBooking(complexSlug, payload);
const channel = `complex-${booking.complexId}`;
sseManager.emit(
channel,
JSON.stringify({
type: 'booking_created',
booking: {
id: booking.id,
courtId: booking.courtId,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
},
})
);
return c.json(booking, 201); return c.json(booking, 201);
} catch (error) { } catch (error) {
if (error instanceof PublicBookingServiceError) { if (error instanceof PublicBookingServiceError) {

View File

@@ -183,6 +183,7 @@ async function getComplexWithBookingData(complexSlug: string) {
id: true, id: true,
complexName: true, complexName: true,
complexSlug: true, complexSlug: true,
physicalAddress: true,
plan: { plan: {
select: { select: {
rules: true, rules: true,
@@ -234,7 +235,7 @@ function mapBookingResponse(input: {
endTime: string; endTime: string;
customerName: string; customerName: string;
customerPhone: string; customerPhone: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'; status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
court: { court: {
id: string; id: string;
name: string; name: string;
@@ -414,6 +415,7 @@ export async function listPublicAvailability(complexSlug: string, query: PublicA
return { return {
complexId: complex.id, complexId: complex.id,
complexName: complex.complexName, complexName: complex.complexName,
complexAddress: complex.physicalAddress ?? null,
complexSlug: complex.complexSlug, complexSlug: complex.complexSlug,
date: query.date, date: query.date,
sportSelectionRequired, sportSelectionRequired,

View File

@@ -1,18 +1,10 @@
import { Prisma } from '@/generated/prisma/client'; import { Prisma } from '@/generated/prisma/client';
import { handleResult } from '@/lib/http/handle-result';
import { createSport } from '@/modules/sport/services/sport.service'; import { createSport } from '@/modules/sport/services/sport.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
import type { CreateSportInput } from '@repo/api-contract'; import type { CreateSportInput } from '@repo/api-contract';
export async function createSportHandler(c: AppContext) { export async function createSportHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as CreateSportInput; const payload = c.req.valid('json' as never) as CreateSportInput;
return handleResult(c, await createSport(payload), 201);
try {
const sport = await createSport(payload);
return c.json(sport, 201);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return c.json({ message: 'No se pudo crear el deporte.' }, 409);
}
throw error;
}
} }

View File

@@ -1,7 +1,8 @@
import { handleResult } from '@/lib/http/handle-result';
import { listSports } from '@/modules/sport/services/sport.service'; import { listSports } from '@/modules/sport/services/sport.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
export async function listSportsHandler(c: AppContext) { export async function listSportsHandler(c: AppContext) {
const sports = await listSports(); const sports = await listSports();
return c.json(sports); return handleResult(c, sports);
} }

View File

@@ -1,5 +1,7 @@
import { Prisma } from '@/generated/prisma/client'; import { Prisma, Sport } from '@/generated/prisma/client';
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma'; import { db } from '@/lib/prisma';
import { Result, err, ok } from '@/lib/result';
import { v7 as uuidv7 } from 'uuid'; import { v7 as uuidv7 } from 'uuid';
export type CreateSportInput = { export type CreateSportInput = {
@@ -51,16 +53,25 @@ async function buildUniqueSlug(source: string, excludeSportId?: string): Promise
} }
} }
export async function listSports() { export async function listSports(): Promise<Result<Sport[]>> {
return db.sport.findMany({ const sports = await db.sport.findMany({
orderBy: { name: 'asc' }, orderBy: { name: 'asc' },
}); });
return ok(sports);
} }
export async function createSport(input: CreateSportInput) { export async function createSport(input: CreateSportInput) {
const slug = await buildUniqueSlug(input.name); const slug = await buildUniqueSlug(input.name);
return db.sport.create({ const existing = await db.sport.count({
where: { slug },
});
if (existing > 0) {
return err(Errors.conflict('Ya existe un deporte con ese nombre.'));
}
const created = await db.sport.create({
data: { data: {
id: uuidv7(), id: uuidv7(),
name: input.name.trim(), name: input.name.trim(),
@@ -68,6 +79,8 @@ export async function createSport(input: CreateSportInput) {
isActive: true, isActive: true,
}, },
}); });
return ok(created);
} }
export async function getSportById(id: string) { export async function getSportById(id: string) {

View File

@@ -1,3 +1,4 @@
import { validate } from '@/lib/http/validate';
import { requireAuth } from '@/middlewares/require-auth.middleware'; import { requireAuth } from '@/middlewares/require-auth.middleware';
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware'; import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler'; import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler';
@@ -8,18 +9,17 @@ import { zValidator } from '@hono/zod-validator';
import { createSportSchema, updateSportSchema } from '@repo/api-contract'; import { createSportSchema, updateSportSchema } from '@repo/api-contract';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { z } from 'zod'; import { z } from 'zod';
export const sportRoutes = new Hono<AppEnv>(); export const sportRoutes = new Hono<AppEnv>();
const sportIdParamsSchema = z.object({ id: z.uuid() }); const sportIdParamsSchema = z.object({ id: z.uuid() });
sportRoutes.use('*', requireAuth); sportRoutes.use('*', requireAuth);
sportRoutes.get('/', listSportsHandler); sportRoutes.get('/', listSportsHandler);
sportRoutes.post('/', requireSuperAdmin, zValidator('json', createSportSchema), createSportHandler); sportRoutes.post('/', requireSuperAdmin, validate.json(createSportSchema), createSportHandler);
sportRoutes.patch( sportRoutes.patch(
'/:id', '/:id',
requireSuperAdmin, requireSuperAdmin,
zValidator('param', sportIdParamsSchema), validate.param(sportIdParamsSchema),
zValidator('json', updateSportSchema), validate.json(updateSportSchema),
updateSportHandler updateSportHandler
); );

Some files were not shown because too many files have changed in this diff Show More