mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
Set up proper DB migrations and fix remaining mobile responsive issues
Migrations: - drizzle.config.ts: add out: './drizzle' so drizzle-kit generate writes SQL migration files instead of only supporting push - drizzle/0000_glossy_magneto.sql: initial migration capturing all 9 current tables (users, accounts, sessions, verification_tokens, sso_providers, clients, businesses, invoices, invoice_items) - src/server/db/migrate.ts: programmatic runner using drizzle-orm's migrate() — tracks applied migrations in __drizzle_migrations, safe to run on every deploy - package.json: db:migrate now runs the programmatic runner instead of drizzle-kit migrate (CLI requires devDeps at runtime) - start.sh: replace drizzle-kit push with bun src/server/db/migrate.ts - Dockerfile: copy drizzle/ folder into the runner image so migrations are available at container startup Mobile fixes: - data-table.tsx: pagination buttons grow from 32px to 40px on mobile (h-10 w-10 md:h-8 md:w-8) to meet 44px touch-target guidelines - floating-action-bar.tsx: stack left-content + action buttons to column layout on narrow screens (flex-col sm:flex-row), reduce padding on mobile (p-3 sm:p-4) - revenue-chart.tsx: responsive chart height (h-48 md:h-64) so the chart doesn't consume too much vertical space on small screens https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
This commit is contained in:
@@ -44,6 +44,7 @@ RUN bun install --frozen-lockfile --production --verbose
|
||||
COPY --from=builder /app/start.sh ./start.sh
|
||||
COPY --from=builder /app/next.config.js ./next.config.js
|
||||
COPY --from=builder /app/src ./src
|
||||
COPY --from=builder /app/drizzle ./drizzle
|
||||
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
|
||||
COPY --from=builder /app/.env.example ./.env.example
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ if (!process.env.DATABASE_URL) {
|
||||
|
||||
export default {
|
||||
schema: "./src/server/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
CREATE TABLE "beenvoice_account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"userId" varchar(255) NOT NULL,
|
||||
"accountId" varchar(255) NOT NULL,
|
||||
"providerId" varchar(255) NOT NULL,
|
||||
"accessToken" text,
|
||||
"refreshToken" text,
|
||||
"accessTokenExpiresAt" timestamp,
|
||||
"refreshTokenExpiresAt" timestamp,
|
||||
"scope" varchar(255),
|
||||
"idToken" text,
|
||||
"password" text,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_business" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"nickname" varchar(255),
|
||||
"email" varchar(255),
|
||||
"phone" varchar(50),
|
||||
"addressLine1" varchar(255),
|
||||
"addressLine2" varchar(255),
|
||||
"city" varchar(100),
|
||||
"state" varchar(50),
|
||||
"postalCode" varchar(20),
|
||||
"country" varchar(100),
|
||||
"website" varchar(255),
|
||||
"taxId" varchar(100),
|
||||
"logoUrl" varchar(500),
|
||||
"isDefault" boolean DEFAULT false,
|
||||
"resendApiKey" varchar(255),
|
||||
"resendDomain" varchar(255),
|
||||
"emailFromName" varchar(255),
|
||||
"createdById" varchar(255) NOT NULL,
|
||||
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_client" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"email" varchar(255),
|
||||
"phone" varchar(50),
|
||||
"addressLine1" varchar(255),
|
||||
"addressLine2" varchar(255),
|
||||
"city" varchar(100),
|
||||
"state" varchar(50),
|
||||
"postalCode" varchar(20),
|
||||
"country" varchar(100),
|
||||
"defaultHourlyRate" real,
|
||||
"createdById" varchar(255) NOT NULL,
|
||||
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_invoice_item" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"invoiceId" varchar(255) NOT NULL,
|
||||
"date" timestamp NOT NULL,
|
||||
"description" varchar(500) NOT NULL,
|
||||
"hours" real NOT NULL,
|
||||
"rate" real NOT NULL,
|
||||
"amount" real NOT NULL,
|
||||
"position" integer DEFAULT 0 NOT NULL,
|
||||
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_invoice" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"invoiceNumber" varchar(100) NOT NULL,
|
||||
"businessId" varchar(255),
|
||||
"clientId" varchar(255) NOT NULL,
|
||||
"issueDate" timestamp NOT NULL,
|
||||
"dueDate" timestamp NOT NULL,
|
||||
"status" varchar(50) DEFAULT 'draft' NOT NULL,
|
||||
"totalAmount" real DEFAULT 0 NOT NULL,
|
||||
"taxRate" real DEFAULT 0 NOT NULL,
|
||||
"notes" varchar(1000),
|
||||
"createdById" varchar(255) NOT NULL,
|
||||
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"userId" varchar(255) NOT NULL,
|
||||
"token" varchar(255) NOT NULL,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"ipAddress" text,
|
||||
"userAgent" text,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "beenvoice_session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_sso_provider" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"providerId" varchar(255) NOT NULL,
|
||||
"userId" varchar(255) NOT NULL,
|
||||
"redirectURI" varchar(255) DEFAULT '' NOT NULL,
|
||||
"oidcConfig" text,
|
||||
"samlConfig" text,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "beenvoice_sso_provider_providerId_unique" UNIQUE("providerId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_user" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"emailVerified" boolean DEFAULT false NOT NULL,
|
||||
"image" varchar(255),
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
"password" varchar(255),
|
||||
"resetToken" varchar(255),
|
||||
"resetTokenExpiry" timestamp,
|
||||
"prefersReducedMotion" boolean DEFAULT false NOT NULL,
|
||||
"animationSpeedMultiplier" real DEFAULT 1 NOT NULL,
|
||||
"colorTheme" varchar(50) DEFAULT 'slate' NOT NULL,
|
||||
"customColor" varchar(50),
|
||||
"theme" varchar(20) DEFAULT 'system' NOT NULL,
|
||||
CONSTRAINT "beenvoice_user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "beenvoice_verification_token" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" varchar(255) NOT NULL,
|
||||
"value" varchar(255) NOT NULL,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_account" ADD CONSTRAINT "beenvoice_account_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_business" ADD CONSTRAINT "beenvoice_business_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_client" ADD CONSTRAINT "beenvoice_client_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_invoice_item" ADD CONSTRAINT "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_businessId_beenvoice_business_id_fk" FOREIGN KEY ("businessId") REFERENCES "public"."beenvoice_business"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_session" ADD CONSTRAINT "beenvoice_session_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "beenvoice_sso_provider" ADD CONSTRAINT "beenvoice_sso_provider_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "account_userId_idx" ON "beenvoice_account" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE INDEX "business_created_by_idx" ON "beenvoice_business" USING btree ("createdById");--> statement-breakpoint
|
||||
CREATE INDEX "business_name_idx" ON "beenvoice_business" USING btree ("name");--> statement-breakpoint
|
||||
CREATE INDEX "business_nickname_idx" ON "beenvoice_business" USING btree ("nickname");--> statement-breakpoint
|
||||
CREATE INDEX "business_email_idx" ON "beenvoice_business" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "business_is_default_idx" ON "beenvoice_business" USING btree ("isDefault");--> statement-breakpoint
|
||||
CREATE INDEX "client_created_by_idx" ON "beenvoice_client" USING btree ("createdById");--> statement-breakpoint
|
||||
CREATE INDEX "client_name_idx" ON "beenvoice_client" USING btree ("name");--> statement-breakpoint
|
||||
CREATE INDEX "client_email_idx" ON "beenvoice_client" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_item_invoice_id_idx" ON "beenvoice_invoice_item" USING btree ("invoiceId");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_item_date_idx" ON "beenvoice_invoice_item" USING btree ("date");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_item_position_idx" ON "beenvoice_invoice_item" USING btree ("position");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_business_id_idx" ON "beenvoice_invoice" USING btree ("businessId");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_client_id_idx" ON "beenvoice_invoice" USING btree ("clientId");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_created_by_idx" ON "beenvoice_invoice" USING btree ("createdById");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_number_idx" ON "beenvoice_invoice" USING btree ("invoiceNumber");--> statement-breakpoint
|
||||
CREATE INDEX "invoice_status_idx" ON "beenvoice_invoice" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "session_userId_idx" ON "beenvoice_session" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE INDEX "sso_provider_user_id_idx" ON "beenvoice_sso_provider" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE INDEX "verification_token_identifier_idx" ON "beenvoice_verification_token" USING btree ("identifier");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1775354242672,
|
||||
"tag": "0000_glossy_magneto",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"build": "next build",
|
||||
"check": "eslint . && tsc --noEmit",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:migrate": "bun src/server/db/migrate.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:clone": "./scripts/clone-local.sh",
|
||||
|
||||
@@ -86,7 +86,7 @@ export function RevenueChart({ data }: RevenueChartProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-64 w-full">
|
||||
<div className="h-48 w-full md:h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
|
||||
@@ -471,7 +471,7 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-10 w-10 md:h-8 md:w-8"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
@@ -481,7 +481,7 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-10 w-10 md:h-8 md:w-8"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
@@ -503,7 +503,7 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-10 w-10 md:h-8 md:w-8"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
@@ -513,7 +513,7 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-10 w-10 md:h-8 md:w-8"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function FloatingActionBar({
|
||||
)}
|
||||
>
|
||||
<Card className="hover-lift bg-card border-border border shadow-lg">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<CardContent className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between sm:p-4">
|
||||
{/* Left content */}
|
||||
{leftContent && (
|
||||
<div className="text-card-foreground animate-fade-in flex flex-1 items-center gap-3">
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Programmatic migration runner for production deployments.
|
||||
*
|
||||
* Run with: bun src/server/db/migrate.ts
|
||||
*
|
||||
* This applies any pending migrations from the drizzle/ directory to the
|
||||
* database specified by DATABASE_URL. It is safe to run multiple times —
|
||||
* Drizzle tracks applied migrations in the __drizzle_migrations table.
|
||||
*/
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
// Load env files before importing anything that reads process.env
|
||||
dotenv.config({ path: ".env.local" });
|
||||
dotenv.config({ path: ".env" });
|
||||
|
||||
import { Pool } from "pg";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error("[migrate] ERROR: DATABASE_URL is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const migrationsFolder = path.resolve(__dirname, "../../../drizzle");
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: databaseUrl,
|
||||
ssl: process.env.DB_DISABLE_SSL === "true" ? false : { rejectUnauthorized: false },
|
||||
max: 1,
|
||||
});
|
||||
|
||||
const db = drizzle(pool);
|
||||
|
||||
console.log("[migrate] Running migrations from", migrationsFolder);
|
||||
|
||||
try {
|
||||
await migrate(db, { migrationsFolder });
|
||||
console.log("[migrate] All migrations applied successfully");
|
||||
} catch (err) {
|
||||
console.error("[migrate] Migration failed:", err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
@@ -108,9 +108,8 @@ fi
|
||||
SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION:-false}
|
||||
|
||||
if [ "$SKIP_DB_MIGRATION" != "true" ]; then
|
||||
echo "[start.sh] Applying database migrations (drizzle-kit push via bunx)"
|
||||
# Use bunx so we don't need devDependencies inside the container
|
||||
SKIP_ENV_VALIDATION=1 bunx -y drizzle-kit@0.30.6 push
|
||||
echo "[start.sh] Applying database migrations"
|
||||
SKIP_ENV_VALIDATION=1 bun src/server/db/migrate.ts
|
||||
else
|
||||
echo "[start.sh] Skipping DB migration due to SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION}"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user