112 Commits

Author SHA1 Message Date
c1b47674c8 Merge pull request 'feat: add customer email functionality for booking confirmations' (#7) from feat/email-booking-confirmation into development
Reviewed-on: #7
2026-06-02 22:21:39 +00:00
Jose Selesan
85f234b05e feat: add customer email functionality for booking confirmations
- Implemented email confirmation for admin and public bookings.
- Added customerEmail field to booking schemas and services.
- Created email templates for booking confirmation, cancellation, and no-show notifications.
- Updated booking handlers to send emails upon booking creation and status updates.
- Enhanced frontend forms to capture customer email during booking creation.
2026-06-02 19:21:04 -03:00
50fa4ed9a5 Merge pull request 'feat/improve-public-booking' (#6) from feat/improve-public-booking into development
Reviewed-on: #6
2026-06-02 19:56:28 +00:00
Jose Selesan
07496e6673 feat(public-booking): enhance booking confirmation with price and address details 2026-06-02 16:55:47 -03:00
Jose Selesan
9045a8c017 feat(public-booking): add pricing logic to booking slots and update schemas 2026-06-02 16:37:11 -03:00
Jose Selesan
3e314a9b9a feat(public-booking): refactor court selection and available slots components for improved usability 2026-06-02 16:26:47 -03:00
bc4716c48f Merge pull request 'feat(email-validation): implement email verification checks for booking creation and resend functionality' (#5) from feat/require-email-validation into development
Reviewed-on: #5
2026-06-02 19:07:50 +00:00
Jose Selesan
0e69759549 feat(email-validation): implement email verification checks for booking creation and resend functionality 2026-06-02 15:47:36 -03:00
Jose Selesan
88ea7e70da feat(entrypoint): add database seeding step to entrypoint script 2026-06-02 11:05:13 -03:00
Jose Selesan
7a30f9a29b refactor(routes): convert route components to lazy-loaded for improved performance 2026-06-02 10:55:55 -03:00
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
Jose Selesan
e2299c6d46 feat: update .gitignore for Bun and generated files, minor formatting fix in password reset handler 2026-04-13 21:25:53 -03:00
Jose Selesan
62d28734fc cleanup: remove debug console.log from auth benchmarking 2026-04-13 19:33:13 -03:00
Jose Selesan
b7c303377a perf: validate JWT locally instead of calling Supabase API
- Use jose library for ES256 JWT verification with cached JWKS
- Fallback to Supabase only if JWT verification fails
- Use findUnique instead of findFirst for DB lookup
- Estimated improvement: ~450ms -> ~5ms per request
2026-04-13 17:50:52 -03:00
Jose Selesan
1d09716492 Updated Dockerfile 2026-04-13 13:45:28 -03:00
Jose Selesan
c409d44765 Removed REMOTE_ADDR variable 2026-04-13 13:24:45 -03:00
Jose Selesan
2b95ab37f6 chore(frontend): add generated route tree for Docker build 2026-04-13 09:34:29 -03:00
Jose Selesan
ff42f3cd60 fix(docker): copy full backend in prisma stage for .prisma cache 2026-04-13 09:29:04 -03:00
Jose Selesan
43df46f520 fix(docker): add prisma generate stage to Dockerfile build 2026-04-13 09:25:46 -03:00
Jose Selesan
70273f6760 feat(auth): add password reset flow with OTP
- Add password-reset Prisma schema for OTP tracking
- Add API contract schemas (start, verify, resend)
- Add password-reset service with OTP validation and email sending
- Add frontend reset-password page with 2-step flow
- Add InputOTP component usage for OTP input
- Add link to reset-password in login page
- Remove link to About in login
- Center and resize login/reset password forms
2026-04-13 09:10:06 -03:00
Jose Selesan
566291fb43 Changed Dockerfile 2026-04-10 23:11:32 -03:00
Jose Selesan
91dfdce66d Improved Dockerfile 2026-04-10 19:44:33 -03:00
Jose Selesan
8210a4bd9b feat(home): move manual booking to responsive dialog
- Add Dice UI responsive-dialog component
- Move manual booking form to dialog (drawer on mobile)
- Filter past time slots when booking for today
- Rename button to 'Nueva Reserva'
2026-04-10 16:33:45 -03:00
Jose Selesan
839e316226 chore: track bun.lock in git for Docker builds 2026-04-10 16:11:02 -03:00
Jose Selesan
c2f5c0d97c refactor(docker): move Dockerfile to root for simpler Dokploy setup 2026-04-10 16:05:31 -03:00
Jose Selesan
1911d5623c perf(docker): optimize build with separate dependency stages for better layer caching 2026-04-10 15:50:46 -03:00
Jose Selesan
9ee98a4cb4 feat: add city/state/country to complex, new settings page with sidebar, and Biome linting
- Added city, state, and country optional fields to Complex model
- Updated onboarding to include optional location fields
- Created new settings page with sidebar navigation (Datos del Complejo, Canchas)
- Replaced ESLint with Biome for frontend and backend linting
- Added parallel dev script with concurrently
- Migrated register-routes to use direct app.route() pattern
2026-04-10 15:36:15 -03:00
Jose Selesan
77004b14b0 Improved booking panel 2026-04-10 09:05:10 -03:00
Jose Selesan
2d86881f94 Added 'No show' status. Edit user profile 2026-04-09 23:01:09 -03:00
Jose Selesan
4dcaae7136 Fixed build error 2026-04-09 16:27:21 -03:00
Jose Selesan
3cba2fa485 New fields for complex 2026-04-09 16:18:58 -03:00
Jose Selesan
ae90562d88 Improved routes registration 2026-04-09 15:39:42 -03:00
Jose Selesan
7ca74970fa Added healthcheck route 2026-04-09 15:06:45 -03:00
Jose Selesan
aa2fe41ecc New booking dashboard 2026-04-09 14:54:13 -03:00
Jose Selesan
11dd12682c Minor change in court configuration 2026-04-09 11:47:10 -03:00
Jose Selesan
e58fa6efa2 Fixed Dockerfile to run migrations 2026-04-09 11:25:51 -03:00
Jose Selesan
9f14b74936 Fix Dockerfile 2026-04-09 11:07:12 -03:00
Jose Selesan
063991770a Changed Dockerfile 2026-04-09 10:43:19 -03:00
Jose Selesan
2a0558347a Add button to share booking confirmation 2026-04-09 10:13:33 -03:00
Jose Selesan
58c5ce339b Code refactoring 2026-04-09 10:01:54 -03:00
Jose Selesan
30ac0d569a Better court configuration 2026-04-09 09:38:21 -03:00
Jose Selesan
8a01606c80 Updated gitignore 2026-04-08 22:57:59 -03:00
291 changed files with 23346 additions and 7134 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?

19
.gitignore vendored
View File

@@ -1,5 +1,4 @@
node_modules
bun.lock
.DS_Store
# ---> Node
# Logs
@@ -10,6 +9,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.vite
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
@@ -78,6 +78,7 @@ web_modules/
# dotenv environment variable files
.env
apps/backend/.env
.env.development.local
.env.test.local
.env.production.local
@@ -139,3 +140,19 @@ dist
.yarn/install-state.gz
.pnp.*
# Bun
bun.lock
# Generated
apps/backend/src/generated
apps/backend/prisma.config.js
apps/backend/prisma/*.js
apps/backend/prisma/*.seed-data.js
apps/backend/src/app.js
apps/backend/src/lib/*.js
apps/backend/src/middlewares/*.js
apps/backend/src/modules/**/*.js
apps/backend/src/register-routes.js
apps/backend/src/server.js
apps/backend/src/index.js
apps/backend/src/types/*.js

195
AGENTS.md Normal file
View File

@@ -0,0 +1,195 @@
# AGENTS.md
## Commands
```sh
# Root (runs both frontend + backend)
bun run dev
# Individual packages
bun run dev:frontend
bun run dev:backend
bun run build:frontend
# Linting (Biome - ESLint was replaced)
bun run lint
bun run lint:fix # fixes and formats
# Prisma (run from backend)
bun --filter backend prisma:generate # regenerate client after schema changes
bun --filter backend prisma:migrate # apply migrations
```
## Architecture
```
apps/frontend/ # React + Vite + TanStack Router/Query
apps/backend/ # Bun + Hono + Prisma
packages/api-contract/ # Shared Zod schemas, types, route definitions
```
**Shared contract pattern:**
1. Update `packages/api-contract` first (schemas/types)
2. Implement handler in `apps/backend`
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
- **Prisma 7**: Uses `prisma.config.ts`, requires `DATABASE_URL` env var at generate time.
- **Gravatar Fallback**: Users without an `image` get an automatic Gravatar Identicon.
- 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.
## 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)
- **Build Context**: `./` (monorepo root)
- **Dockerfile**: `Dockerfile` (in root, not `apps/backend/`)
- **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
## Linting
Biome is used (not ESLint). Config in `biome.json`:
- Line width: 100
- Quote style: single
- Semicolons: always
- `noUnusedVariables`: warn
## Environment Variables
**Backend** (`apps/backend/.env`):
- `DATABASE_URL` - PostgreSQL connection (required for Prisma)
- `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` - Auth core
- `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`):
- `VITE_*` prefix required (Vite embeds these at build time)
- `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`

63
Dockerfile Normal file
View File

@@ -0,0 +1,63 @@
# syntax=docker/dockerfile:1.7
# =============================================================================
# Stage 1: Install frontend dependencies
# =============================================================================
FROM oven/bun:1.3.11 AS deps-frontend
WORKDIR /app
COPY package.json bun.lock ./
COPY apps/frontend/package.json apps/frontend/
COPY packages/api-contract/package.json packages/api-contract/
RUN bun install
# =============================================================================
# Stage 2: Build Frontend
# =============================================================================
FROM deps-frontend AS build-frontend
WORKDIR /app
COPY apps/frontend ./apps/frontend
COPY packages ./packages
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN bun run build:frontend
# =============================================================================
# Stage 3: Install backend dependencies
# =============================================================================
FROM oven/bun:1.3.11 AS deps-backend
WORKDIR /app
COPY package.json bun.lock ./
COPY apps/backend/package.json apps/backend/
COPY packages/api-contract/package.json packages/api-contract/
RUN bun install
# =============================================================================
# Stage 4: Build Backend (prisma generate + tsc)
# =============================================================================
FROM deps-backend AS build-backend
WORKDIR /app
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL:-postgresql://dummy:dummy@localhost:5432/dummy}
COPY apps/backend ./apps/backend
COPY packages ./packages
RUN bun --cwd apps/backend prisma:generate
RUN bun --cwd apps/backend build
# =============================================================================
# Stage 5: Runner - Production image
# =============================================================================
FROM oven/bun:1.3.11 AS runner
WORKDIR /app
ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000
COPY --from=build-backend /app/node_modules ./node_modules
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/packages ./packages
COPY package.json bun.lock ./
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
EXPOSE 3000
CMD ["/app/entrypoint.sh"]

183
README.md
View File

@@ -1,184 +1,105 @@
# Monorepo (Frontend + Backend + Contrato API)
# Playzer Monorepo
Este repositorio contiene:
- `apps/frontend`: app web en React + Vite + TanStack Router/Query.
- `apps/backend`: API en Bun + Hono + Prisma + Supabase Auth.
- `packages/api-contract`: contratos compartidos (schemas/types/rutas) entre frontend y backend.
- `apps/frontend`: React + Vite + TanStack Router/Query.
- `apps/backend`: Bun + Hono + Prisma + Better Auth.
- `packages/api-contract`: contratos Zod/tipos compartidos.
## Objetivo de este README
## Requisitos
Dejarte productivo/a en pocos minutos para que puedas:
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).
- Bun reciente
- PostgreSQL disponible
## Setup rápido
1. Instalar dependencias desde la raíz:
1. Instalar dependencias:
```sh
bun install
```
2. Configurar variables de entorno del backend:
2. Configurar backend:
```sh
cp apps/backend/.env.example apps/backend/.env
```
3. Configurar variables de entorno del frontend:
3. Configurar frontend:
```sh
cp apps/frontend/.env.example apps/frontend/.env
```
4. Revisar/editar valores en ambos `.env` según tu entorno.
## 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:
4. Ejecutar migraciones:
```sh
bun --filter backend prisma:migrate
```
Si cambiás el schema, regenerá cliente:
## Variables de entorno
```sh
bun --filter backend prisma:generate
```
### Backend (`apps/backend/.env`)
## 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
bun run dev:backend
```
Terminal 2 (frontend):
Frontend:
```sh
bun run dev:frontend
```
Puertos por defecto:
Todo junto:
- Frontend: `http://localhost:5173`
- Backend: `http://localhost:3000`
## 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
```sh
bun run dev
```
## Flujo de autenticación (resumen)
## Prisma
1. Frontend autentica con Supabase.
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/*`).
Regenerar cliente después de cambios de schema:
## Endpoints principales
```sh
bun --filter backend prisma:generate
```
- `GET /api/user/profile`
- `POST /api/tenants`
- `GET /api/tenants/:id`
- `PATCH /api/tenants/:id`
## Autenticación
## 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).
- Si agregás/cambiás endpoint:
1. Actualizá schema/tipos en `api-contract`.
2. Implementá handler/ruta en backend.
3. Consumí el contrato desde frontend.
- Validar payloads con Zod tanto en contrato como en rutas.
- Mantener PRs chicos y enfocados.
- Backend: `c.get('user')` y `c.get('session')` en contexto Hono.
- Frontend: `authClient` con `fetchOptions.credentials = 'include'`.
- Frontend API: Axios con `withCredentials = true`.
- Backend CORS: `credentials: true`.
## Troubleshooting rápido
## Deploy (Docker)
- Error de CORS:
- Revisar `CORS_ORIGIN` en backend y que incluya `http://localhost:5173`.
- `403 Missing bearer token`:
- Verificar login en frontend y envío del header `Authorization`.
- Error de Supabase env vars:
- Confirmar `SUPABASE_URL`/`SUPABASE_ANON_KEY` (backend) y `VITE_*` (frontend).
- Error de Prisma/DB:
- Confirmar `DATABASE_URL`, conectividad a PostgreSQL y migraciones aplicadas.
- Build context: raíz del monorepo (`./`)
- Dockerfile: `./Dockerfile`
- Build args requeridos:
- `DATABASE_URL`
- `VITE_API_BASE_URL`
- `BETTER_AUTH_SECRET`
- `BETTER_AUTH_URL`
## Primer día sugerido para onboarding
## Flujo recomendado de cambios de API
1. Levantar backend y frontend en local.
2. Crear usuario o iniciar sesión desde la pantalla de onboard.
3. Crear un tenant de prueba.
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.
1. Actualizar contrato en `packages/api-contract`.
2. Implementar backend en `apps/backend`.
3. Consumir contrato desde frontend (`@repo/api-contract`).

View File

@@ -1,12 +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
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
SUPABASE_ANON_KEY=your-anon-key
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
BETTER_AUTH_SECRET=your-random-secret
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
LOG_LEVEL=debug
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/monorepo
@@ -16,3 +12,7 @@ SMTP_PORT=587
SMTP_USER=user@example.com
SMTP_PASS=your-smtp-password
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
node_modules/
prisma.config.prod.ts

View File

@@ -1,40 +0,0 @@
# syntax=docker/dockerfile:1.7
FROM oven/bun:1.3 AS deps
WORKDIR /app
# Workspace manifests first for better layer caching.
COPY package.json bun.lock ./
COPY apps/backend/package.json apps/backend/package.json
COPY packages/api-contract/package.json packages/api-contract/package.json
RUN bun install --frozen-lockfile
FROM deps AS build
WORKDIR /app
COPY apps/backend apps/backend
COPY packages/api-contract packages/api-contract
# Prisma 7 config resolves DATABASE_URL at generate time.
ARG DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
ENV DATABASE_URL=${DATABASE_URL}
RUN bun --cwd apps/backend run prisma:generate
FROM oven/bun:1.3-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/apps/backend ./apps/backend
COPY --from=build /app/packages ./packages
COPY package.json bun.lock ./
WORKDIR /app/apps/backend
EXPOSE 3000
CMD ["bun", "run", "start"]

View File

@@ -9,3 +9,21 @@ bun run dev
```
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/`.

26
apps/backend/bun.lock Normal file
View File

@@ -0,0 +1,26 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "backend",
"dependencies": {
"hono": "^4.12.9",
},
"devDependencies": {
"@types/bun": "latest",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}

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

@@ -3,29 +3,38 @@
"scripts": {
"dev": "bun run --hot src/server.ts",
"start": "bun src/server.ts",
"build": "tsc -b",
"test": "bun test",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy",
"prisma:seed": "prisma db seed",
"prisma:seed:reset:plans": "bun prisma/reset-plans.ts"
},
"dependencies": {
"@better-auth/infra": "^0.2.4",
"@hono/zod-validator": "^0.7.6",
"@prisma/adapter-pg": "^7.6.0",
"@prisma/client": "^7",
"@repo/api-contract": "workspace:*",
"@supabase/supabase-js": "2.101.1",
"better-auth": "^1.6.4",
"dotenv": "^17.4.1",
"hono": "4.12.10",
"mailtrap": "^4.5.1",
"nodemailer": "^8.0.5",
"pg": "^8.20.0",
"pino": "10.3.1",
"pino-pretty": "13.1.3",
"pino-std-serializers": "7.1.0",
"prisma": "^7",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/nodemailer": "^8.0.0",
"prisma": "^7"
"bun-mock-prisma": "^1.2.1"
}
}

View File

@@ -1,5 +1,5 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma',
@@ -10,4 +10,4 @@ export default defineConfig({
datasource: {
url: env('DATABASE_URL'),
},
})
});

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

@@ -1,15 +1,19 @@
model Complex {
id String @id @db.Uuid
complexName String @map("complex_name")
physicalAddress String? @map("physical_address") @db.VarChar(200)
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[]
courts Court[]
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])
@@ -18,17 +22,35 @@ model Complex {
enum ComplexUserRole {
ADMIN
EMPLOYEE
}
model ComplexUser {
complexId String @map("complex_id") @db.Uuid
userId String @map("user_id") @db.Uuid
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)
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

@@ -11,6 +11,8 @@ enum DayOfWeek {
enum CourtBookingStatus {
CONFIRMED
CANCELLED
COMPLETED
NOSHOW
}
model Sport {
@@ -83,6 +85,7 @@ model CourtBooking {
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String? @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -93,3 +96,21 @@ model CourtBooking {
@@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)
customerEmail String? @map("customer_email") @db.VarChar(254)
changedAt DateTime @default(now()) @map("changed_at")
@@map("court_booking_logs")
@@index([courtId, newStatus])
}

View File

@@ -0,0 +1 @@
ALTER TYPE "CourtBookingStatus" ADD VALUE 'COMPLETED';

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "complexes" ADD COLUMN "city" VARCHAR(100),
ADD COLUMN "country" VARCHAR(100),
ADD COLUMN "state" VARCHAR(100);

View File

@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "password_reset_requests" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
"otp_hash" TEXT NOT NULL,
"otp_expires_at" TIMESTAMP(3) NOT NULL,
"otp_attempts" INTEGER NOT NULL DEFAULT 0,
"otp_last_sent_at" TIMESTAMP(3) NOT NULL,
"otp_resend_count" INTEGER NOT NULL DEFAULT 0,
"used_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "password_reset_requests_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "password_reset_requests_email_idx" ON "password_reset_requests"("email");
-- CreateIndex
CREATE INDEX "password_reset_requests_otp_expires_at_idx" ON "password_reset_requests"("otp_expires_at");

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,14 @@
/*
Warnings:
- You are about to drop the `onboarding_requests` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "court_booking_logs" ADD COLUMN "customer_email" VARCHAR(254);
-- AlterTable
ALTER TABLE "court_bookings" ADD COLUMN "customer_email" VARCHAR(254);
-- DropTable
DROP TABLE "onboarding_requests";

View File

@@ -1,18 +1,16 @@
model OnboardingRequest {
model PasswordResetRequest {
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")
usedAt DateTime? @map("used_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([email])
@@index([otpExpiresAt])
@@map("onboarding_requests")
@@map("password_reset_requests")
}

View File

@@ -74,4 +74,4 @@ export const planSeeds = [
},
},
},
] as const
] as const;

View File

@@ -1,33 +1,33 @@
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../src/generated/prisma/client'
import { planSeeds } from './plans.seed-data'
import 'dotenv/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/generated/prisma/client';
import { planSeeds } from './plans.seed-data';
const databaseUrl = process.env.DATABASE_URL
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('Missing DATABASE_URL in environment for plan reset.')
throw new Error('Missing DATABASE_URL in environment for plan reset.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
})
});
const prisma = new PrismaClient({ adapter })
const prisma = new PrismaClient({ adapter });
async function run() {
const complexesUsingPlans = await prisma.complex.count({
where: { planCode: { not: null } },
})
});
if (complexesUsingPlans > 0) {
throw new Error(
`No se puede resetear planes: hay ${complexesUsingPlans} complejos con plan asignado.`,
)
`No se puede resetear planes: hay ${complexesUsingPlans} complejos con plan asignado.`
);
}
await prisma.$transaction(async (tx) => {
await tx.plan.deleteMany({})
await tx.plan.deleteMany({});
for (const plan of planSeeds) {
await tx.plan.create({
@@ -37,19 +37,19 @@ async function run() {
price: plan.price,
rules: plan.rules,
},
})
});
}
})
});
console.log('Planes reseteados: BASIC, ADVANCED, ENTERPRISE')
console.log('Planes reseteados: BASIC, ADVANCED, ENTERPRISE');
}
run()
.then(async () => {
await prisma.$disconnect()
await prisma.$disconnect();
})
.catch(async (error) => {
console.error(error)
await prisma.$disconnect()
process.exit(1)
})
console.error(error);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -1,21 +1,21 @@
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import { v7 as uuidv7 } from 'uuid'
import { PrismaClient } from '../src/generated/prisma/client'
import { planSeeds } from './plans.seed-data'
import { sportSeeds } from './sports.seed-data'
import 'dotenv/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { v7 as uuidv7 } from 'uuid';
import { PrismaClient } from '../src/generated/prisma/client';
import { planSeeds } from './plans.seed-data';
import { sportSeeds } from './sports.seed-data';
const databaseUrl = process.env.DATABASE_URL
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('Missing DATABASE_URL in environment for seeding.')
throw new Error('Missing DATABASE_URL in environment for seeding.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
})
});
const prisma = new PrismaClient({ adapter })
const prisma = new PrismaClient({ adapter });
async function run() {
for (const plan of planSeeds) {
@@ -33,7 +33,7 @@ async function run() {
price: plan.price,
rules: plan.rules,
},
})
});
}
for (const sport of sportSeeds) {
@@ -49,16 +49,16 @@ async function run() {
slug: sport.slug,
isActive: true,
},
})
});
}
}
run()
.then(async () => {
await prisma.$disconnect()
await prisma.$disconnect();
})
.catch(async (error) => {
console.error(error)
await prisma.$disconnect()
process.exit(1)
})
console.error(error);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -5,4 +5,4 @@ export const sportSeeds = [
{ name: 'Pickleball', slug: 'pickleball' },
{ name: 'Basquet', slug: 'basquet' },
{ name: 'Basquet 3', slug: 'basquet-3' },
] as const
] as const;

View File

@@ -1,11 +1 @@
model User {
id String @id @db.Uuid
supabaseUserId String @unique @map("supabase_user_id") @db.Uuid
email String @unique
fullName String @map("full_name")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complexes ComplexUser[]
@@map("users")
}
// User model is defined in auth.prisma (Better Auth schema).

View File

@@ -1,36 +1,41 @@
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from '@/lib/logger'
import type { AppEnv } from '@/types/hono'
import { requestId } from 'hono/request-id'
import { logger } from '@/lib/logger';
import type { AppEnv } from '@/types/hono';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { requestId } from 'hono/request-id';
import { errorHandler } from './lib/http/error-handler';
import { requestIdMiddleware } from './lib/http/request-id';
export function createApp() {
const app = new Hono<AppEnv>()
const app = new Hono<AppEnv>();
const allowedOrigins = (
Bun.env.CORS_ORIGIN ??
'http://localhost:5173,http://127.0.0.1:5173'
)
app.use('*', requestIdMiddleware);
app.use('*', errorHandler);
const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
.split(',')
.map((value) => value.trim())
.filter(Boolean)
.filter(Boolean);
app.use(
'*',
cors({
origin: (origin) => {
if (!origin) return allowedOrigins[0] ?? ''
return allowedOrigins.includes(origin) ? origin : ''
if (!origin) return allowedOrigins[0] ?? '';
return allowedOrigins.includes(origin) ? origin : '';
},
allowHeaders: ['Authorization', 'Content-Type'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
}),
)
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true,
})
);
app.use('*', async (c, next) => {
const start = performance.now()
await next()
const durationMs = Number((performance.now() - start).toFixed(1))
const start = performance.now();
await next();
const durationMs = Number((performance.now() - start).toFixed(1));
logger.info(
{
@@ -38,11 +43,11 @@ export function createApp() {
path: c.req.path,
status: c.res.status,
durationMs,
requestId
requestId,
},
'http_request',
)
})
'http_request'
);
});
return app
return app;
}

View File

@@ -0,0 +1,288 @@
type BookingEmailData = {
bookingCode: string;
complexName: string;
date: string;
startTime: string;
endTime: string;
courtName: string;
sportName: string;
customerName: string;
price?: number;
};
function formatFriendlyDate(isoDate: string): string {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
if (!match) return isoDate;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'long' }).format(date);
const day = date.getDate();
const month = new Intl.DateTimeFormat('es-AR', { month: 'long' }).format(date);
return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${day} de ${month}`;
}
const BASE_STYLES = `
body {
margin: 0;
padding: 0;
background-color: #f4f4f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
* {
box-sizing: border-box;
}
`;
function wrapLayout(content: string) {
return `<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playzer</title>
<style>${BASE_STYLES}</style>
</head>
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;min-height:100vh;">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
${content}
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
function headerSection() {
return `
<tr>
<td style="background-color:#0a1628;padding:32px 32px 28px;text-align:center;">
<span style="font-size:24px;font-weight:700;color:#10b981;letter-spacing:-0.3px;">Playzer</span>
</td>
</tr>`;
}
function divider() {
return `
<tr>
<td style="padding:0 32px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="height:1px;background-color:#e5e7eb;line-height:1px;font-size:1px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>`;
}
function dateTimeBlock(date: string, startTime: string, endTime: string): string {
const friendlyDate = formatFriendlyDate(date);
return `
<tr>
<td style="padding:0 32px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:10px;border:1px solid #bbf7d0;">
<tr>
<td style="padding:20px 24px;text-align:center;">
<p style="margin:0;font-size:16px;font-weight:700;color:#065f46;line-height:1.4;">
${friendlyDate}
</p>
<p style="margin:8px 0 0;font-size:22px;font-weight:800;color:#059669;letter-spacing:-0.3px;">
${startTime} &mdash; ${endTime}
</p>
</td>
</tr>
</table>
</td>
</tr>`;
}
export function bookingConfirmationHtml(data: BookingEmailData): string {
const formattedPrice =
data.price !== undefined && data.price > 0
? new Intl.NumberFormat('es-AR', {
style: 'currency',
currency: 'ARS',
maximumFractionDigits: Number.isInteger(data.price) ? 0 : 2,
}).format(data.price)
: undefined;
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#111827;text-align:center;">
Reserva confirmada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Código', `<span style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</span>`)}
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
${formattedPrice ? detailRow('Precio', formattedPrice) : ''}
</table>
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:13px;color:#6b7280;text-align:center;line-height:1.5;">
Presentá el código de reserva al llegar al complejo.
<br />
Si necesitás cancelar o modificar, contactate directamente con el complejo.
</p>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
return wrapLayout(content);
}
export function bookingCancelledHtml(data: BookingEmailData): string {
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#dc2626;text-align:center;">
Reserva cancelada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue cancelada. Si tenés dudas, contactate con el complejo.
</p>
</td>
</tr>
<tr>
<td style="padding:0 32px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
</table>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
return wrapLayout(content);
}
export function bookingNoShowHtml(data: BookingEmailData): string {
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#d97706;text-align:center;">
Reserva no concretada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue registrada como no concretada por falta de asistencia.
</p>
</td>
</tr>
<tr>
<td style="padding:0 32px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
</table>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
return wrapLayout(content);
}
function detailRow(label: string, value: string): string {
return `
<tr>
<td style="padding:6px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="width:100px;font-size:13px;color:#6b7280;vertical-align:top;padding:2px 0;">
${label}
</td>
<td style="font-size:13px;font-weight:600;color:#111827;vertical-align:top;padding:2px 0;">
${value}
</td>
</tr>
</table>
</td>
</tr>`;
}

View File

@@ -17,6 +17,26 @@ import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model User
*
*/
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
*
@@ -27,6 +47,11 @@ export type Complex = Prisma.ComplexModel
*
*/
export type ComplexUser = Prisma.ComplexUserModel
/**
* Model ComplexInvitation
*
*/
export type ComplexInvitation = Prisma.ComplexInvitationModel
/**
* Model Sport
*
@@ -53,17 +78,17 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
*/
export type CourtBooking = Prisma.CourtBookingModel
/**
* Model OnboardingRequest
* Model CourtBookingLog
*
*/
export type OnboardingRequest = Prisma.OnboardingRequestModel
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model PasswordResetRequest
*
*/
export type PasswordResetRequest = Prisma.PasswordResetRequestModel
/**
* Model Plan
*
*/
export type Plan = Prisma.PlanModel
/**
* Model User
*
*/
export type User = Prisma.UserModel

View File

@@ -31,8 +31,8 @@ export * from "./enums"
* const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Complexes
* const complexes = await prisma.complex.findMany()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
@@ -41,6 +41,26 @@ export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model User
*
*/
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
*
@@ -51,6 +71,11 @@ export type Complex = Prisma.ComplexModel
*
*/
export type ComplexUser = Prisma.ComplexUserModel
/**
* Model ComplexInvitation
*
*/
export type ComplexInvitation = Prisma.ComplexInvitationModel
/**
* Model Sport
*
@@ -77,17 +102,17 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
*/
export type CourtBooking = Prisma.CourtBookingModel
/**
* Model OnboardingRequest
* Model CourtBookingLog
*
*/
export type OnboardingRequest = Prisma.OnboardingRequestModel
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model PasswordResetRequest
*
*/
export type PasswordResetRequest = Prisma.PasswordResetRequestModel
/**
* Model Plan
*
*/
export type Plan = Prisma.PlanModel
/**
* Model User
*
*/
export type User = Prisma.UserModel

View File

@@ -14,18 +14,6 @@ import * as $Enums from "./enums"
import type * as Prisma from "./internal/prismaNamespace"
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 StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -41,6 +29,11 @@ export type StringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
@@ -72,21 +65,6 @@ export type SortOrderInput = {
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>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -105,6 +83,14 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
@@ -137,6 +123,58 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_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> = {
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
@@ -154,19 +192,6 @@ export type EnumComplexUserRoleWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedEnumComplexUserRoleFilter<$PrismaModel>
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
@@ -272,31 +297,6 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$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 JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
@@ -348,17 +348,6 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
_max?: Prisma.NestedJsonFilter<$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 NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -373,6 +362,11 @@ export type NestedStringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
@@ -398,7 +392,7 @@ export type NestedDateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -406,7 +400,10 @@ export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidWithAggregatesFilter<$PrismaModel> | string
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
@@ -423,21 +420,12 @@ export type NestedIntFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedStringWithAggregatesFilter<$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>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
@@ -482,6 +470,56 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_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> = {
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
@@ -499,19 +537,6 @@ export type NestedEnumComplexUserRoleWithAggregatesFilter<$PrismaModel = never>
_max?: Prisma.NestedEnumComplexUserRoleFilter<$PrismaModel>
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$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>
@@ -617,31 +642,6 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$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 NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,

View File

@@ -10,7 +10,8 @@
*/
export const ComplexUserRole = {
ADMIN: 'ADMIN'
ADMIN: 'ADMIN',
EMPLOYEE: 'EMPLOYEE'
} as const
export type ComplexUserRole = (typeof ComplexUserRole)[keyof typeof ComplexUserRole]
@@ -31,7 +32,9 @@ export type DayOfWeek = (typeof DayOfWeek)[keyof typeof DayOfWeek]
export const CourtBookingStatus = {
CONFIRMED: 'CONFIRMED',
CANCELLED: 'CANCELLED'
CANCELLED: 'CANCELLED',
COMPLETED: 'COMPLETED',
NOSHOW: 'NOSHOW'
} as const
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]

File diff suppressed because one or more lines are too long

View File

@@ -384,16 +384,21 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
export const ModelName = {
User: 'User',
Session: 'Session',
Account: 'Account',
Verification: 'Verification',
Complex: 'Complex',
ComplexUser: 'ComplexUser',
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
OnboardingRequest: 'OnboardingRequest',
Plan: 'Plan',
User: 'User'
CourtBookingLog: 'CourtBookingLog',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -409,10 +414,306 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "complex" | "complexUser" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "onboardingRequest" | "plan" | "user"
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "passwordResetRequest" | "plan"
txIsolationLevel: TransactionIsolationLevel
}
model: {
User: {
payload: Prisma.$UserPayload<ExtArgs>
fields: Prisma.UserFieldRefs
operations: {
findUnique: {
args: Prisma.UserFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findUniqueOrThrow: {
args: Prisma.UserFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findFirst: {
args: Prisma.UserFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findFirstOrThrow: {
args: Prisma.UserFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findMany: {
args: Prisma.UserFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
create: {
args: Prisma.UserCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
createMany: {
args: Prisma.UserCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.UserCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
delete: {
args: Prisma.UserDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
update: {
args: Prisma.UserUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
deleteMany: {
args: Prisma.UserDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.UserUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.UserUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
upsert: {
args: Prisma.UserUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
aggregate: {
args: Prisma.UserAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser>
}
groupBy: {
args: Prisma.UserGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserGroupByOutputType>[]
}
count: {
args: Prisma.UserCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserCountAggregateOutputType> | number
}
}
}
Session: {
payload: Prisma.$SessionPayload<ExtArgs>
fields: Prisma.SessionFieldRefs
operations: {
findUnique: {
args: Prisma.SessionFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload> | null
}
findUniqueOrThrow: {
args: Prisma.SessionFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
findFirst: {
args: Prisma.SessionFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload> | null
}
findFirstOrThrow: {
args: Prisma.SessionFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
findMany: {
args: Prisma.SessionFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>[]
}
create: {
args: Prisma.SessionCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
createMany: {
args: Prisma.SessionCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.SessionCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>[]
}
delete: {
args: Prisma.SessionDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
update: {
args: Prisma.SessionUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
deleteMany: {
args: Prisma.SessionDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.SessionUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.SessionUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>[]
}
upsert: {
args: Prisma.SessionUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SessionPayload>
}
aggregate: {
args: Prisma.SessionAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateSession>
}
groupBy: {
args: Prisma.SessionGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SessionGroupByOutputType>[]
}
count: {
args: Prisma.SessionCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SessionCountAggregateOutputType> | number
}
}
}
Account: {
payload: Prisma.$AccountPayload<ExtArgs>
fields: Prisma.AccountFieldRefs
operations: {
findUnique: {
args: Prisma.AccountFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AccountFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
findFirst: {
args: Prisma.AccountFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload> | null
}
findFirstOrThrow: {
args: Prisma.AccountFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
findMany: {
args: Prisma.AccountFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
create: {
args: Prisma.AccountCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
createMany: {
args: Prisma.AccountCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AccountCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
delete: {
args: Prisma.AccountDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
update: {
args: Prisma.AccountUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
deleteMany: {
args: Prisma.AccountDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AccountUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AccountUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
upsert: {
args: Prisma.AccountUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
aggregate: {
args: Prisma.AccountAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAccount>
}
groupBy: {
args: Prisma.AccountGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AccountGroupByOutputType>[]
}
count: {
args: Prisma.AccountCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AccountCountAggregateOutputType> | number
}
}
}
Verification: {
payload: Prisma.$VerificationPayload<ExtArgs>
fields: Prisma.VerificationFieldRefs
operations: {
findUnique: {
args: Prisma.VerificationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload> | null
}
findUniqueOrThrow: {
args: Prisma.VerificationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
findFirst: {
args: Prisma.VerificationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload> | null
}
findFirstOrThrow: {
args: Prisma.VerificationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
findMany: {
args: Prisma.VerificationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>[]
}
create: {
args: Prisma.VerificationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
createMany: {
args: Prisma.VerificationCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.VerificationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>[]
}
delete: {
args: Prisma.VerificationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
update: {
args: Prisma.VerificationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
deleteMany: {
args: Prisma.VerificationDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.VerificationUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.VerificationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>[]
}
upsert: {
args: Prisma.VerificationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$VerificationPayload>
}
aggregate: {
args: Prisma.VerificationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateVerification>
}
groupBy: {
args: Prisma.VerificationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.VerificationGroupByOutputType>[]
}
count: {
args: Prisma.VerificationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.VerificationCountAggregateOutputType> | number
}
}
}
Complex: {
payload: Prisma.$ComplexPayload<ExtArgs>
fields: Prisma.ComplexFieldRefs
@@ -561,6 +862,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ComplexInvitation: {
payload: Prisma.$ComplexInvitationPayload<ExtArgs>
fields: Prisma.ComplexInvitationFieldRefs
operations: {
findUnique: {
args: Prisma.ComplexInvitationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ComplexInvitationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
findFirst: {
args: Prisma.ComplexInvitationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload> | null
}
findFirstOrThrow: {
args: Prisma.ComplexInvitationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
findMany: {
args: Prisma.ComplexInvitationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>[]
}
create: {
args: Prisma.ComplexInvitationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
createMany: {
args: Prisma.ComplexInvitationCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ComplexInvitationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>[]
}
delete: {
args: Prisma.ComplexInvitationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
update: {
args: Prisma.ComplexInvitationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
deleteMany: {
args: Prisma.ComplexInvitationDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ComplexInvitationUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ComplexInvitationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>[]
}
upsert: {
args: Prisma.ComplexInvitationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexInvitationPayload>
}
aggregate: {
args: Prisma.ComplexInvitationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateComplexInvitation>
}
groupBy: {
args: Prisma.ComplexInvitationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ComplexInvitationGroupByOutputType>[]
}
count: {
args: Prisma.ComplexInvitationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ComplexInvitationCountAggregateOutputType> | number
}
}
}
Sport: {
payload: Prisma.$SportPayload<ExtArgs>
fields: Prisma.SportFieldRefs
@@ -931,77 +1306,151 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
OnboardingRequest: {
payload: Prisma.$OnboardingRequestPayload<ExtArgs>
fields: Prisma.OnboardingRequestFieldRefs
CourtBookingLog: {
payload: Prisma.$CourtBookingLogPayload<ExtArgs>
fields: Prisma.CourtBookingLogFieldRefs
operations: {
findUnique: {
args: Prisma.OnboardingRequestFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
args: Prisma.CourtBookingLogFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload> | null
}
findUniqueOrThrow: {
args: Prisma.OnboardingRequestFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
findFirst: {
args: Prisma.OnboardingRequestFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
args: Prisma.CourtBookingLogFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload> | null
}
findFirstOrThrow: {
args: Prisma.OnboardingRequestFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
findMany: {
args: Prisma.OnboardingRequestFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
args: Prisma.CourtBookingLogFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
create: {
args: Prisma.OnboardingRequestCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
createMany: {
args: Prisma.OnboardingRequestCreateManyArgs<ExtArgs>
args: Prisma.CourtBookingLogCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.OnboardingRequestCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
args: Prisma.CourtBookingLogCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
delete: {
args: Prisma.OnboardingRequestDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
update: {
args: Prisma.OnboardingRequestUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
deleteMany: {
args: Prisma.OnboardingRequestDeleteManyArgs<ExtArgs>
args: Prisma.CourtBookingLogDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.OnboardingRequestUpdateManyArgs<ExtArgs>
args: Prisma.CourtBookingLogUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.OnboardingRequestUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
args: Prisma.CourtBookingLogUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
upsert: {
args: Prisma.OnboardingRequestUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
args: Prisma.CourtBookingLogUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
aggregate: {
args: Prisma.OnboardingRequestAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateOnboardingRequest>
args: Prisma.CourtBookingLogAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateCourtBookingLog>
}
groupBy: {
args: Prisma.OnboardingRequestGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestGroupByOutputType>[]
args: Prisma.CourtBookingLogGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtBookingLogGroupByOutputType>[]
}
count: {
args: Prisma.OnboardingRequestCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestCountAggregateOutputType> | number
args: Prisma.CourtBookingLogCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtBookingLogCountAggregateOutputType> | number
}
}
}
PasswordResetRequest: {
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
fields: Prisma.PasswordResetRequestFieldRefs
operations: {
findUnique: {
args: Prisma.PasswordResetRequestFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload> | null
}
findUniqueOrThrow: {
args: Prisma.PasswordResetRequestFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
findFirst: {
args: Prisma.PasswordResetRequestFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload> | null
}
findFirstOrThrow: {
args: Prisma.PasswordResetRequestFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
findMany: {
args: Prisma.PasswordResetRequestFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>[]
}
create: {
args: Prisma.PasswordResetRequestCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
createMany: {
args: Prisma.PasswordResetRequestCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.PasswordResetRequestCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>[]
}
delete: {
args: Prisma.PasswordResetRequestDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
update: {
args: Prisma.PasswordResetRequestUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
deleteMany: {
args: Prisma.PasswordResetRequestDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.PasswordResetRequestUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.PasswordResetRequestUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>[]
}
upsert: {
args: Prisma.PasswordResetRequestUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$PasswordResetRequestPayload>
}
aggregate: {
args: Prisma.PasswordResetRequestAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregatePasswordResetRequest>
}
groupBy: {
args: Prisma.PasswordResetRequestGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.PasswordResetRequestGroupByOutputType>[]
}
count: {
args: Prisma.PasswordResetRequestCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.PasswordResetRequestCountAggregateOutputType> | number
}
}
}
@@ -1079,80 +1528,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
User: {
payload: Prisma.$UserPayload<ExtArgs>
fields: Prisma.UserFieldRefs
operations: {
findUnique: {
args: Prisma.UserFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findUniqueOrThrow: {
args: Prisma.UserFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findFirst: {
args: Prisma.UserFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findFirstOrThrow: {
args: Prisma.UserFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findMany: {
args: Prisma.UserFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
create: {
args: Prisma.UserCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
createMany: {
args: Prisma.UserCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.UserCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
delete: {
args: Prisma.UserDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
update: {
args: Prisma.UserUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
deleteMany: {
args: Prisma.UserDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.UserUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.UserUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
upsert: {
args: Prisma.UserUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
aggregate: {
args: Prisma.UserAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser>
}
groupBy: {
args: Prisma.UserGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserGroupByOutputType>[]
}
count: {
args: Prisma.UserCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
@@ -1192,10 +1567,73 @@ export const TransactionIsolationLevel = runtime.makeStrictEnum({
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
} as const
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 = {
id: 'id',
complexName: 'complexName',
physicalAddress: 'physicalAddress',
city: 'city',
state: 'state',
country: 'country',
complexSlug: 'complexSlug',
adminEmail: 'adminEmail',
planCode: 'planCode',
@@ -1216,6 +1654,21 @@ export const ComplexUserScalarFieldEnum = {
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',
updatedAt: 'updatedAt'
} as const
export type ComplexInvitationScalarFieldEnum = (typeof ComplexInvitationScalarFieldEnum)[keyof typeof ComplexInvitationScalarFieldEnum]
export const SportScalarFieldEnum = {
id: 'id',
name: 'name',
@@ -1278,6 +1731,7 @@ export const CourtBookingScalarFieldEnum = {
endTime: 'endTime',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
@@ -1286,22 +1740,38 @@ export const CourtBookingScalarFieldEnum = {
export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum]
export const OnboardingRequestScalarFieldEnum = {
export const CourtBookingLogScalarFieldEnum = {
id: 'id',
bookingCode: 'bookingCode',
courtId: 'courtId',
bookingDate: 'bookingDate',
startTime: 'startTime',
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
changedAt: 'changedAt'
} as const
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
email: 'email',
otpHash: 'otpHash',
otpExpiresAt: 'otpExpiresAt',
otpAttempts: 'otpAttempts',
otpLastSentAt: 'otpLastSentAt',
otpResendCount: 'otpResendCount',
emailVerifiedAt: 'emailVerifiedAt',
completedAt: 'completedAt',
usedAt: 'usedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
export type PasswordResetRequestScalarFieldEnum = (typeof PasswordResetRequestScalarFieldEnum)[keyof typeof PasswordResetRequestScalarFieldEnum]
export const PlanScalarFieldEnum = {
@@ -1315,18 +1785,6 @@ export const PlanScalarFieldEnum = {
export type PlanScalarFieldEnum = (typeof PlanScalarFieldEnum)[keyof typeof PlanScalarFieldEnum]
export const UserScalarFieldEnum = {
id: 'id',
supabaseUserId: 'supabaseUserId',
email: 'email',
fullName: 'fullName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -1387,6 +1845,13 @@ export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaMod
/**
* Reference to a field of type 'Boolean'
*/
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
/**
* Reference to a field of type 'DateTime'
*/
@@ -1415,13 +1880,6 @@ export type ListEnumComplexUserRoleFieldRefInput<$PrismaModel> = FieldRefInputTy
/**
* Reference to a field of type 'Boolean'
*/
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
/**
* Reference to a field of type 'Int'
*/
@@ -1600,16 +2058,21 @@ export type PrismaClientOptions = ({
comments?: runtime.SqlCommenterPlugin[]
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
session?: Prisma.SessionOmit
account?: Prisma.AccountOmit
verification?: Prisma.VerificationOmit
complex?: Prisma.ComplexOmit
complexUser?: Prisma.ComplexUserOmit
complexInvitation?: Prisma.ComplexInvitationOmit
sport?: Prisma.SportOmit
court?: Prisma.CourtOmit
courtAvailability?: Prisma.CourtAvailabilityOmit
courtPriceRule?: Prisma.CourtPriceRuleOmit
courtBooking?: Prisma.CourtBookingOmit
onboardingRequest?: Prisma.OnboardingRequestOmit
courtBookingLog?: Prisma.CourtBookingLogOmit
passwordResetRequest?: Prisma.PasswordResetRequestOmit
plan?: Prisma.PlanOmit
user?: Prisma.UserOmit
}
/* Types for Logging */

View File

@@ -51,16 +51,21 @@ export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Session: 'Session',
Account: 'Account',
Verification: 'Verification',
Complex: 'Complex',
ComplexUser: 'ComplexUser',
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
OnboardingRequest: 'OnboardingRequest',
Plan: 'Plan',
User: 'User'
CourtBookingLog: 'CourtBookingLog',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -79,10 +84,73 @@ export const TransactionIsolationLevel = runtime.makeStrictEnum({
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
} as const
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 = {
id: 'id',
complexName: 'complexName',
physicalAddress: 'physicalAddress',
city: 'city',
state: 'state',
country: 'country',
complexSlug: 'complexSlug',
adminEmail: 'adminEmail',
planCode: 'planCode',
@@ -103,6 +171,21 @@ export const ComplexUserScalarFieldEnum = {
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',
updatedAt: 'updatedAt'
} as const
export type ComplexInvitationScalarFieldEnum = (typeof ComplexInvitationScalarFieldEnum)[keyof typeof ComplexInvitationScalarFieldEnum]
export const SportScalarFieldEnum = {
id: 'id',
name: 'name',
@@ -165,6 +248,7 @@ export const CourtBookingScalarFieldEnum = {
endTime: 'endTime',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
@@ -173,22 +257,38 @@ export const CourtBookingScalarFieldEnum = {
export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum]
export const OnboardingRequestScalarFieldEnum = {
export const CourtBookingLogScalarFieldEnum = {
id: 'id',
bookingCode: 'bookingCode',
courtId: 'courtId',
bookingDate: 'bookingDate',
startTime: 'startTime',
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
changedAt: 'changedAt'
} as const
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
email: 'email',
otpHash: 'otpHash',
otpExpiresAt: 'otpExpiresAt',
otpAttempts: 'otpAttempts',
otpLastSentAt: 'otpLastSentAt',
otpResendCount: 'otpResendCount',
emailVerifiedAt: 'emailVerifiedAt',
completedAt: 'completedAt',
usedAt: 'usedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
export type PasswordResetRequestScalarFieldEnum = (typeof PasswordResetRequestScalarFieldEnum)[keyof typeof PasswordResetRequestScalarFieldEnum]
export const PlanScalarFieldEnum = {
@@ -202,18 +302,6 @@ export const PlanScalarFieldEnum = {
export type PlanScalarFieldEnum = (typeof PlanScalarFieldEnum)[keyof typeof PlanScalarFieldEnum]
export const UserScalarFieldEnum = {
id: 'id',
supabaseUserId: 'supabaseUserId',
email: 'email',
fullName: 'fullName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'

View File

@@ -8,14 +8,19 @@
*
* 🟢 You can import this file directly.
*/
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/ComplexUser'
export type * from './models/ComplexInvitation'
export type * from './models/Sport'
export type * from './models/Court'
export type * from './models/CourtAvailability'
export type * from './models/CourtPriceRule'
export type * from './models/CourtBooking'
export type * from './models/OnboardingRequest'
export type * from './models/CourtBookingLog'
export type * from './models/PasswordResetRequest'
export type * from './models/Plan'
export type * from './models/User'
export type * from './commonInputTypes'

View File

@@ -28,6 +28,9 @@ export type ComplexMinAggregateOutputType = {
id: string | null
complexName: string | null
physicalAddress: string | null
city: string | null
state: string | null
country: string | null
complexSlug: string | null
adminEmail: string | null
planCode: string | null
@@ -39,6 +42,9 @@ export type ComplexMaxAggregateOutputType = {
id: string | null
complexName: string | null
physicalAddress: string | null
city: string | null
state: string | null
country: string | null
complexSlug: string | null
adminEmail: string | null
planCode: string | null
@@ -50,6 +56,9 @@ export type ComplexCountAggregateOutputType = {
id: number
complexName: number
physicalAddress: number
city: number
state: number
country: number
complexSlug: number
adminEmail: number
planCode: number
@@ -63,6 +72,9 @@ export type ComplexMinAggregateInputType = {
id?: true
complexName?: true
physicalAddress?: true
city?: true
state?: true
country?: true
complexSlug?: true
adminEmail?: true
planCode?: true
@@ -74,6 +86,9 @@ export type ComplexMaxAggregateInputType = {
id?: true
complexName?: true
physicalAddress?: true
city?: true
state?: true
country?: true
complexSlug?: true
adminEmail?: true
planCode?: true
@@ -85,6 +100,9 @@ export type ComplexCountAggregateInputType = {
id?: true
complexName?: true
physicalAddress?: true
city?: true
state?: true
country?: true
complexSlug?: true
adminEmail?: true
planCode?: true
@@ -169,6 +187,9 @@ export type ComplexGroupByOutputType = {
id: string
complexName: string
physicalAddress: string | null
city: string | null
state: string | null
country: string | null
complexSlug: string
adminEmail: string
planCode: string | null
@@ -201,6 +222,9 @@ export type ComplexWhereInput = {
id?: Prisma.UuidFilter<"Complex"> | string
complexName?: Prisma.StringFilter<"Complex"> | string
physicalAddress?: Prisma.StringNullableFilter<"Complex"> | string | null
city?: Prisma.StringNullableFilter<"Complex"> | string | null
state?: Prisma.StringNullableFilter<"Complex"> | string | null
country?: Prisma.StringNullableFilter<"Complex"> | string | null
complexSlug?: Prisma.StringFilter<"Complex"> | string
adminEmail?: Prisma.StringFilter<"Complex"> | string
planCode?: Prisma.StringNullableFilter<"Complex"> | string | null
@@ -208,6 +232,7 @@ export type ComplexWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Complex"> | Date | string
plan?: Prisma.XOR<Prisma.PlanNullableScalarRelationFilter, Prisma.PlanWhereInput> | null
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
}
@@ -215,6 +240,9 @@ export type ComplexOrderByWithRelationInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
physicalAddress?: Prisma.SortOrderInput | Prisma.SortOrder
city?: Prisma.SortOrderInput | Prisma.SortOrder
state?: Prisma.SortOrderInput | Prisma.SortOrder
country?: Prisma.SortOrderInput | Prisma.SortOrder
complexSlug?: Prisma.SortOrder
adminEmail?: Prisma.SortOrder
planCode?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -222,6 +250,7 @@ export type ComplexOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder
plan?: Prisma.PlanOrderByWithRelationInput
users?: Prisma.ComplexUserOrderByRelationAggregateInput
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
courts?: Prisma.CourtOrderByRelationAggregateInput
}
@@ -233,12 +262,16 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
NOT?: Prisma.ComplexWhereInput | Prisma.ComplexWhereInput[]
complexName?: Prisma.StringFilter<"Complex"> | string
physicalAddress?: Prisma.StringNullableFilter<"Complex"> | string | null
city?: Prisma.StringNullableFilter<"Complex"> | string | null
state?: Prisma.StringNullableFilter<"Complex"> | string | null
country?: Prisma.StringNullableFilter<"Complex"> | string | null
adminEmail?: Prisma.StringFilter<"Complex"> | string
planCode?: Prisma.StringNullableFilter<"Complex"> | string | null
createdAt?: Prisma.DateTimeFilter<"Complex"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Complex"> | Date | string
plan?: Prisma.XOR<Prisma.PlanNullableScalarRelationFilter, Prisma.PlanWhereInput> | null
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
}, "id" | "complexSlug">
@@ -246,6 +279,9 @@ export type ComplexOrderByWithAggregationInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
physicalAddress?: Prisma.SortOrderInput | Prisma.SortOrder
city?: Prisma.SortOrderInput | Prisma.SortOrder
state?: Prisma.SortOrderInput | Prisma.SortOrder
country?: Prisma.SortOrderInput | Prisma.SortOrder
complexSlug?: Prisma.SortOrder
adminEmail?: Prisma.SortOrder
planCode?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -263,6 +299,9 @@ export type ComplexScalarWhereWithAggregatesInput = {
id?: Prisma.UuidWithAggregatesFilter<"Complex"> | string
complexName?: Prisma.StringWithAggregatesFilter<"Complex"> | string
physicalAddress?: Prisma.StringNullableWithAggregatesFilter<"Complex"> | string | null
city?: Prisma.StringNullableWithAggregatesFilter<"Complex"> | string | null
state?: Prisma.StringNullableWithAggregatesFilter<"Complex"> | string | null
country?: Prisma.StringNullableWithAggregatesFilter<"Complex"> | string | null
complexSlug?: Prisma.StringWithAggregatesFilter<"Complex"> | string
adminEmail?: Prisma.StringWithAggregatesFilter<"Complex"> | string
planCode?: Prisma.StringNullableWithAggregatesFilter<"Complex"> | string | null
@@ -274,12 +313,16 @@ export type ComplexCreateInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
}
@@ -287,12 +330,16 @@ export type ComplexUncheckedCreateInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
}
@@ -300,12 +347,16 @@ export type ComplexUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
}
@@ -313,12 +364,16 @@ export type ComplexUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
}
@@ -326,6 +381,9 @@ export type ComplexCreateManyInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
@@ -337,6 +395,9 @@ export type ComplexUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -347,6 +408,9 @@ export type ComplexUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -358,6 +422,9 @@ export type ComplexCountOrderByAggregateInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
physicalAddress?: Prisma.SortOrder
city?: Prisma.SortOrder
state?: Prisma.SortOrder
country?: Prisma.SortOrder
complexSlug?: Prisma.SortOrder
adminEmail?: Prisma.SortOrder
planCode?: Prisma.SortOrder
@@ -369,6 +436,9 @@ export type ComplexMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
physicalAddress?: Prisma.SortOrder
city?: Prisma.SortOrder
state?: Prisma.SortOrder
country?: Prisma.SortOrder
complexSlug?: Prisma.SortOrder
adminEmail?: Prisma.SortOrder
planCode?: Prisma.SortOrder
@@ -380,6 +450,9 @@ export type ComplexMinOrderByAggregateInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
physicalAddress?: Prisma.SortOrder
city?: Prisma.SortOrder
state?: Prisma.SortOrder
country?: Prisma.SortOrder
complexSlug?: Prisma.SortOrder
adminEmail?: Prisma.SortOrder
planCode?: Prisma.SortOrder
@@ -402,18 +475,6 @@ export type ComplexOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type ComplexCreateNestedOneWithoutUsersInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutUsersInput, Prisma.ComplexUncheckedCreateWithoutUsersInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutUsersInput
@@ -428,6 +489,20 @@ export type ComplexUpdateOneRequiredWithoutUsersNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutUsersInput, Prisma.ComplexUpdateWithoutUsersInput>, Prisma.ComplexUncheckedUpdateWithoutUsersInput>
}
export type ComplexCreateNestedOneWithoutInvitationsInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutInvitationsInput, Prisma.ComplexUncheckedCreateWithoutInvitationsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutInvitationsInput
connect?: Prisma.ComplexWhereUniqueInput
}
export type ComplexUpdateOneRequiredWithoutInvitationsNestedInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutInvitationsInput, Prisma.ComplexUncheckedCreateWithoutInvitationsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutInvitationsInput
upsert?: Prisma.ComplexUpsertWithoutInvitationsInput
connect?: Prisma.ComplexWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutInvitationsInput, Prisma.ComplexUpdateWithoutInvitationsInput>, Prisma.ComplexUncheckedUpdateWithoutInvitationsInput>
}
export type ComplexCreateNestedOneWithoutCourtsInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutCourtsInput, Prisma.ComplexUncheckedCreateWithoutCourtsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutCourtsInput
@@ -488,11 +563,15 @@ export type ComplexCreateWithoutUsersInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
}
@@ -500,11 +579,15 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
}
@@ -528,11 +611,15 @@ export type ComplexUpdateWithoutUsersInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
}
@@ -540,11 +627,95 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutInvitationsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutInvitationsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutInvitationsInput = {
where: Prisma.ComplexWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexCreateWithoutInvitationsInput, Prisma.ComplexUncheckedCreateWithoutInvitationsInput>
}
export type ComplexUpsertWithoutInvitationsInput = {
update: Prisma.XOR<Prisma.ComplexUpdateWithoutInvitationsInput, Prisma.ComplexUncheckedUpdateWithoutInvitationsInput>
create: Prisma.XOR<Prisma.ComplexCreateWithoutInvitationsInput, Prisma.ComplexUncheckedCreateWithoutInvitationsInput>
where?: Prisma.ComplexWhereInput
}
export type ComplexUpdateToOneWithWhereWithoutInvitationsInput = {
where?: Prisma.ComplexWhereInput
data: Prisma.XOR<Prisma.ComplexUpdateWithoutInvitationsInput, Prisma.ComplexUncheckedUpdateWithoutInvitationsInput>
}
export type ComplexUpdateWithoutInvitationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
}
@@ -552,24 +723,32 @@ export type ComplexCreateWithoutCourtsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutCourtsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutCourtsInput = {
@@ -592,35 +771,47 @@ export type ComplexUpdateWithoutCourtsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutCourtsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutPlanInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
}
@@ -628,11 +819,15 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
}
@@ -669,6 +864,9 @@ export type ComplexScalarWhereInput = {
id?: Prisma.UuidFilter<"Complex"> | string
complexName?: Prisma.StringFilter<"Complex"> | string
physicalAddress?: Prisma.StringNullableFilter<"Complex"> | string | null
city?: Prisma.StringNullableFilter<"Complex"> | string | null
state?: Prisma.StringNullableFilter<"Complex"> | string | null
country?: Prisma.StringNullableFilter<"Complex"> | string | null
complexSlug?: Prisma.StringFilter<"Complex"> | string
adminEmail?: Prisma.StringFilter<"Complex"> | string
planCode?: Prisma.StringNullableFilter<"Complex"> | string | null
@@ -680,6 +878,9 @@ export type ComplexCreateManyPlanInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
@@ -690,11 +891,15 @@ export type ComplexUpdateWithoutPlanInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
}
@@ -702,11 +907,15 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
}
@@ -714,6 +923,9 @@ export type ComplexUncheckedUpdateManyWithoutPlanInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -727,11 +939,13 @@ export type ComplexUncheckedUpdateManyWithoutPlanInput = {
export type ComplexCountOutputType = {
users: number
invitations: number
courts: number
}
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
users?: boolean | ComplexCountOutputTypeCountUsersArgs
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
}
@@ -752,6 +966,13 @@ export type ComplexCountOutputTypeCountUsersArgs<ExtArgs extends runtime.Types.E
where?: Prisma.ComplexUserWhereInput
}
/**
* ComplexCountOutputType without action
*/
export type ComplexCountOutputTypeCountInvitationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ComplexInvitationWhereInput
}
/**
* ComplexCountOutputType without action
*/
@@ -764,6 +985,9 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
id?: boolean
complexName?: boolean
physicalAddress?: boolean
city?: boolean
state?: boolean
country?: boolean
complexSlug?: boolean
adminEmail?: boolean
planCode?: boolean
@@ -771,6 +995,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
updatedAt?: boolean
plan?: boolean | Prisma.Complex$planArgs<ExtArgs>
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complex"]>
@@ -779,6 +1004,9 @@ export type ComplexSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
id?: boolean
complexName?: boolean
physicalAddress?: boolean
city?: boolean
state?: boolean
country?: boolean
complexSlug?: boolean
adminEmail?: boolean
planCode?: boolean
@@ -791,6 +1019,9 @@ export type ComplexSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
id?: boolean
complexName?: boolean
physicalAddress?: boolean
city?: boolean
state?: boolean
country?: boolean
complexSlug?: boolean
adminEmail?: boolean
planCode?: boolean
@@ -803,6 +1034,9 @@ export type ComplexSelectScalar = {
id?: boolean
complexName?: boolean
physicalAddress?: boolean
city?: boolean
state?: boolean
country?: boolean
complexSlug?: boolean
adminEmail?: boolean
planCode?: boolean
@@ -810,10 +1044,11 @@ export type ComplexSelectScalar = {
updatedAt?: boolean
}
export type ComplexOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexName" | "physicalAddress" | "complexSlug" | "adminEmail" | "planCode" | "createdAt" | "updatedAt", ExtArgs["result"]["complex"]>
export type ComplexOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexName" | "physicalAddress" | "city" | "state" | "country" | "complexSlug" | "adminEmail" | "planCode" | "createdAt" | "updatedAt", ExtArgs["result"]["complex"]>
export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
plan?: boolean | Prisma.Complex$planArgs<ExtArgs>
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}
@@ -829,12 +1064,16 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
objects: {
plan: Prisma.$PlanPayload<ExtArgs> | null
users: Prisma.$ComplexUserPayload<ExtArgs>[]
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
courts: Prisma.$CourtPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
complexName: string
physicalAddress: string | null
city: string | null
state: string | null
country: string | null
complexSlug: string
adminEmail: string
planCode: string | null
@@ -1236,6 +1475,7 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
readonly [Symbol.toStringTag]: "PrismaPromise"
plan<T extends Prisma.Complex$planArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$planArgs<ExtArgs>>): Prisma.Prisma__PlanClient<runtime.Types.Result.GetResult<Prisma.$PlanPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -1269,6 +1509,9 @@ export interface ComplexFieldRefs {
readonly id: Prisma.FieldRef<"Complex", 'String'>
readonly complexName: Prisma.FieldRef<"Complex", 'String'>
readonly physicalAddress: Prisma.FieldRef<"Complex", 'String'>
readonly city: Prisma.FieldRef<"Complex", 'String'>
readonly state: Prisma.FieldRef<"Complex", 'String'>
readonly country: Prisma.FieldRef<"Complex", 'String'>
readonly complexSlug: Prisma.FieldRef<"Complex", 'String'>
readonly adminEmail: Prisma.FieldRef<"Complex", 'String'>
readonly planCode: Prisma.FieldRef<"Complex", 'String'>
@@ -1717,6 +1960,30 @@ export type Complex$usersArgs<ExtArgs extends runtime.Types.Extensions.InternalA
distinct?: Prisma.ComplexUserScalarFieldEnum | Prisma.ComplexUserScalarFieldEnum[]
}
/**
* Complex.invitations
*/
export type Complex$invitationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ComplexInvitation
*/
select?: Prisma.ComplexInvitationSelect<ExtArgs> | null
/**
* Omit specific fields from the ComplexInvitation
*/
omit?: Prisma.ComplexInvitationOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ComplexInvitationInclude<ExtArgs> | null
where?: Prisma.ComplexInvitationWhereInput
orderBy?: Prisma.ComplexInvitationOrderByWithRelationInput | Prisma.ComplexInvitationOrderByWithRelationInput[]
cursor?: Prisma.ComplexInvitationWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ComplexInvitationScalarFieldEnum | Prisma.ComplexInvitationScalarFieldEnum[]
}
/**
* Complex.courts
*/

View File

@@ -171,7 +171,7 @@ export type ComplexUserWhereInput = {
OR?: Prisma.ComplexUserWhereInput[]
NOT?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[]
complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -193,7 +193,7 @@ export type ComplexUserWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.ComplexUserWhereInput[]
NOT?: Prisma.ComplexUserWhereInput | Prisma.ComplexUserWhereInput[]
complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.StringFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -215,7 +215,7 @@ export type ComplexUserScalarWhereWithAggregatesInput = {
OR?: Prisma.ComplexUserScalarWhereWithAggregatesInput[]
NOT?: Prisma.ComplexUserScalarWhereWithAggregatesInput | Prisma.ComplexUserScalarWhereWithAggregatesInput[]
complexId?: Prisma.UuidWithAggregatesFilter<"ComplexUser"> | string
userId?: Prisma.UuidWithAggregatesFilter<"ComplexUser"> | string
userId?: Prisma.StringWithAggregatesFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleWithAggregatesFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeWithAggregatesFilter<"ComplexUser"> | Date | string
}
@@ -303,6 +303,48 @@ export type ComplexUserMinOrderByAggregateInput = {
createdAt?: 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 = {
create?: Prisma.XOR<Prisma.ComplexUserCreateWithoutComplexInput, Prisma.ComplexUserUncheckedCreateWithoutComplexInput> | Prisma.ComplexUserCreateWithoutComplexInput[] | Prisma.ComplexUserUncheckedCreateWithoutComplexInput[]
connectOrCreate?: Prisma.ComplexUserCreateOrConnectWithoutComplexInput | Prisma.ComplexUserCreateOrConnectWithoutComplexInput[]
@@ -349,46 +391,52 @@ export type EnumComplexUserRoleFieldUpdateOperationsInput = {
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 ComplexUserCreateWithoutUserInput = {
role?: $Enums.ComplexUserRole
createdAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutUsersInput
}
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 ComplexUserUncheckedCreateWithoutUserInput = {
complexId: string
role?: $Enums.ComplexUserRole
createdAt?: Date | string
}
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 ComplexUserCreateOrConnectWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput>
}
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 ComplexUserCreateManyUserInputEnvelope = {
data: Prisma.ComplexUserCreateManyUserInput | Prisma.ComplexUserCreateManyUserInput[]
skipDuplicates?: boolean
}
export type ComplexUserUpsertWithWhereUniqueWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
update: Prisma.XOR<Prisma.ComplexUserUpdateWithoutUserInput, Prisma.ComplexUserUncheckedUpdateWithoutUserInput>
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput>
}
export type ComplexUserUpdateWithWhereUniqueWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
data: Prisma.XOR<Prisma.ComplexUserUpdateWithoutUserInput, Prisma.ComplexUserUncheckedUpdateWithoutUserInput>
}
export type ComplexUserUpdateManyWithWhereWithoutUserInput = {
where: Prisma.ComplexUserScalarWhereInput
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutUserInput>
}
export type ComplexUserScalarWhereInput = {
AND?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
OR?: Prisma.ComplexUserScalarWhereInput[]
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 = {
@@ -429,52 +477,28 @@ export type ComplexUserUpdateManyWithWhereWithoutComplexInput = {
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutComplexInput>
}
export type ComplexUserScalarWhereInput = {
AND?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
OR?: Prisma.ComplexUserScalarWhereInput[]
NOT?: Prisma.ComplexUserScalarWhereInput | Prisma.ComplexUserScalarWhereInput[]
complexId?: Prisma.UuidFilter<"ComplexUser"> | string
userId?: Prisma.UuidFilter<"ComplexUser"> | string
role?: Prisma.EnumComplexUserRoleFilter<"ComplexUser"> | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFilter<"ComplexUser"> | Date | string
}
export type ComplexUserCreateWithoutUserInput = {
role?: $Enums.ComplexUserRole
createdAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutUsersInput
}
export type ComplexUserUncheckedCreateWithoutUserInput = {
export type ComplexUserCreateManyUserInput = {
complexId: string
role?: $Enums.ComplexUserRole
createdAt?: Date | string
}
export type ComplexUserCreateOrConnectWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput>
export type ComplexUserUpdateWithoutUserInput = {
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput
}
export type ComplexUserCreateManyUserInputEnvelope = {
data: Prisma.ComplexUserCreateManyUserInput | Prisma.ComplexUserCreateManyUserInput[]
skipDuplicates?: boolean
export type ComplexUserUncheckedUpdateWithoutUserInput = {
complexId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexUserUpsertWithWhereUniqueWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
update: Prisma.XOR<Prisma.ComplexUserUpdateWithoutUserInput, Prisma.ComplexUserUncheckedUpdateWithoutUserInput>
create: Prisma.XOR<Prisma.ComplexUserCreateWithoutUserInput, Prisma.ComplexUserUncheckedCreateWithoutUserInput>
}
export type ComplexUserUpdateWithWhereUniqueWithoutUserInput = {
where: Prisma.ComplexUserWhereUniqueInput
data: Prisma.XOR<Prisma.ComplexUserUpdateWithoutUserInput, Prisma.ComplexUserUncheckedUpdateWithoutUserInput>
}
export type ComplexUserUpdateManyWithWhereWithoutUserInput = {
where: Prisma.ComplexUserScalarWhereInput
data: Prisma.XOR<Prisma.ComplexUserUpdateManyMutationInput, Prisma.ComplexUserUncheckedUpdateManyWithoutUserInput>
export type ComplexUserUncheckedUpdateManyWithoutUserInput = {
complexId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexUserCreateManyComplexInput = {
@@ -501,30 +525,6 @@ export type ComplexUserUncheckedUpdateManyWithoutComplexInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexUserCreateManyUserInput = {
complexId: string
role?: $Enums.ComplexUserRole
createdAt?: Date | string
}
export type ComplexUserUpdateWithoutUserInput = {
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutUsersNestedInput
}
export type ComplexUserUncheckedUpdateWithoutUserInput = {
complexId?: Prisma.StringFieldUpdateOperationsInput | string
role?: Prisma.EnumComplexUserRoleFieldUpdateOperationsInput | $Enums.ComplexUserRole
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexUserUncheckedUpdateManyWithoutUserInput = {
complexId?: 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<{

View File

@@ -33,6 +33,7 @@ export type CourtBookingMinAggregateOutputType = {
endTime: string | null
customerName: string | null
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
createdAt: Date | null
updatedAt: Date | null
@@ -47,6 +48,7 @@ export type CourtBookingMaxAggregateOutputType = {
endTime: string | null
customerName: string | null
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
createdAt: Date | null
updatedAt: Date | null
@@ -61,6 +63,7 @@ export type CourtBookingCountAggregateOutputType = {
endTime: number
customerName: number
customerPhone: number
customerEmail: number
status: number
createdAt: number
updatedAt: number
@@ -77,6 +80,7 @@ export type CourtBookingMinAggregateInputType = {
endTime?: true
customerName?: true
customerPhone?: true
customerEmail?: true
status?: true
createdAt?: true
updatedAt?: true
@@ -91,6 +95,7 @@ export type CourtBookingMaxAggregateInputType = {
endTime?: true
customerName?: true
customerPhone?: true
customerEmail?: true
status?: true
createdAt?: true
updatedAt?: true
@@ -105,6 +110,7 @@ export type CourtBookingCountAggregateInputType = {
endTime?: true
customerName?: true
customerPhone?: true
customerEmail?: true
status?: true
createdAt?: true
updatedAt?: true
@@ -192,6 +198,7 @@ export type CourtBookingGroupByOutputType = {
endTime: string
customerName: string
customerPhone: string
customerEmail: string | null
status: $Enums.CourtBookingStatus
createdAt: Date
updatedAt: Date
@@ -227,6 +234,7 @@ export type CourtBookingWhereInput = {
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
@@ -242,6 +250,7 @@ export type CourtBookingOrderByWithRelationInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
status?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -261,6 +270,7 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
@@ -276,6 +286,7 @@ export type CourtBookingOrderByWithAggregationInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
status?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -296,6 +307,7 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableWithAggregatesFilter<"CourtBooking"> | string | null
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
@@ -309,6 +321,7 @@ export type CourtBookingCreateInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -324,6 +337,7 @@ export type CourtBookingUncheckedCreateInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -337,6 +351,7 @@ export type CourtBookingUpdateInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -352,6 +367,7 @@ export type CourtBookingUncheckedUpdateInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -366,6 +382,7 @@ export type CourtBookingCreateManyInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -379,6 +396,7 @@ export type CourtBookingUpdateManyMutationInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -393,6 +411,7 @@ export type CourtBookingUncheckedUpdateManyInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -423,6 +442,7 @@ export type CourtBookingCountOrderByAggregateInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -437,6 +457,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -451,6 +472,7 @@ export type CourtBookingMinOrderByAggregateInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -510,6 +532,7 @@ export type CourtBookingCreateWithoutCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -523,6 +546,7 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -566,6 +590,7 @@ export type CourtBookingScalarWhereInput = {
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
@@ -579,6 +604,7 @@ export type CourtBookingCreateManyCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
@@ -592,6 +618,7 @@ export type CourtBookingUpdateWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -605,6 +632,7 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -618,6 +646,7 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -634,6 +663,7 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
endTime?: boolean
customerName?: boolean
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -649,6 +679,7 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
endTime?: boolean
customerName?: boolean
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -664,6 +695,7 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
endTime?: boolean
customerName?: boolean
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -679,12 +711,13 @@ export type CourtBookingSelectScalar = {
endTime?: boolean
customerName?: boolean
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
}
@@ -709,6 +742,7 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
endTime: string
customerName: string
customerPhone: string
customerEmail: string | null
status: $Enums.CourtBookingStatus
createdAt: Date
updatedAt: Date
@@ -1144,6 +1178,7 @@ export interface CourtBookingFieldRefs {
readonly endTime: Prisma.FieldRef<"CourtBooking", 'String'>
readonly customerName: Prisma.FieldRef<"CourtBooking", 'String'>
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>

File diff suppressed because it is too large Load Diff

View File

@@ -339,10 +339,6 @@ export type SportScalarRelationFilter = {
isNot?: Prisma.SportWhereInput
}
export type BoolFieldUpdateOperationsInput = {
set?: boolean
}
export type SportCreateNestedOneWithoutCourtsInput = {
create?: Prisma.XOR<Prisma.SportCreateWithoutCourtsInput, Prisma.SportUncheckedCreateWithoutCourtsInput>
connectOrCreate?: Prisma.SportCreateOrConnectWithoutCourtsInput

View File

@@ -26,58 +26,76 @@ export type AggregateUser = {
export type UserMinAggregateOutputType = {
id: string | null
supabaseUserId: string | null
name: string | null
email: string | null
fullName: string | null
emailVerified: boolean | null
image: string | null
phone: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
}
export type UserMaxAggregateOutputType = {
id: string | null
supabaseUserId: string | null
name: string | null
email: string | null
fullName: string | null
emailVerified: boolean | null
image: string | null
phone: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
}
export type UserCountAggregateOutputType = {
id: number
supabaseUserId: number
name: number
email: number
fullName: number
emailVerified: number
image: number
phone: number
createdAt: number
updatedAt: number
role: number
_all: number
}
export type UserMinAggregateInputType = {
id?: true
supabaseUserId?: true
name?: true
email?: true
fullName?: true
emailVerified?: true
image?: true
phone?: true
createdAt?: true
updatedAt?: true
role?: true
}
export type UserMaxAggregateInputType = {
id?: true
supabaseUserId?: true
name?: true
email?: true
fullName?: true
emailVerified?: true
image?: true
phone?: true
createdAt?: true
updatedAt?: true
role?: true
}
export type UserCountAggregateInputType = {
id?: true
supabaseUserId?: true
name?: true
email?: true
fullName?: true
emailVerified?: true
image?: true
phone?: true
createdAt?: true
updatedAt?: true
role?: true
_all?: true
}
@@ -155,11 +173,14 @@ export type UserGroupByArgs<ExtArgs extends runtime.Types.Extensions.InternalArg
export type UserGroupByOutputType = {
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified: boolean
image: string | null
phone: string | null
createdAt: Date
updatedAt: Date
role: string
_count: UserCountAggregateOutputType | null
_min: UserMinAggregateOutputType | null
_max: UserMaxAggregateOutputType | null
@@ -184,45 +205,63 @@ export type UserWhereInput = {
AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
OR?: Prisma.UserWhereInput[]
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
id?: Prisma.UuidFilter<"User"> | string
supabaseUserId?: Prisma.UuidFilter<"User"> | string
id?: Prisma.StringFilter<"User"> | string
name?: Prisma.StringFilter<"User"> | string
email?: Prisma.StringFilter<"User"> | string
fullName?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter
}
export type UserOrderByWithRelationInput = {
id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
fullName?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
complexes?: Prisma.ComplexUserOrderByRelationAggregateInput
}
export type UserWhereUniqueInput = Prisma.AtLeast<{
id?: string
supabaseUserId?: string
email?: string
AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
OR?: Prisma.UserWhereInput[]
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
fullName?: Prisma.StringFilter<"User"> | string
name?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter
}, "id" | "supabaseUserId" | "email">
}, "id" | "email">
export type UserOrderByWithAggregationInput = {
id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
fullName?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
_count?: Prisma.UserCountOrderByAggregateInput
_max?: Prisma.UserMaxOrderByAggregateInput
_min?: Prisma.UserMinOrderByAggregateInput
@@ -232,79 +271,147 @@ export type UserScalarWhereWithAggregatesInput = {
AND?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[]
OR?: Prisma.UserScalarWhereWithAggregatesInput[]
NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[]
id?: Prisma.UuidWithAggregatesFilter<"User"> | string
supabaseUserId?: Prisma.UuidWithAggregatesFilter<"User"> | string
id?: Prisma.StringWithAggregatesFilter<"User"> | string
name?: Prisma.StringWithAggregatesFilter<"User"> | string
email?: Prisma.StringWithAggregatesFilter<"User"> | string
fullName?: Prisma.StringWithAggregatesFilter<"User"> | string
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
role?: Prisma.StringWithAggregatesFilter<"User"> | string
}
export type UserCreateInput = {
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateInput = {
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
}
export type UserUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateManyInput = {
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
}
export type UserUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
}
export type UserUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
}
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 = {
@@ -312,31 +419,48 @@ export type UserScalarRelationFilter = {
isNot?: Prisma.UserWhereInput
}
export type UserCountOrderByAggregateInput = {
id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrder
email?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type UserMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrder
email?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
export type BoolFieldUpdateOperationsInput = {
set?: boolean
}
export type UserMinOrderByAggregateInput = {
id?: Prisma.SortOrder
supabaseUserId?: Prisma.SortOrder
email?: Prisma.SortOrder
fullName?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type UserCreateNestedOneWithoutSessionsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSessionsInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutSessionsNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSessionsInput
upsert?: Prisma.UserUpsertWithoutSessionsInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutSessionsInput, Prisma.UserUpdateWithoutSessionsInput>, Prisma.UserUncheckedUpdateWithoutSessionsInput>
}
export type UserCreateNestedOneWithoutAccountsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutAccountsInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutAccountsNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutAccountsInput
upsert?: Prisma.UserUpsertWithoutAccountsInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAccountsInput, Prisma.UserUpdateWithoutAccountsInput>, Prisma.UserUncheckedUpdateWithoutAccountsInput>
}
export type UserCreateNestedOneWithoutComplexesInput = {
@@ -353,22 +477,176 @@ export type UserUpdateOneRequiredWithoutComplexesNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutComplexesInput, Prisma.UserUpdateWithoutComplexesInput>, Prisma.UserUncheckedUpdateWithoutComplexesInput>
}
export type UserCreateWithoutComplexesInput = {
export type UserCreateWithoutSessionsInput = {
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutSessionsInput = {
id: string
name: string
email: string
emailVerified?: boolean
image?: string | null
phone?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutSessionsInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
}
export type UserUpsertWithoutSessionsInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutSessionsInput, Prisma.UserUncheckedUpdateWithoutSessionsInput>
create: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutSessionsInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutSessionsInput, Prisma.UserUncheckedUpdateWithoutSessionsInput>
}
export type UserUpdateWithoutSessionsInput = {
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
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutSessionsInput = {
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
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutAccountsInput = {
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
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutAccountsInput = {
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
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutAccountsInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
}
export type UserUpsertWithoutAccountsInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutAccountsInput, Prisma.UserUncheckedUpdateWithoutAccountsInput>
create: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutAccountsInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutAccountsInput, Prisma.UserUncheckedUpdateWithoutAccountsInput>
}
export type UserUpdateWithoutAccountsInput = {
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
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutAccountsInput = {
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
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
supabaseUserId: string
name: string
email: string
fullName: 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 = {
@@ -389,20 +667,30 @@ export type UserUpdateToOneWithWhereWithoutComplexesInput = {
export type UserUpdateWithoutComplexesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
supabaseUserId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: 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
}
@@ -411,10 +699,14 @@ export type UserUncheckedUpdateWithoutComplexesInput = {
*/
export type UserCountOutputType = {
sessions: number
accounts: number
complexes: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
complexes?: boolean | UserCountOutputTypeCountComplexesArgs
}
@@ -428,6 +720,20 @@ export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensi
select?: Prisma.UserCountOutputTypeSelect<ExtArgs> | null
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountSessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.SessionWhereInput
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.AccountWhereInput
}
/**
* UserCountOutputType without action
*/
@@ -438,44 +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<{
id?: boolean
supabaseUserId?: boolean
name?: boolean
email?: boolean
fullName?: boolean
emailVerified?: boolean
image?: boolean
phone?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
complexes?: boolean | Prisma.User$complexesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
supabaseUserId?: boolean
name?: boolean
email?: boolean
fullName?: boolean
emailVerified?: boolean
image?: boolean
phone?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]>
export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
supabaseUserId?: boolean
name?: boolean
email?: boolean
fullName?: boolean
emailVerified?: boolean
image?: boolean
phone?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]>
export type UserSelectScalar = {
id?: boolean
supabaseUserId?: boolean
name?: boolean
email?: boolean
fullName?: boolean
emailVerified?: boolean
image?: boolean
phone?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "supabaseUserId" | "email" | "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> = {
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
complexes?: boolean | Prisma.User$complexesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
@@ -485,15 +807,20 @@ export type UserIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "User"
objects: {
sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[]
complexes: Prisma.$ComplexUserPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
supabaseUserId: string
name: string
email: string
fullName: string
emailVerified: boolean
image: string | null
phone: string | null
createdAt: Date
updatedAt: Date
role: string
}, ExtArgs["result"]["user"]>
composites: {}
}
@@ -888,6 +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> {
readonly [Symbol.toStringTag]: "PrismaPromise"
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>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -919,11 +1248,14 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
*/
export interface UserFieldRefs {
readonly id: Prisma.FieldRef<"User", 'String'>
readonly supabaseUserId: Prisma.FieldRef<"User", 'String'>
readonly name: Prisma.FieldRef<"User", 'String'>
readonly email: Prisma.FieldRef<"User", 'String'>
readonly fullName: Prisma.FieldRef<"User", 'String'>
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
readonly image: Prisma.FieldRef<"User", 'String'>
readonly phone: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly role: Prisma.FieldRef<"User", 'String'>
}
@@ -1316,6 +1648,54 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* User.sessions
*/
export type User$sessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Session
*/
select?: Prisma.SessionSelect<ExtArgs> | null
/**
* Omit specific fields from the Session
*/
omit?: Prisma.SessionOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.SessionInclude<ExtArgs> | null
where?: Prisma.SessionWhereInput
orderBy?: Prisma.SessionOrderByWithRelationInput | Prisma.SessionOrderByWithRelationInput[]
cursor?: Prisma.SessionWhereUniqueInput
take?: number
skip?: number
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
*/

View File

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

View File

@@ -0,0 +1,59 @@
import { dash } from '@better-auth/infra';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { openAPI } from 'better-auth/plugins';
import { sendMail } from './mailer';
import { db } from './prisma';
export const auth = betterAuth({
database: prismaAdapter(db, {
provider: 'postgresql',
}),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
session: {
cookieCache: {
enabled: false,
},
},
emailAndPassword: {
enabled: true,
},
user: {
additionalFields: {
phone: {
type: 'string',
required: false,
},
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
const verificationUrl = new URL(url);
const appUrl = process.env.APP_BASE_URL ?? 'http://localhost:5173';
verificationUrl.searchParams.set('callbackURL', appUrl);
await sendMail({
to: user.email,
subject: 'Verificá tu email en Playzer',
html: `Hacé click para verificar tu email: <a href="${verificationUrl.toString()}">${verificationUrl.toString()}</a>`,
text: `Hacé click para verificar tu email: ${verificationUrl.toString()}`,
});
},
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
trustedOrigins: [
process.env.APP_BASE_URL ?? 'http://localhost:5173',
process.env.BETTER_AUTH_URL ?? 'http://localhost:3000',
],
plugins: [openAPI(), dash()],
});
export type Session = typeof auth.$Infer.Session.session;
export type User = typeof auth.$Infer.Session.user;

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,6 +1,6 @@
import pino from 'pino'
import pino from 'pino';
const isProd = Bun.env.NODE_ENV === 'production'
const isProd = Bun.env.NODE_ENV === 'production';
export const logger = pino({
level: Bun.env.LOG_LEVEL ?? (isProd ? 'info' : 'debug'),
@@ -15,4 +15,4 @@ export const logger = pino({
ignore: 'pid,hostname',
},
},
})
});

View File

@@ -1,44 +1,39 @@
import nodemailer from 'nodemailer'
import { MailtrapClientConfig, MailtrapTransport } from 'mailtrap';
import nodemailer from 'nodemailer';
let cachedTransporter: ReturnType<typeof nodemailer.createTransport> | null = null
const TOKEN = Bun.env.MAILTRAP_API_TOKEN;
let cachedTransporter: ReturnType<typeof nodemailer.createTransport> | null = null;
function getTransporter() {
if (cachedTransporter) return cachedTransporter
if (cachedTransporter) return cachedTransporter;
const smtpHost = Bun.env.SMTP_HOST
const smtpPort = Number(Bun.env.SMTP_PORT ?? 587)
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.',
)
if (!TOKEN) {
throw new Error('Missing MAILTRAP_API_TOKEN env var.');
}
cachedTransporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: smtpUser,
pass: smtpPass,
},
})
const config: MailtrapClientConfig = {
token: TOKEN,
sandbox: Bun.env.MAIL_SANDBOX === 'true',
testInboxId: Bun.env.MAIL_INBOX_ID ? Number(Bun.env.MAIL_INBOX_ID) : undefined,
};
return cachedTransporter
console.log(JSON.stringify(config, null, 2));
cachedTransporter = nodemailer.createTransport(MailtrapTransport(config));
return cachedTransporter;
}
export async function sendMail(input: {
to: string
subject: string
html: string
text: string
to: string;
subject: string;
html: string;
text: string;
}) {
const smtpFrom = Bun.env.SMTP_FROM
const smtpFrom = Bun.env.SMTP_FROM;
if (!smtpFrom) {
throw new Error('Missing SMTP_FROM env var.')
throw new Error('Missing SMTP_FROM env var.');
}
await getTransporter().sendMail({
@@ -47,5 +42,5 @@ export async function sendMail(input: {
subject: input.subject,
html: input.html,
text: input.text,
})
});
}

View File

@@ -1,50 +1,50 @@
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '@/generated/prisma/client'
import { PrismaClient } from '@/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
function hasExpectedDelegates(client: PrismaClient) {
const prismaClient = client as unknown as {
court?: { findMany?: unknown }
sport?: { findMany?: unknown }
}
court?: { findMany?: unknown };
sport?: { findMany?: unknown };
};
return (
typeof prismaClient.court?.findMany === 'function' &&
typeof prismaClient.sport?.findMany === 'function'
)
);
}
function buildPrismaClient() {
const databaseUrl = Bun.env.DATABASE_URL
const databaseUrl = Bun.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('Missing DATABASE_URL in backend environment.')
throw new Error('Missing DATABASE_URL in backend environment.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
})
});
return new PrismaClient({ adapter })
return new PrismaClient({ adapter });
}
export function getPrismaClient() {
if (globalForPrisma.prisma && hasExpectedDelegates(globalForPrisma.prisma)) {
return globalForPrisma.prisma
return globalForPrisma.prisma;
}
if (globalForPrisma.prisma && !hasExpectedDelegates(globalForPrisma.prisma)) {
void globalForPrisma.prisma.$disconnect()
void globalForPrisma.prisma.$disconnect();
}
const prisma = buildPrismaClient()
const prisma = buildPrismaClient();
if (Bun.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
globalForPrisma.prisma = prisma;
}
return prisma
return prisma;
}
export const db = getPrismaClient()
export const db = getPrismaClient();

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,26 +0,0 @@
import { createClient } from '@supabase/supabase-js'
let cachedClient: ReturnType<typeof createClient> | null = null
export function getSupabaseAdminClient() {
if (cachedClient) return cachedClient
const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL
const supabaseServiceRoleKey = Bun.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error(
'Missing Supabase admin env vars. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in backend environment.',
)
}
cachedClient = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
})
return cachedClient
}

View File

@@ -1,19 +0,0 @@
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL
const supabaseAnonKey =
Bun.env.SUPABASE_ANON_KEY ?? Bun.env.VITE_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing Supabase env vars. Set SUPABASE_URL/SUPABASE_ANON_KEY (or VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY) in backend environment.',
)
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
})

View File

@@ -1,50 +1,18 @@
import type { MiddlewareHandler } from 'hono'
import { db } from '@/lib/prisma'
import { supabase } from '@/lib/supabase'
import type { AppEnv } from '@/types/hono'
import { auth } from '@/lib/auth';
import type { AppEnv } from '@/types/hono';
import { createMiddleware } from 'hono/factory';
function getBearerToken(value: string | undefined): string | null {
if (!value) return null
const [scheme, token] = value.split(' ')
if (scheme?.toLowerCase() !== 'bearer' || !token) return null
return token
}
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
export const requireAuth: MiddlewareHandler<AppEnv> = async (
c,
next,
) => {
const token = getBearerToken(c.req.header('authorization'))
if (!token) {
return c.json({ message: 'Missing bearer token.' }, 403)
if (!session) {
return c.json({ message: 'Unauthorized' }, 401);
}
const { data, error } = await supabase.auth.getUser(token)
c.set('user', session.user);
c.set('session', session.session);
if (error || !data.user) {
return c.json({ message: 'Invalid or expired token.' }, 403)
}
const email = data.user.email?.toLowerCase()
if (!email) {
return c.json({ message: 'Auth user does not contain email.' }, 403)
}
const appUser = await db.user.findFirst({
where: {
supabaseUserId: data.user.id,
email,
},
select: { id: true },
})
if (!appUser) {
return c.json({ message: 'User not provisioned in backend.' }, 403)
}
c.set('authUser', data.user)
c.set('appUserId', appUser.id)
await next()
}
await next();
});

View File

@@ -1,35 +1,29 @@
import type { MiddlewareHandler } from 'hono'
import type { AppEnv } from '@/types/hono'
import type { AppEnv } from '@/types/hono';
import type { MiddlewareHandler } from 'hono';
function getSuperAdminEmails(): Set<string> {
const raw = Bun.env.SUPER_ADMIN_EMAILS ?? ''
const raw = Bun.env.SUPER_ADMIN_EMAILS ?? '';
const emails = raw
.split(',')
.map((value) => value.trim().toLowerCase())
.filter(Boolean)
return new Set(emails)
.filter(Boolean);
return new Set(emails);
}
export const requireSuperAdmin: MiddlewareHandler<AppEnv> = async (
c,
next,
) => {
const authUser = c.get('authUser')
const role =
typeof authUser.app_metadata?.role === 'string'
? authUser.app_metadata.role
: null
const email = authUser.email?.toLowerCase()
export const requireSuperAdmin: MiddlewareHandler<AppEnv> = async (c, next) => {
const user = c.get('user');
const role = (user as any).role || 'member';
const email = user.email.toLowerCase();
if (role === 'super_admin') {
await next()
return
await next();
return;
}
if (email && getSuperAdminEmails().has(email)) {
await next()
return
await next();
return;
}
return c.json({ message: 'Este recurso requiere permisos de super admin.' }, 403)
}
return c.json({ message: 'Este recurso requiere permisos de super admin.' }, 403);
};

View File

@@ -0,0 +1,40 @@
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {
createAdminBookingSchema,
listAdminBookingsQuerySchema,
updateAdminBookingStatusSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
export const adminBookingRoutes = new Hono<AppEnv>();
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
const bookingIdParamsSchema = z.object({ id: z.uuid() });
adminBookingRoutes.use('*', requireAuth);
adminBookingRoutes.get(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('query', listAdminBookingsQuerySchema),
listAdminBookingsHandler
);
adminBookingRoutes.post(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('json', createAdminBookingSchema),
createAdminBookingHandler
);
adminBookingRoutes.patch(
'/:id/status',
zValidator('param', bookingIdParamsSchema),
zValidator('json', updateAdminBookingStatusSchema),
updateAdminBookingStatusHandler
);

View File

@@ -0,0 +1,39 @@
import {
AdminBookingServiceError,
createAdminBooking,
} from '@/modules/admin-booking/services/admin-booking.service';
import { sendBookingConfirmation } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { CreateAdminBookingInput } from '@repo/api-contract';
type ComplexIdParams = { complexId: string };
export async function createAdminBookingHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
const user = c.get('user');
try {
const booking = await createAdminBooking(user.id, complexId, payload);
void sendBookingConfirmation({
bookingCode: booking.bookingCode,
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
courtName: booking.courtName,
sportName: booking.sport.name,
customerName: booking.customerName,
customerEmail: booking.customerEmail,
price: booking.price,
});
return c.json(booking, 201);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,24 @@
import {
AdminBookingServiceError,
listAdminBookings,
} from '@/modules/admin-booking/services/admin-booking.service';
import type { AppContext } from '@/types/hono';
import type { ListAdminBookingsQuery } from '@repo/api-contract';
type ComplexIdParams = { complexId: string };
export async function listAdminBookingsHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
const user = c.get('user');
try {
const response = await listAdminBookings(user.id, complexId, query);
return c.json(response);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,52 @@
import {
AdminBookingServiceError,
updateAdminBookingStatus,
} from '@/modules/admin-booking/services/admin-booking.service';
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
type BookingIdParams = { id: string };
export async function updateAdminBookingStatusHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BookingIdParams;
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
const user = c.get('user');
try {
const booking = await updateAdminBookingStatus(user.id, id, payload);
if (payload.status === 'CANCELLED') {
void sendBookingCancelled({
bookingCode: booking.bookingCode,
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
courtName: booking.courtName,
sportName: booking.sport.name,
customerName: booking.customerName,
customerEmail: booking.customerEmail,
});
} else if (payload.status === 'NOSHOW') {
void sendBookingNoShow({
bookingCode: booking.bookingCode,
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
courtName: booking.courtName,
sportName: booking.sport.name,
customerName: booking.customerName,
customerEmail: booking.customerEmail,
});
}
return c.json(booking);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,592 @@
import { randomInt } from 'node:crypto';
import { CourtBookingStatus } from '@/generated/prisma/enums';
import { db } from '@/lib/prisma';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { DayOfWeek } from '@repo/api-contract';
import type {
AdminBooking,
CreateAdminBookingInput,
ListAdminBookingsQuery,
UpdateAdminBookingStatusInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
type Slot = {
startTime: string;
endTime: string;
};
const DAY_OF_WEEK_BY_INDEX = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
] as const;
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
export class AdminBookingServiceError extends Error {
status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409) {
super(message);
this.name = 'AdminBookingServiceError';
this.status = status;
}
}
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
function minutesToTime(minutes: number): string {
const safeMinutes = Math.max(0, minutes);
const hours = Math.floor(safeMinutes / 60);
const mins = safeMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
function parseIsoDate(date: string): Date {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
bookingDate.getUTCFullYear() !== year ||
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
}
return bookingDate;
}
function getDayOfWeek(date: Date) {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
}
return dayOfWeek;
}
function formatIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
function buildSlots(
availability: Array<{
startTime: string;
endTime: string;
}>,
slotDurationMinutes: number
): Slot[] {
const slots: Slot[] = [];
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
for (
let current = start;
current + slotDurationMinutes <= end;
current += slotDurationMinutes
) {
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
});
}
}
return slots;
}
async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: {
select: {
id: true,
complexName: true,
plan: {
select: {
rules: true,
},
},
},
},
},
});
if (!complexUser) {
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
return complexUser.complex;
}
function resolvePrice(
court: {
basePrice: unknown;
priceRules: Array<{
dayOfWeek: string | null;
startTime: string | null;
endTime: string | null;
price: unknown;
}>;
},
dayOfWeek: string,
startTime: string,
endTime: string
): number {
const slotStart = toMinutes(startTime);
const slotEnd = toMinutes(endTime);
const matchingRules = court.priceRules
.filter((rule) => {
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
if (!rule.startTime || !rule.endTime) return true;
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
})
.sort((first, second) => {
const firstSpecificity =
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
const secondSpecificity =
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
return secondSpecificity - firstSpecificity;
});
return Number(matchingRules[0]?.price ?? court.basePrice);
}
function mapBookingResponse(booking: {
id: string;
bookingCode: string;
bookingDate: Date;
startTime: string;
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string | null;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
createdAt: Date;
updatedAt: Date;
court: {
id: string;
name: string;
complex: {
id: string;
complexName: string;
};
sport: {
id: string;
name: string;
slug: string;
};
};
price?: number;
}): AdminBooking {
return {
id: booking.id,
bookingCode: booking.bookingCode,
complexId: booking.court.complex.id,
complexName: booking.court.complex.complexName,
courtId: booking.court.id,
courtName: booking.court.name,
sport: {
id: booking.court.sport.id,
name: booking.court.sport.name,
slug: booking.court.sport.slug,
},
date: formatIsoDate(booking.bookingDate),
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail ?? undefined,
price: booking.price ?? 0,
status: booking.status,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
};
}
export async function listAdminBookings(
userId: string,
complexId: string,
query: ListAdminBookingsQuery
) {
await ensureComplexAccess(complexId, userId);
const fromDate = parseIsoDate(query.fromDate);
const bookings = await db.courtBooking.findMany({
where: {
bookingDate: {
gte: fromDate,
},
court: {
complexId,
},
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
});
const response = bookings.map((booking) => mapBookingResponse(booking));
return {
bookings: response,
};
}
export async function createAdminBooking(
userId: string,
complexId: string,
input: CreateAdminBookingInput
) {
const complex = await ensureComplexAccess(complexId, userId);
const adminUser = await db.user.findUnique({
where: { id: userId },
select: { emailVerified: true },
});
if (!adminUser?.emailVerified) {
throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
}
const bookingDate = parseIsoDate(input.date);
const dayOfWeek = getDayOfWeek(bookingDate);
const court = await db.court.findFirst({
where: {
id: input.courtId,
complexId,
},
include: {
availabilities: {
where: {
dayOfWeek,
},
orderBy: {
startTime: 'asc',
},
},
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
});
if (!court) {
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedStartMinutes = toMinutes(input.startTime);
const selectedEndMinutes = selectedStartMinutes + court.slotDurationMinutes;
const selectedSlot = {
startTime: input.startTime,
endTime: minutesToTime(selectedEndMinutes),
};
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
throw new AdminBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
409
);
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const booking = await db.$transaction(async (tx) => {
if (complex.plan) {
const rules = parsePlanRules(complex.plan.rules);
const bookingsForDate = await tx.courtBooking.count({
where: {
bookingDate,
status: 'CONFIRMED',
court: {
complexId,
},
},
});
const courtsCount = await tx.court.count({
where: {
complexId,
},
});
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
});
const maxBookingsViolation = violations.find(
(violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
);
if (maxBookingsViolation) {
throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
}
}
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId: court.id,
bookingDate,
status: 'CONFIRMED',
startTime: {
lt: selectedSlot.endTime,
},
endTime: {
gt: selectedSlot.startTime,
},
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
return tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId: court.id,
bookingDate,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() || null,
status: 'CONFIRMED',
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
});
});
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
return mapBookingResponse({ ...booking, price });
} catch (error) {
if (error instanceof AdminBookingServiceError) {
throw error;
}
const prismaError = error as {
code?: string;
meta?: {
target?: string[] | string;
};
};
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
: [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
String(target).includes('booking_code')
);
if (isBookingCodeCollision) {
continue;
}
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
throw error;
}
}
throw new AdminBookingServiceError(
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
409
);
}
export async function updateAdminBookingStatus(
userId: string,
bookingId: string,
input: UpdateAdminBookingStatusInput
) {
const booking = await db.courtBooking.findFirst({
where: {
id: bookingId,
court: {
complex: {
users: {
some: {
userId,
},
},
},
},
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
});
if (!booking) {
throw new AdminBookingServiceError('Reserva no encontrada.', 404);
}
if (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden cancelar reservas en estado confirmada.',
409
);
}
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como cumplidas las reservas confirmadas.',
409
);
}
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como no show las reservas confirmadas.',
409
);
}
await db.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: input.status,
changedAt: new Date(),
},
});
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,37 +1,80 @@
import { zValidator } from '@hono/zod-validator'
import { createComplexSchema, updateComplexSchema } from '@repo/api-contract'
import { Hono } from 'hono'
import { z } from 'zod'
import { requireAuth } from '@/middlewares/require-auth.middleware'
import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler'
import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler'
import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler'
import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler'
import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler'
import type { AppEnv } from '@/types/hono'
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 { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.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 { 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 type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {
acceptComplexInvitationsSchema,
createComplexSchema,
inviteComplexUserSchema,
updateComplexSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
export const complexRoutes = new Hono<AppEnv>()
const complexIdParamsSchema = z.object({ id: z.uuid() })
const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) })
export const complexRoutes = new Hono<AppEnv>();
const complexIdParamsSchema = z.object({ id: z.uuid() });
const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) });
complexRoutes.use('*', requireAuth)
complexRoutes.use('*', requireAuth);
complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler)
complexRoutes.get('/mine', listMyComplexesHandler)
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(
'/slug/:slug',
zValidator('param', complexSlugParamsSchema),
getComplexBySlugHandler,
)
getComplexBySlugHandler
);
complexRoutes.get(
'/:id',
'/:id/users',
zValidator('param', complexIdParamsSchema),
getComplexByIdHandler,
)
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.patch(
'/:id',
zValidator('param', complexIdParamsSchema),
zValidator('json', updateComplexSchema),
updateComplexHandler,
)
updateComplexHandler
);

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,23 +1,51 @@
import type { CreateComplexInput } from '@repo/api-contract'
import type { AppContext } from '@/types/hono'
import { createComplex } from '@/modules/complex/services/complex.service'
import { db } from '@/lib/prisma';
import { createComplex } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
import type { CreateComplexInput } from '@repo/api-contract';
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 adminEmail = authUser.email
const user = c.get('user');
let adminEmail = user?.email;
let userId = user?.id;
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({
complexName: payload.complexName,
physicalAddress: payload.physicalAddress,
adminEmail,
userId,
planCode: payload.planCode,
})
city: payload.city,
state: payload.state,
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

@@ -1,16 +1,16 @@
import type { AppContext } from '@/types/hono'
import { getComplexById } from '@/modules/complex/services/complex.service'
import { getComplexById } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
type ComplexIdParams = { id: string }
type ComplexIdParams = { id: string };
export async function getComplexByIdHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as ComplexIdParams
const { id } = c.req.valid('param' as never) as ComplexIdParams;
const complex = await getComplexById(id)
const complex = await getComplexById(id);
if (!complex) {
return c.json({ message: 'Complejo no encontrado.' }, 404)
return c.json({ message: 'Complejo no encontrado.' }, 404);
}
return c.json(complex)
return c.json(complex);
}

View File

@@ -1,16 +1,16 @@
import type { AppContext } from '@/types/hono'
import { getComplexBySlug } from '@/modules/complex/services/complex.service'
import { getComplexBySlug } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
type ComplexSlugParams = { slug: string }
type ComplexSlugParams = { slug: string };
export async function getComplexBySlugHandler(c: AppContext) {
const { slug } = c.req.valid('param' as never) as ComplexSlugParams
const { slug } = c.req.valid('param' as never) as ComplexSlugParams;
const complex = await getComplexBySlug(slug)
const complex = await getComplexBySlug(slug);
if (!complex) {
return c.json({ message: 'Complejo no encontrado.' }, 404)
return c.json({ message: 'Complejo no encontrado.' }, 404);
}
return c.json(complex)
return c.json(complex);
}

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

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

@@ -1,27 +1,27 @@
import type { UpdateComplexInput } from '@repo/api-contract'
import { Prisma } from '@/generated/prisma/client'
import type { AppContext } from '@/types/hono'
import { getComplexById, updateComplex } from '@/modules/complex/services/complex.service'
import { Prisma } from '@/generated/prisma/client';
import { getComplexById, updateComplex } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
import type { UpdateComplexInput } from '@repo/api-contract';
type ComplexIdParams = { id: string }
type ComplexIdParams = { id: string };
export async function updateComplexHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as ComplexIdParams
const payload = c.req.valid('json' as never) as UpdateComplexInput
const { id } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as UpdateComplexInput;
const existing = await getComplexById(id)
const existing = await getComplexById(id);
if (!existing) {
return c.json({ message: 'Complejo no encontrado.' }, 404)
return c.json({ message: 'Complejo no encontrado.' }, 404);
}
try {
const complex = await updateComplex(id, payload)
return c.json(complex)
const complex = await updateComplex(id, payload);
return c.json(complex);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return c.json({ message: 'No se pudo actualizar el complejo.' }, 409)
return c.json({ message: 'No se pudo actualizar el complejo.' }, 409);
}
throw error
throw error;
}
}

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

@@ -1,21 +1,67 @@
import { v7 as uuidv7 } from 'uuid'
import { Prisma } from '@/generated/prisma/client'
import { db } from '@/lib/prisma'
import { Prisma } from '@/generated/prisma/client';
import { db } from '@/lib/prisma';
import { v7 as uuidv7 } from 'uuid';
export type CreateComplexInput = {
complexName: string
physicalAddress: string
adminEmail: string
planCode?: string
complexName: string;
physicalAddress: string;
adminEmail: string;
userId?: string;
planCode?: string;
city?: string;
state?: 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 = {
complexName?: string
physicalAddress?: string | null
complexSlug?: string
adminEmail?: string
planCode?: string | null
}
complexName?: string;
physicalAddress?: string | null;
complexSlug?: string;
adminEmail?: string;
planCode?: string | null;
city?: string | null;
state?: string | null;
country?: string | null;
};
function slugify(value: string): string {
return value
@@ -25,18 +71,15 @@ function slugify(value: string): string {
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/-+/g, '-');
}
async function buildUniqueSlug(
source: string,
excludeComplexId?: string,
): Promise<string> {
const base = slugify(source)
const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`
async function buildUniqueSlug(source: string, excludeComplexId?: string): Promise<string> {
const base = slugify(source);
const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
let candidate = fallback
let index = 1
let candidate = fallback;
let index = 1;
while (true) {
const existing = await db.complex.findFirst({
@@ -51,82 +94,190 @@ async function buildUniqueSlug(
: {}),
},
select: { id: true },
})
});
if (!existing) return candidate
if (!existing) return candidate;
index += 1
candidate = `${fallback}-${index}`
index += 1;
candidate = `${fallback}-${index}`;
}
}
export async function createComplex(input: CreateComplexInput) {
const complexSlug = await buildUniqueSlug(input.complexName)
const { userId } = input;
return db.complex.create({
data: {
id: uuidv7(),
complexName: input.complexName,
physicalAddress: input.physicalAddress.trim(),
complexSlug,
adminEmail: input.adminEmail,
planCode: input.planCode,
},
})
return db.$transaction(async (tx) => {
const complexSlug = await buildUniqueSlug(input.complexName);
const complex = await tx.complex.create({
data: {
id: uuidv7(),
complexName: input.complexName,
physicalAddress: input.physicalAddress.trim(),
city: input.city?.trim() || null,
state: input.state?.trim() || null,
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;
});
}
export async function getComplexById(id: string) {
return db.complex.findUnique({
where: { id },
})
});
}
export async function getComplexBySlug(slug: string) {
return db.complex.findUnique({
where: { complexSlug: slug },
})
});
}
export async function listMyComplexes(appUserId: string) {
export async function listMyComplexes(userId: string) {
const complexUsers = await db.complexUser.findMany({
where: { userId: appUserId },
where: { userId },
include: {
complex: true,
},
orderBy: {
createdAt: 'asc',
},
})
});
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) {
const data: Record<string, unknown> = {}
const data: Record<string, unknown> = {};
if (input.complexName) {
data.complexName = input.complexName
data.complexSlug = await buildUniqueSlug(input.complexName, id)
data.complexName = input.complexName;
data.complexSlug = await buildUniqueSlug(input.complexName, id);
}
if (input.complexSlug) {
data.complexSlug = await buildUniqueSlug(input.complexSlug, id)
data.complexSlug = await buildUniqueSlug(input.complexSlug, id);
}
if (input.adminEmail) {
data.adminEmail = input.adminEmail
data.adminEmail = input.adminEmail;
}
if (input.planCode !== undefined) {
data.planCode = input.planCode
data.planCode = input.planCode;
}
if (input.physicalAddress !== undefined) {
data.physicalAddress = input.physicalAddress?.trim() ?? null
data.physicalAddress = input.physicalAddress?.trim() ?? null;
}
if (input.city !== undefined) {
data.city = input.city?.trim() ?? null;
}
if (input.state !== undefined) {
data.state = input.state?.trim() ?? null;
}
if (input.country !== undefined) {
data.country = input.country?.trim() ?? null;
}
return db.complex.update({
where: { id },
data: data as Prisma.ComplexUncheckedUpdateInput,
})
});
}

View File

@@ -1,33 +1,33 @@
import { zValidator } from '@hono/zod-validator'
import { createCourtSchema, updateCourtSchema } from '@repo/api-contract'
import { Hono } from 'hono'
import { z } from 'zod'
import { requireAuth } from '@/middlewares/require-auth.middleware'
import { createCourtHandler } from '@/modules/court/handlers/create-court.handler'
import { listCourtsByComplexHandler } from '@/modules/court/handlers/list-courts-by-complex.handler'
import { updateCourtHandler } from '@/modules/court/handlers/update-court.handler'
import type { AppEnv } from '@/types/hono'
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { createCourtHandler } from '@/modules/court/handlers/create-court.handler';
import { listCourtsByComplexHandler } from '@/modules/court/handlers/list-courts-by-complex.handler';
import { updateCourtHandler } from '@/modules/court/handlers/update-court.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import { createCourtSchema, updateCourtSchema } from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
export const courtRoutes = new Hono<AppEnv>()
const complexIdParamsSchema = z.object({ complexId: z.uuid() })
const courtIdParamsSchema = z.object({ id: z.uuid() })
export const courtRoutes = new Hono<AppEnv>();
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
const courtIdParamsSchema = z.object({ id: z.uuid() });
courtRoutes.use('*', requireAuth)
courtRoutes.use('*', requireAuth);
courtRoutes.get(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
listCourtsByComplexHandler,
)
listCourtsByComplexHandler
);
courtRoutes.post(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('json', createCourtSchema),
createCourtHandler,
)
createCourtHandler
);
courtRoutes.patch(
'/:id',
zValidator('param', courtIdParamsSchema),
zValidator('json', updateCourtSchema),
updateCourtHandler,
)
updateCourtHandler
);

View File

@@ -1,21 +1,21 @@
import type { CreateCourtInput } from '@repo/api-contract'
import { CourtServiceError, createCourt } from '@/modules/court/services/court.service'
import type { AppContext } from '@/types/hono'
import { CourtServiceError, createCourt } from '@/modules/court/services/court.service';
import type { AppContext } from '@/types/hono';
import type { CreateCourtInput } from '@repo/api-contract';
type ComplexIdParams = { complexId: string }
type ComplexIdParams = { complexId: string };
export async function createCourtHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams
const payload = c.req.valid('json' as never) as CreateCourtInput
const appUserId = c.get('appUserId')
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateCourtInput;
const user = c.get('user');
try {
const court = await createCourt(appUserId, complexId, payload)
return c.json(court, 201)
const court = await createCourt(user.id, complexId, payload);
return c.json(court, 201);
} catch (error) {
if (error instanceof CourtServiceError) {
return c.json({ message: error.message }, error.status)
return c.json({ message: error.message }, error.status);
}
throw error
throw error;
}
}

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