migration correction et maj
Some checks failed
Build and Push App / build (push) Failing after 11m56s
Some checks failed
Build and Push App / build (push) Failing after 11m56s
This commit is contained in:
BIN
admin/prisma/migrations/0_init/migration.sql
Normal file
BIN
admin/prisma/migrations/0_init/migration.sql
Normal file
Binary file not shown.
459
admin/prisma/migrations/20260427214502_init/migration.sql
Normal file
459
admin/prisma/migrations/20260427214502_init/migration.sql
Normal file
@@ -0,0 +1,459 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RatingStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReportStatus" AS ENUM ('PENDING', 'RESOLVED', 'DISMISSED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserRole" AS ENUM ('VISITOR', 'ENTREPRENEUR', 'ADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Plan" AS ENUM ('STARTER', 'BOOSTER', 'EMPIRE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OfferType" AS ENUM ('PRODUCT', 'SERVICE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "InterviewType" AS ENUM ('VIDEO', 'ARTICLE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"role" "UserRole" NOT NULL DEFAULT 'VISITOR',
|
||||
"avatar" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"bio" TEXT,
|
||||
"location" TEXT,
|
||||
"phone" TEXT,
|
||||
"isSuspended" BOOLEAN NOT NULL DEFAULT false,
|
||||
"suspensionReason" TEXT,
|
||||
"countryId" TEXT,
|
||||
"resetToken" TEXT,
|
||||
"resetTokenExpiry" TIMESTAMP(3),
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"verificationToken" TEXT,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Business" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"location" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"logoUrl" TEXT NOT NULL,
|
||||
"videoUrl" TEXT,
|
||||
"contactEmail" TEXT NOT NULL,
|
||||
"contactPhone" TEXT,
|
||||
"socialLinks" JSONB,
|
||||
"verified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"rating" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
||||
"ratingCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"tags" TEXT[],
|
||||
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
|
||||
"founderName" TEXT,
|
||||
"founderImageUrl" TEXT,
|
||||
"keyMetric" TEXT,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"slug" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT false,
|
||||
"showEmail" BOOLEAN NOT NULL DEFAULT true,
|
||||
"showPhone" BOOLEAN NOT NULL DEFAULT true,
|
||||
"showSocials" BOOLEAN NOT NULL DEFAULT true,
|
||||
"websiteUrl" TEXT,
|
||||
"isSuspended" BOOLEAN NOT NULL DEFAULT false,
|
||||
"suspensionReason" TEXT,
|
||||
"plan" "Plan" NOT NULL DEFAULT 'STARTER',
|
||||
"city" TEXT,
|
||||
"countryId" TEXT,
|
||||
"coverUrl" TEXT,
|
||||
"coverPosition" TEXT DEFAULT '50% 50%',
|
||||
"coverZoom" DOUBLE PRECISION DEFAULT 1.0,
|
||||
"suggestedCategory" TEXT,
|
||||
"categoryId" TEXT,
|
||||
|
||||
CONSTRAINT "Business_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BusinessCategory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"icon" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BusinessCategory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Country" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"flag" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"description" TEXT,
|
||||
|
||||
CONSTRAINT "Country_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PricingPlan" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tier" "Plan" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"priceXOF" TEXT NOT NULL,
|
||||
"priceEUR" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"features" TEXT[],
|
||||
"offerLimit" INTEGER NOT NULL DEFAULT 1,
|
||||
"recommended" BOOLEAN NOT NULL DEFAULT false,
|
||||
"color" TEXT NOT NULL DEFAULT 'gray',
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"yearlyPriceEUR" TEXT,
|
||||
"yearlyPriceXOF" TEXT,
|
||||
|
||||
CONSTRAINT "PricingPlan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Offer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"type" "OfferType" NOT NULL,
|
||||
"price" DOUBLE PRECISION NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'XOF',
|
||||
"description" TEXT,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"businessId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Offer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BlogPost" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"excerpt" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"author" TEXT NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metaDescription" TEXT,
|
||||
"metaTitle" TEXT,
|
||||
"slug" TEXT,
|
||||
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
|
||||
CONSTRAINT "BlogPost_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Interview" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"guestName" TEXT NOT NULL,
|
||||
"companyName" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"type" "InterviewType" NOT NULL,
|
||||
"thumbnailUrl" TEXT NOT NULL,
|
||||
"videoUrl" TEXT,
|
||||
"content" TEXT,
|
||||
"excerpt" TEXT NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"duration" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metaDescription" TEXT,
|
||||
"metaTitle" TEXT,
|
||||
"slug" TEXT,
|
||||
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
|
||||
CONSTRAINT "Interview_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Comment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"businessId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AnalyticsEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"label" TEXT,
|
||||
"value" DOUBLE PRECISION,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"browser" TEXT,
|
||||
"city" TEXT,
|
||||
"country" TEXT,
|
||||
"device" TEXT,
|
||||
"ip" TEXT,
|
||||
"os" TEXT,
|
||||
"referrer" TEXT,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "AnalyticsEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Rating" (
|
||||
"id" TEXT NOT NULL,
|
||||
"value" INTEGER NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"businessId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"comment" TEXT,
|
||||
"reply" TEXT,
|
||||
"replyAt" TIMESTAMP(3),
|
||||
"status" "RatingStatus" NOT NULL DEFAULT 'PENDING',
|
||||
|
||||
CONSTRAINT "Rating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Conversation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"businessId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ConversationParticipant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"conversationId" TEXT NOT NULL,
|
||||
"isArchived" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "ConversationParticipant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"senderId" TEXT NOT NULL,
|
||||
"conversationId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"read" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MessageReport" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reason" TEXT,
|
||||
"messageId" TEXT NOT NULL,
|
||||
"reporterId" TEXT NOT NULL,
|
||||
"status" "ReportStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MessageReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SiteSetting" (
|
||||
"id" TEXT NOT NULL DEFAULT 'singleton',
|
||||
"siteName" TEXT NOT NULL DEFAULT 'Afrohub',
|
||||
"siteSlogan" TEXT NOT NULL DEFAULT 'La plateforme de référence pour l''entrepreneuriat africain.',
|
||||
"contactEmail" TEXT NOT NULL DEFAULT 'support@afrohub.com',
|
||||
"contactPhone" TEXT DEFAULT '+225 00 00 00 00 00',
|
||||
"address" TEXT DEFAULT 'Abidjan, Côte d''Ivoire',
|
||||
"facebookUrl" TEXT,
|
||||
"twitterUrl" TEXT,
|
||||
"instagramUrl" TEXT,
|
||||
"linkedinUrl" TEXT,
|
||||
"footerText" TEXT DEFAULT '© 2025 Afrohub. Tous droits réservés.',
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"homeBlogCount" INTEGER NOT NULL DEFAULT 3,
|
||||
"homeBlogShow" BOOLEAN NOT NULL DEFAULT true,
|
||||
"homeBlogSubtitle" TEXT NOT NULL DEFAULT 'Toutes les actualités de l''entrepreneuriat africain.',
|
||||
"homeBlogTitle" TEXT NOT NULL DEFAULT 'Derniers Articles',
|
||||
"homeBlogCategories" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"homeBlogIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"homeBlogSelection" TEXT NOT NULL DEFAULT 'latest',
|
||||
"homeCategories" TEXT[] DEFAULT ARRAY['Technologie & IT', 'Agriculture & Agrobusiness', 'Mode & Textile', 'Cosmétique & Beauté']::TEXT[],
|
||||
"resetPasswordSubject" TEXT NOT NULL DEFAULT 'Réinitialisation de votre mot de passe - Afrohub',
|
||||
"resetPasswordTemplate" TEXT NOT NULL DEFAULT '<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;"><h2 style="color: #0d9488; text-align: center;">{siteName}</h2><p>Bonjour {name},</p><p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p><div style="text-align: center; margin: 30px 0;"><a href="{resetUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Réinitialiser mon mot de passe</a></div><p>Ce lien expirera dans 1 heure. Si vous n''avez pas demandé cette réinitialisation, vous pouvez ignorer cet email.</p><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;"><p style="font-size: 12px; color: #6b7280; text-align: center;">© 2025 {siteName}. Tous droits réservés.</p></div>',
|
||||
"verifyEmailSubject" TEXT NOT NULL DEFAULT 'Vérification de votre compte - Afrohub',
|
||||
"verifyEmailTemplate" TEXT NOT NULL DEFAULT '<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;"><h2 style="color: #0d9488; text-align: center;">{siteName}</h2><p>Bonjour {name},</p><p>Merci de vous être inscrit sur {siteName}. Pour finaliser la création de votre compte, merci de vérifier votre adresse email en cliquant sur le bouton ci-dessous :</p><div style="text-align: center; margin: 30px 0;"><a href="{verifyUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Vérifier mon compte</a></div><p>Si vous n''avez pas créé de compte sur notre plateforme, vous pouvez ignorer cet email.</p><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;"><p style="font-size: 12px; color: #6b7280; text-align: center;">© 2025 {siteName}. Tous droits réservés.</p></div>',
|
||||
|
||||
CONSTRAINT "SiteSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LegalDocument" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LegalDocument_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Event" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL,
|
||||
"location" TEXT NOT NULL,
|
||||
"thumbnailUrl" TEXT NOT NULL,
|
||||
"link" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metaDescription" TEXT,
|
||||
"metaTitle" TEXT,
|
||||
"slug" TEXT,
|
||||
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
|
||||
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Favorite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"businessId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Favorite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_resetToken_key" ON "User"("resetToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_verificationToken_key" ON "User"("verificationToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Business_ownerId_key" ON "Business"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Business_slug_key" ON "Business"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BusinessCategory_name_key" ON "BusinessCategory"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BusinessCategory_slug_key" ON "BusinessCategory"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Country_name_key" ON "Country"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Country_code_key" ON "Country"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PricingPlan_tier_key" ON "PricingPlan"("tier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Interview_slug_key" ON "Interview"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Rating_userId_businessId_key" ON "Rating"("userId", "businessId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ConversationParticipant_userId_conversationId_key" ON "ConversationParticipant"("userId", "conversationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LegalDocument_type_key" ON "LegalDocument"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Event_slug_key" ON "Event"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Favorite_userId_businessId_key" ON "Favorite"("userId", "businessId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Business" ADD CONSTRAINT "Business_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "BusinessCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Business" ADD CONSTRAINT "Business_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Business" ADD CONSTRAINT "Business_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Offer" ADD CONSTRAINT "Offer_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rating" ADD CONSTRAINT "Rating_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rating" ADD CONSTRAINT "Rating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ConversationParticipant" ADD CONSTRAINT "ConversationParticipant_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ConversationParticipant" ADD CONSTRAINT "ConversationParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MessageReport" ADD CONSTRAINT "MessageReport_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MessageReport" ADD CONSTRAINT "MessageReport_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "BlogPost" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Event" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Interview" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ContentStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "BlogPost" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Event" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Interview" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED';
|
||||
3
admin/prisma/migrations/migration_lock.toml
Normal file
3
admin/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -68,6 +68,8 @@ model Business {
|
||||
websiteUrl String?
|
||||
isSuspended Boolean @default(false)
|
||||
suspensionReason String?
|
||||
metaTitle String?
|
||||
metaDescription String?
|
||||
plan Plan @default(STARTER)
|
||||
city String?
|
||||
countryId String?
|
||||
|
||||
@@ -79,3 +79,23 @@ export async function getPendingVerificationCount() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
export async function updateBusinessSeo(id: string, data: {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
}) {
|
||||
try {
|
||||
await prisma.business.update({
|
||||
where: { id },
|
||||
data: {
|
||||
metaTitle: data.metaTitle,
|
||||
metaDescription: data.metaDescription,
|
||||
},
|
||||
});
|
||||
revalidatePath("/users");
|
||||
revalidatePath(`/annuaire/${id}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to update business SEO:", error);
|
||||
return { success: false, error: "Erreur lors de la mise à jour SEO" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,8 @@ export default async function UsersPage({
|
||||
userName={business.owner.name}
|
||||
isBusinessSuspended={!!business.isSuspended}
|
||||
isUserSuspended={!!business.owner?.isSuspended}
|
||||
metaTitle={business.metaTitle}
|
||||
metaDescription={business.metaDescription}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
120
admin/src/components/BusinessSeoModal.tsx
Normal file
120
admin/src/components/BusinessSeoModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useTransition } from 'react';
|
||||
import { X, Save, Loader2, Globe } from 'lucide-react';
|
||||
import { updateBusinessSeo } from '@/app/actions/business';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface BusinessSeoModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
business: {
|
||||
id: string;
|
||||
name: string;
|
||||
metaTitle?: string | null;
|
||||
metaDescription?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default function BusinessSeoModal({ isOpen, onClose, business }: BusinessSeoModalProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [metaTitle, setMetaTitle] = useState(business.metaTitle || '');
|
||||
const [metaDescription, setMetaDescription] = useState(business.metaDescription || '');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
startTransition(async () => {
|
||||
const result = await updateBusinessSeo(business.id, {
|
||||
metaTitle,
|
||||
metaDescription
|
||||
});
|
||||
if (result.success) {
|
||||
toast.success("Configuration SEO mise à jour");
|
||||
onClose();
|
||||
} else {
|
||||
toast.error(result.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
<div className="relative bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl animate-in zoom-in duration-200">
|
||||
<div className="p-6 border-b border-slate-800 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
|
||||
<Globe className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">SEO & Partage</h3>
|
||||
<p className="text-xs text-slate-400">Personnaliser l'aperçu WhatsApp pour {business.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 hover:bg-slate-800 rounded-lg text-slate-400 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Meta Titre (WhatsApp/Google)</label>
|
||||
<input
|
||||
value={metaTitle}
|
||||
onChange={(e) => setMetaTitle(e.target.value)}
|
||||
placeholder={`Ex: ${business.name} - Expert en ...`}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500 transition-all"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 italic">Recommandé : Moins de 60 caractères.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Meta Description</label>
|
||||
<textarea
|
||||
value={metaDescription}
|
||||
onChange={(e) => setMetaDescription(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Décrivez l'entreprise en quelques mots pour attirer les clics..."
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500 transition-all resize-none"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 italic">Recommandé : Entre 140 et 160 caractères.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-xl p-4">
|
||||
<h4 className="text-[10px] font-bold text-indigo-400 uppercase tracking-widest mb-2">Aperçu du partage</h4>
|
||||
<div className="bg-slate-950 rounded-lg p-3 border border-slate-800">
|
||||
<div className="text-sm font-bold text-indigo-400 line-clamp-1">{metaTitle || business.name}</div>
|
||||
<div className="text-xs text-slate-500 line-clamp-2 mt-1">{metaDescription || "Découvrez cette entreprise sur Afroprenariat..."}</div>
|
||||
<div className="text-[10px] text-slate-600 mt-2">afroprenariat.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 py-3 rounded-xl border border-slate-800 text-slate-400 font-bold hover:bg-slate-800 transition-all"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="flex-[2] py-3 rounded-xl bg-indigo-600 text-white font-bold hover:bg-indigo-500 transition-all flex items-center justify-center gap-2 shadow-lg shadow-indigo-600/20"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-5 h-5" />
|
||||
)}
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { ShieldAlert, ShieldCheck, ShoppingBag, User } from 'lucide-react';
|
||||
import { ShieldAlert, ShieldCheck, ShoppingBag, User, Globe } from 'lucide-react';
|
||||
import { suspendUser, unsuspendUser, suspendBusiness, unsuspendBusiness } from '@/app/actions/suspension';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import SuspensionModal from './SuspensionModal';
|
||||
import BusinessSeoModal from './BusinessSeoModal';
|
||||
|
||||
interface EntrepreneurActionsProps {
|
||||
businessId: string;
|
||||
@@ -13,6 +14,8 @@ interface EntrepreneurActionsProps {
|
||||
userName: string;
|
||||
isBusinessSuspended: boolean;
|
||||
isUserSuspended: boolean;
|
||||
metaTitle?: string | null;
|
||||
metaDescription?: string | null;
|
||||
}
|
||||
|
||||
export default function EntrepreneurActions({
|
||||
@@ -21,9 +24,12 @@ export default function EntrepreneurActions({
|
||||
userId,
|
||||
userName,
|
||||
isBusinessSuspended,
|
||||
isUserSuspended
|
||||
isUserSuspended,
|
||||
metaTitle,
|
||||
metaDescription
|
||||
}: EntrepreneurActionsProps) {
|
||||
const [modalType, setModalType] = useState<'USER' | 'BUSINESS' | null>(null);
|
||||
const [seoModalOpen, setSeoModalOpen] = useState(false);
|
||||
|
||||
const handleConfirm = async (reason: string) => {
|
||||
if (modalType === 'USER') {
|
||||
@@ -41,6 +47,16 @@ export default function EntrepreneurActions({
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 justify-end">
|
||||
{/* SEO Button */}
|
||||
<button
|
||||
onClick={() => setSeoModalOpen(true)}
|
||||
className="btn-action bg-indigo-500/10 text-indigo-500 hover:bg-indigo-500 hover:text-white"
|
||||
title="Modifier le SEO / Partage WhatsApp"
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
<span className="text-[10px] uppercase font-bold">SEO</span>
|
||||
</button>
|
||||
|
||||
{/* User Suspension */}
|
||||
{isUserSuspended ? (
|
||||
<button
|
||||
@@ -93,6 +109,17 @@ export default function EntrepreneurActions({
|
||||
? `Voulez-vous suspendre TOUT le compte de ${userName} ? Sa boutique sera également masquée.`
|
||||
: `Voulez-vous masquer la boutique "${businessName}" ? L'entrepreneur gardera l'accès à son compte.`}
|
||||
/>
|
||||
|
||||
<BusinessSeoModal
|
||||
isOpen={seoModalOpen}
|
||||
onClose={() => setSeoModalOpen(false)}
|
||||
business={{
|
||||
id: businessId,
|
||||
name: businessName,
|
||||
metaTitle,
|
||||
metaDescription
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user