refactor: layer portal routes through feature modules

This commit is contained in:
NTumurbars 2025-09-18 16:39:57 +09:00
parent 143aad11d8
commit e9acbd899c
63 changed files with 761 additions and 635 deletions

View File

@ -6,26 +6,31 @@ This document outlines the new feature-driven architecture implemented for the c
```
apps/portal/src/
├── app/ # Next.js App Router pages
├── components/ # Shared UI components (Design System)
│ ├── ui/ # Base UI components (atoms)
│ ├── layout/ # Layout components (organisms)
│ └── common/ # Shared business components (molecules)
├── features/ # Feature-specific modules
│ ├── auth/ # Authentication feature
│ ├── dashboard/ # Dashboard feature
│ ├── billing/ # Billing feature
│ ├── subscriptions/ # Subscriptions feature
│ ├── catalog/ # Product catalog feature
│ └── support/ # Support feature
├── lib/ # Core utilities and services (replaces core/shared)
│ ├── api/ # API client and base services
│ ├── query.ts # Query client and keys
│ ├── env.ts # Runtime env parsing
│ ├── types/ # Shared TypeScript types
│ └── utils/ # Utility functions (cn, currency, error-display, ...)
├── providers/ # React context providers (e.g., QueryProvider)
└── styles/ # Global styles and design tokens
├── app/ # Next.js App Router entry points (route groups only)
│ ├── (public)/ # Marketing + auth routes, pages import feature views
│ ├── (authenticated)/ # Signed-in portal routes, thin wrappers around features
│ ├── api/ # App Router API routes
│ ├── favicon.ico / globals.css # Global assets
│ └── layout.tsx # Root layout/providers
├── components/ # Shared UI components (design system atoms/molecules)
│ ├── ui/
│ ├── layout/
│ └── common/
├── core/ # App-wide configuration (env, logger, providers)
├── features/ # Feature-specific modules composed by routes
│ ├── account/
│ ├── auth/
│ ├── billing/
│ ├── catalog/
│ ├── dashboard/
│ ├── marketing/
│ ├── orders/
│ ├── service-management/
│ ├── subscriptions/
│ └── support/
├── shared/ # Cross-feature helpers (e.g., constants, locale data)
├── styles/ # Global styles and design tokens
└── types/ # Portal-specific TypeScript types
```
## Design Principles
@ -48,7 +53,7 @@ Each feature module contains:
- `utils/`: Utility functions
### 4. Centralized Shared Resources
Common utilities, types, and components are centralized in the `lib/` and `components/` directories.
Common utilities, types, and components are centralized in the `core/`, `shared/`, and `components/` directories.
## Feature Module Structure
@ -118,20 +123,21 @@ import { DataTable } from '@/components/common';
import type { User, ApiResponse } from '@/types';
// Utility imports
import { designSystem } from '@/lib/design-system';
// Prefer feature services/hooks over direct apiClient usage in pages
import { apiClient } from '@/lib/api/client';
import { QueryProvider } from '@/core/providers';
// Prefer feature services/hooks over direct api usage in pages
import { logger } from '@/core/config';
```
### Path Mappings
- `@/*` - Root src directory
- `@/components/*` - Component library
- `@/core/*` - App-wide configuration and providers
- `@/features/*` - Feature modules
- `@/lib/*` - Core utilities
- `@/types` - Type definitions
- `@/shared/*` - Shared helpers/constants
- `@/styles/*` - Style files
- `@shared/*` - Shared package
- `@/types/*` - Portal-specific types
- `@shared/*` - Shared package exports
## Migration Strategy
@ -179,6 +185,14 @@ The migration to this new architecture will be done incrementally:
This ensures pages remain declarative and the feature layer encapsulates logic.
### Route Layering
- `(public)`: marketing landing and authentication flows. These routes render feature views such as `marketing/PublicLandingView` and `auth` screens while remaining server components by default.
- `(authenticated)`: signed-in portal experience. Pages import dashboard, billing, subscriptions, etc. from the feature layer and rely on the shared route-group layout to provide navigation.
- `api/`: App Router API endpoints remain colocated under `src/app/api` and can reuse feature services for data access.
Only `layout.tsx`, `page.tsx`, and `loading.tsx` files live inside the route groups. All reusable UI, hooks, and services live under `src/features/**` to keep routing concerns thin.
### Current Feature Hooks/Services
- Catalog
@ -191,3 +205,7 @@ This ensures pages remain declarative and the feature layer encapsulates logic.
- Service: `ordersService` (list/detail/create)
- Account
- Service: `accountService` (`/me/address`)
- Support
- Views: `SupportCasesView`, `NewSupportCaseView` (mock data, ready for API wiring)
- Marketing
- Views: `PublicLandingView`, `PublicLandingLoadingView`

View File

@ -0,0 +1,5 @@
import { DashboardView } from "@/features/dashboard";
export default function DashboardPage() {
return <DashboardView />;
}

View File

@ -0,0 +1,5 @@
import { SupportCasesView } from "@/features/support";
export default function SupportCasesPage() {
return <SupportCasesView />;
}

View File

@ -0,0 +1,5 @@
import { NewSupportCaseView } from "@/features/support";
export default function NewSupportCasePage() {
return <NewSupportCaseView />;
}

View File

@ -1,260 +0,0 @@
"use client";
import { logger } from "@/core/config";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import {
ArrowLeftIcon,
PaperAirplaneIcon,
ExclamationCircleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
export default function NewSupportCasePage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
subject: "",
category: "Technical",
priority: "Medium",
description: "",
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Mock submission - would normally send to API
void (async () => {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
// Redirect to cases list with success message
router.push("/support/cases?created=true");
} catch (error) {
logger.error(error, "Error creating case");
} finally {
setIsSubmitting(false);
}
})();
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
const isFormValid = formData.subject.trim() && formData.description.trim();
return (
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center space-x-4 mb-4">
<button
onClick={() => router.back()}
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
>
<ArrowLeftIcon className="h-4 w-4 mr-1" />
Back to Support
</button>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Create Support Case</h1>
<p className="mt-1 text-sm text-gray-600">Get help from our support team</p>
</div>
</div>
{/* Help Tips */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon className="h-5 w-5 text-blue-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">Before creating a case</h3>
<div className="mt-2 text-sm text-blue-700">
<ul className="list-disc list-inside space-y-1">
<li>Check our knowledge base for common solutions</li>
<li>Include relevant error messages or screenshots</li>
<li>Provide detailed steps to reproduce the issue</li>
<li>Mention your service or subscription if applicable</li>
</ul>
</div>
</div>
</div>
</div>
{/* Form */}
<div className="bg-white shadow rounded-lg">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Subject */}
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
Subject *
</label>
<input
type="text"
id="subject"
value={formData.subject}
onChange={e => handleInputChange("subject", e.target.value)}
placeholder="Brief description of your issue"
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
{/* Category and Priority */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label
htmlFor="category"
className="block text-sm font-medium text-gray-700 mb-2"
>
Category
</label>
<select
id="category"
value={formData.category}
onChange={e => handleInputChange("category", e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="Technical">Technical Support</option>
<option value="Billing">Billing Question</option>
<option value="General">General Inquiry</option>
<option value="Feature Request">Feature Request</option>
<option value="Bug Report">Bug Report</option>
</select>
</div>
<div>
<label
htmlFor="priority"
className="block text-sm font-medium text-gray-700 mb-2"
>
Priority
</label>
<select
id="priority"
value={formData.priority}
onChange={e => handleInputChange("priority", e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="Low">Low - General question</option>
<option value="Medium">Medium - Issue affecting work</option>
<option value="High">High - Service disruption</option>
<option value="Critical">Critical - Complete outage</option>
</select>
</div>
</div>
{/* Description */}
<div>
<label
htmlFor="description"
className="block text-sm font-medium text-gray-700 mb-2"
>
Description *
</label>
<textarea
id="description"
rows={6}
value={formData.description}
onChange={e => handleInputChange("description", e.target.value)}
placeholder="Please provide a detailed description of your issue, including any error messages and steps to reproduce the problem..."
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required
/>
<p className="mt-2 text-xs text-gray-500">
The more details you provide, the faster we can help you.
</p>
</div>
{/* Priority Warning */}
{formData.priority === "Critical" && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Critical Priority Selected
</h3>
<div className="mt-2 text-sm text-red-700">
<p>
Critical priority should only be used for complete service outages. For
urgent issues that aren&apos;t complete outages, please use High priority.
</p>
</div>
</div>
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-between pt-6 border-t border-gray-200">
<button
type="button"
onClick={() => router.back()}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
disabled={!isFormValid || isSubmitting}
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Creating Case...
</>
) : (
<>
<PaperAirplaneIcon className="h-4 w-4 mr-2" />
Create Case
</>
)}
</button>
</div>
</form>
</div>
{/* Additional Help */}
<div className="mt-8 bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Need immediate help?</h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-gray-900">Phone Support</h4>
<p className="text-sm text-gray-600 mt-1">
9:30-18:00 JST
<br />
<span className="font-medium text-blue-600">0120-660-470</span>
</p>
</div>
<div>
<h4 className="text-sm font-medium text-gray-900">Knowledge Base</h4>
<p className="text-sm text-gray-600 mt-1">
Search our help articles for quick solutions
<br />
<Link
href="/support/kb"
className="font-medium text-blue-600 hover:text-blue-500"
>
Browse Knowledge Base
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
import { PublicLandingLoadingView } from "@/features/marketing";
export default function PublicHomeLoading() {
return <PublicLandingLoadingView />;
}

View File

@ -0,0 +1,5 @@
import { PublicLandingView } from "@/features/marketing";
export default function PublicHomePage() {
return <PublicLandingView />;
}

View File

@ -1,288 +0,0 @@
import Link from "next/link";
import { Logo } from "@/components/ui/logo";
import {
ArrowPathIcon,
UserIcon,
SparklesIcon,
CreditCardIcon,
Cog6ToothIcon,
PhoneIcon,
ChartBarIcon,
} from "@heroicons/react/24/outline";
export default function Home() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
{/* Header */}
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<Logo size={40} />
<div>
<h1 className="text-xl font-semibold text-gray-900">Assist Solutions</h1>
<p className="text-xs text-gray-500">Customer Portal</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Link
href="/auth/login"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium text-sm"
>
Login
</Link>
<Link
href="/support"
className="text-gray-600 hover:text-gray-900 px-4 py-2 text-sm transition-colors duration-200"
>
Support
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="relative py-20 overflow-hidden">
{/* Abstract background elements */}
<div className="absolute inset-0">
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
<div
className="absolute top-40 right-10 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "2s" }}
></div>
<div
className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "4s" }}
></div>
</div>
<div className="relative max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-[var(--cp-space-2xl)]">
New Assist Solutions Customer Portal
</h1>
<div className="flex justify-center">
<a
href="#portal-access"
className="bg-blue-600 text-white px-10 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1"
>
Get Started
</a>
</div>
</div>
</div>
</section>
{/* Customer Portal Access Section */}
<section id="portal-access" className="py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Access Your Portal</h2>
<p className="text-lg text-gray-600">Choose the option that applies to you</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)] items-stretch">
{/* Existing Customers - Migration */}
<div className="bg-white rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-all duration-[var(--cp-transition-slow)] transform hover:-translate-y-2 border-[var(--cp-card-border)]">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<ArrowPathIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Existing Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Migrate to our new portal and enjoy enhanced security with modern interface.
</p>
<div className="mt-auto">
<Link
href="/auth/link-whmcs"
className="block bg-blue-600 text-white px-8 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg mb-3 shadow-md hover:shadow-lg"
>
Migrate Your Account
</Link>
<p className="text-sm text-gray-500">Takes just a few minutes</p>
</div>
</div>
</div>
{/* Portal Users */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<UserIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Portal Users</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Sign in to access your dashboard and manage all your services efficiently.
</p>
<div className="mt-auto">
<Link
href="/auth/login"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
>
Login to Portal
</Link>
<p className="text-sm text-gray-500">Secure access to your account</p>
</div>
</div>
</div>
{/* New Customers */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<SparklesIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">New Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Create your account and access our full range of IT solutions and services.
</p>
<div className="mt-auto">
<Link
href="/auth/signup"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
>
Create Account
</Link>
<p className="text-sm text-gray-500">Start your journey with us</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Portal Features Section */}
<section className="py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Portal Features</h2>
<p className="text-lg text-gray-600">
Everything you need to manage your Assist Solutions services
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div className="flex justify-center mb-3">
<CreditCardIcon className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900">Billing & Payments</h3>
<p className="text-gray-600 text-sm">
View invoices, payment history, and manage billing
</p>
</div>
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div className="flex justify-center mb-3">
<Cog6ToothIcon className="w-8 h-8 text-purple-600" />
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900">Service Management</h3>
<p className="text-gray-600 text-sm">Control and configure your active services</p>
</div>
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div className="flex justify-center mb-3">
<PhoneIcon className="w-8 h-8 text-pink-600" />
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900">Support Tickets</h3>
<p className="text-gray-600 text-sm">Create and track support requests</p>
</div>
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div className="flex justify-center mb-3">
<ChartBarIcon className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900">Usage Reports</h3>
<p className="text-gray-600 text-sm">Monitor service usage and performance</p>
</div>
</div>
</div>
</section>
{/* Support Section */}
<section className="py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Need Help?</h2>
<p className="text-lg text-gray-600">Our support team is here to assist you</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Contact Details Box */}
<div className="bg-white rounded-lg p-8 shadow-lg hover:shadow-xl transition-all duration-300">
<h3 className="text-xl font-semibold mb-6 text-gray-900">Contact Details</h3>
<div className="grid grid-cols-2 gap-6">
<div className="text-center">
<h4 className="font-semibold text-gray-900 mb-2">Phone Support</h4>
<p className="text-gray-600 text-sm mb-1">9:30-18:00 JST</p>
<p className="text-blue-600 font-medium">0120-660-470</p>
<p className="text-gray-500 text-sm">Toll Free within Japan</p>
</div>
<div className="text-center">
<h4 className="font-semibold text-gray-900 mb-2">Email Support</h4>
<p className="text-gray-600 text-sm mb-1">Response within 24h</p>
<Link
href="/contact"
className="text-purple-600 font-medium hover:text-purple-700"
>
Send Message
</Link>
</div>
</div>
</div>
{/* Live Chat & Business Hours Box */}
<div className="bg-white rounded-lg p-8 shadow-lg hover:shadow-xl transition-all duration-300">
<h3 className="text-xl font-semibold mb-6 text-gray-900">
Live Chat & Business Hours
</h3>
<div className="grid grid-cols-2 gap-6">
<div className="text-center">
<h4 className="font-semibold text-gray-900 mb-2">Live Chat</h4>
<p className="text-gray-600 text-sm mb-1">Available 24/7</p>
<Link href="/chat" className="text-green-600 font-medium hover:text-green-700">
Start Chat
</Link>
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-3">Business Hours</h4>
<div className="text-gray-600 text-sm space-y-1">
<p>
<strong>Monday - Saturday:</strong>
<br />
10:00 AM - 6:00 PM JST
</p>
<p>
<strong>Sunday:</strong>
<br />
Closed
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-white/90 backdrop-blur-sm text-gray-900 py-8 border-t border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<p className="text-gray-600 text-sm">
© 2025 Assist Solutions Corp. All Rights Reserved.
</p>
</div>
</div>
</footer>
</div>
);
}

View File

@ -1,2 +1,3 @@
export * from "./components";
export * from "./hooks";
export * from "./views";

View File

@ -1,23 +1,16 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/features/auth/services/auth.store";
import { useDashboardSummary } from "@/features/dashboard/hooks/useDashboard";
import type { Activity } from "@customer-portal/domain";
import type { Activity, DashboardSummary } from "@customer-portal/domain";
import {
CreditCardIcon,
ServerIcon,
ChatBubbleLeftRightIcon,
ExclamationTriangleIcon,
ChevronRightIcon,
PlusIcon,
DocumentTextIcon,
ArrowTrendingUpIcon,
CalendarDaysIcon,
BellIcon,
ClipboardDocumentListIcon,
} from "@heroicons/react/24/outline";
import {
CreditCardIcon as CreditCardIconSolid,
@ -26,15 +19,19 @@ import {
ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,
} from "@heroicons/react/24/solid";
import { format, formatDistanceToNow } from "date-fns";
import { useAuthStore } from "@/features/auth/services/auth.store";
import { useDashboardSummary } from "@/features/dashboard/hooks";
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components";
import { LoadingSpinner } from "@/components/ui/loading-skeleton";
import { ErrorState } from "@/components/ui/error-state";
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
export default function DashboardPage() {
export function DashboardView() {
const router = useRouter();
const { user, isAuthenticated, loading: authLoading } = useAuthStore();
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
const upcomingInvoice = summary?.nextInvoice ?? null;
const [paymentLoading, setPaymentLoading] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
@ -115,7 +112,7 @@ export default function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--cp-space-2xl)] mb-[var(--cp-space-3xl)]">
<StatCard
title="Recent Orders"
value={((summary?.stats as Record<string, unknown>)?.recentOrders as number) || 0}
value={summary?.stats?.recentOrders ?? 0}
icon={ClipboardDocumentListIconSolid}
gradient="from-gray-500 to-gray-600"
href="/orders"
@ -157,7 +154,7 @@ export default function DashboardPage() {
{/* Main Content Area */}
<div className="lg:col-span-2 space-y-[var(--cp-space-3xl)]">
{/* Upcoming Payment - compressed attention banner */}
{summary?.nextInvoice && (
{upcomingInvoice && (
<div
id="attention"
className="bg-white rounded-xl border border-orange-200 shadow-sm p-4"
@ -172,29 +169,28 @@ export default function DashboardPage() {
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
<span className="font-semibold text-gray-900">Upcoming Payment</span>
<span className="text-gray-400"></span>
<span>Invoice #{summary.nextInvoice.id}</span>
<span>Invoice #{upcomingInvoice.id}</span>
<span className="text-gray-400"></span>
<span title={format(new Date(summary.nextInvoice.dueDate), "MMMM d, yyyy")}>
<span title={format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}>
Due{" "}
{formatDistanceToNow(new Date(summary.nextInvoice.dueDate), {
{formatDistanceToNow(new Date(upcomingInvoice.dueDate), {
addSuffix: true,
})}
</span>
</div>
<div className="mt-1 text-2xl font-bold text-gray-900">
{formatCurrency(summary.nextInvoice.amount, {
currency: summary.nextInvoice.currency || "JPY",
locale: getCurrencyLocale(summary.nextInvoice.currency || "JPY"),
{formatCurrency(upcomingInvoice.amount, {
currency: upcomingInvoice.currency || "JPY",
locale: getCurrencyLocale(upcomingInvoice.currency || "JPY"),
})}
</div>
<div className="mt-1 text-xs text-gray-500">
Exact due date:{" "}
{format(new Date(summary.nextInvoice.dueDate), "MMMM d, yyyy")}
Exact due date: {format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}
</div>
</div>
<div className="flex flex-col items-end gap-2">
<button
onClick={() => handlePayNow(summary.nextInvoice!.id)}
onClick={() => handlePayNow(upcomingInvoice.id)}
disabled={paymentLoading}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
@ -205,7 +201,7 @@ export default function DashboardPage() {
{!paymentLoading && <ChevronRightIcon className="ml-2 h-4 w-4" />}
</button>
<Link
href={`/billing/invoices/${summary.nextInvoice.id}`}
href={`/billing/invoices/${upcomingInvoice.id}`}
className="text-blue-600 hover:text-blue-700 font-medium text-sm"
>
View invoice
@ -276,12 +272,13 @@ export default function DashboardPage() {
}
// Helpers and small components (local to dashboard)
function truncateName(name: string, len = 28) {
if (name.length <= len) return name;
return name.slice(0, Math.max(0, len - 1)) + "…";
}
function TasksChip({ summaryLoading, summary }: { summaryLoading: boolean; summary: any }) {
function TasksChip({
summaryLoading,
summary,
}: {
summaryLoading: boolean;
summary: DashboardSummary | undefined;
}) {
const router = useRouter();
if (summaryLoading) return null;
const tasks: Array<{ label: string; href: string }> = [];
@ -339,7 +336,11 @@ function RecentActivityCard({
<button
key={opt.k}
onClick={() => setFilter(opt.k)}
className={`px-2.5 py-1 text-xs rounded-md font-medium ${filter === opt.k ? "bg-white text-gray-900 shadow" : "text-gray-600 hover:text-gray-900"}`}
className={`px-2.5 py-1 text-xs rounded-md font-medium ${
filter === opt.k
? "bg-white text-gray-900 shadow"
: "text-gray-600 hover:text-gray-900"
}`}
>
{opt.label}
</button>

View File

@ -0,0 +1 @@
export * from "./DashboardView";

View File

@ -1,3 +1,5 @@
export * as billing from "./billing";
export * as subscriptions from "./subscriptions";
export * as dashboard from "./dashboard";
export * as marketing from "./marketing";
export * as support from "./support";

View File

@ -0,0 +1,2 @@
export * from "./views/PublicLandingView";
export * from "./views/PublicLandingLoadingView";

View File

@ -1,6 +1,6 @@
import { Skeleton } from "@/components/ui/loading-skeleton";
export default function RootLoading() {
export function PublicLandingLoadingView() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
@ -22,8 +22,11 @@ export default function RootLoading() {
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8 space-y-8">
<Skeleton className="h-12 w-2/3" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl p-8 shadow-lg border border-gray-100 space-y-4">
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className="bg-white rounded-2xl p-8 shadow-lg border border-gray-100 space-y-4"
>
<Skeleton className="h-16 w-16 rounded-full mx-auto" />
<Skeleton className="h-6 w-1/2 mx-auto" />
<Skeleton className="h-4 w-3/4 mx-auto" />
@ -36,5 +39,3 @@ export default function RootLoading() {
</div>
);
}

View File

@ -0,0 +1,368 @@
import Link from "next/link";
import { Logo } from "@/components/ui/logo";
import {
ArrowPathIcon,
UserIcon,
SparklesIcon,
CreditCardIcon,
Cog6ToothIcon,
PhoneIcon,
ChartBarIcon,
} from "@heroicons/react/24/outline";
export function PublicLandingView() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
{/* Header */}
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<Logo size={40} />
<div>
<h1 className="text-xl font-semibold text-gray-900">Assist Solutions</h1>
<p className="text-xs text-gray-500">Customer Portal</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Link
href="/auth/login"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium text-sm"
>
Login
</Link>
<Link
href="/support"
className="text-gray-600 hover:text-gray-900 px-4 py-2 text-sm transition-colors duration-200"
>
Support
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="relative py-20 overflow-hidden">
{/* Abstract background elements */}
<div className="absolute inset-0">
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
<div
className="absolute top-40 right-10 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "2s" }}
></div>
<div
className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "4s" }}
></div>
</div>
<div className="relative max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-[var(--cp-space-2xl)]">
New Assist Solutions Customer Portal
</h1>
<div className="flex justify-center">
<a
href="#portal-access"
className="bg-blue-600 text-white px-10 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1"
>
Get Started
</a>
</div>
</div>
</div>
</section>
{/* Customer Portal Access Section */}
<section id="portal-access" className="py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Access Your Portal</h2>
<p className="text-lg text-gray-600">Choose the option that applies to you</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)] items-stretch">
{/* Existing Customers - Migration */}
<div className="bg-white rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-all duration-[var(--cp-transition-slow)] transform hover:-translate-y-2 border-[var(--cp-card-border)]">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<ArrowPathIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Existing Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Migrate to our new portal and enjoy enhanced security with modern interface.
</p>
<div className="mt-auto">
<Link
href="/auth/link-whmcs"
className="block bg-blue-600 text-white px-8 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg mb-3 shadow-md hover:shadow-lg"
>
Migrate Your Account
</Link>
<p className="text-sm text-gray-500">Takes just a few minutes</p>
</div>
</div>
</div>
{/* Portal Users */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<UserIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Portal Users</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Sign in to access your dashboard and manage all your services efficiently.
</p>
<div className="mt-auto">
<Link
href="/auth/login"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
>
Login to Portal
</Link>
<p className="text-sm text-gray-500">Secure access to your account</p>
</div>
</div>
</div>
{/* New Customers */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<SparklesIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">New Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Create your account and access our full range of IT solutions and services.
</p>
<div className="mt-auto">
<Link
href="/auth/signup"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
>
Create Account
</Link>
<p className="text-sm text-gray-500">Start your journey with us</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Portal Features Section */}
<section className="bg-white py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Why Choose Assist Solutions</h2>
<p className="text-lg text-gray-600">
Modern tools to manage your IT services with confidence
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)]">
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<CreditCardIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Automated Billing</h3>
<p className="text-gray-600">
Transparent invoicing, automated payments, and flexible billing options tailored to
your business.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<Cog6ToothIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Service Management</h3>
<p className="text-gray-600">
Control subscriptions, manage network services, and track usage from a single pane
of glass.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<PhoneIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Expert Support</h3>
<p className="text-gray-600">
Dedicated support team with SLA-backed response times and proactive service
monitoring.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<ChartBarIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Actionable Insights</h3>
<p className="text-gray-600">
Real-time analytics and reporting to help you optimize resource usage and forecast
demand.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<ArrowPathIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Seamless Migration</h3>
<p className="text-gray-600">
Guided onboarding for WHMCS customers with automatic data migration and validation.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<SparklesIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Future-Proof Platform</h3>
<p className="text-gray-600">
Built on modern infrastructure with continuous updates, security patches, and new
features.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-700 py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="text-4xl font-bold text-white mb-6">
Ready to experience the new portal?
</h2>
<p className="text-blue-100 text-lg mb-8">
Join thousands of customers who trust Assist Solutions to keep their business
connected and secure.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link
href="/auth/signup"
className="inline-flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-lg text-blue-600 bg-white hover:bg-blue-50 transition-colors duration-200"
>
Create an Account
</Link>
<Link
href="/auth/login"
className="inline-flex items-center justify-center px-8 py-3 border border-blue-300 text-base font-medium rounded-lg text-white bg-transparent hover:bg-blue-800 transition-colors duration-200"
>
Portal Login
</Link>
</div>
</div>
<div className="bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/20">
<h3 className="text-xl font-semibold text-white mb-4">Whats included?</h3>
<ul className="space-y-4 text-blue-100">
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Centralized service and subscription management dashboard</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Automated billing with support for multiple payment methods</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Priority access to our customer support specialists</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Insights and analytics to track usage and growth</span>
</li>
</ul>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-blue-950 text-blue-100 py-10">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<Logo size={32} />
<p className="mt-4 text-sm text-blue-200">
Delivering reliable IT solutions and support for businesses of all sizes.
</p>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Portal</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>
<Link
href="/auth/login"
className="hover:text-white transition-colors duration-200"
>
Login
</Link>
</li>
<li>
<Link
href="/auth/signup"
className="hover:text-white transition-colors duration-200"
>
Create Account
</Link>
</li>
<li>
<Link href="/support" className="hover:text-white transition-colors duration-200">
Support Center
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">
Solutions
</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>Network Services</li>
<li>Managed Security</li>
<li>Cloud Infrastructure</li>
<li>Professional Services</li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Contact</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>support@assistsolutions.com</li>
<li>+1 (800) 555-0123</li>
<li>123 Innovation Drive, Suite 400</li>
<li>San Francisco, CA 94105</li>
</ul>
</div>
</div>
<div className="mt-10 border-t border-blue-800 pt-6 text-sm text-blue-300 flex flex-col sm:flex-row justify-between gap-4">
<p>&copy; {new Date().getFullYear()} Assist Solutions. All rights reserved.</p>
<div className="flex gap-6">
<Link href="#" className="hover:text-white transition-colors duration-200">
Privacy Policy
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Terms of Service
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Status
</Link>
</div>
</div>
</div>
</footer>
</div>
);
}

View File

@ -1,9 +1 @@
/**
* Support Feature Module
* Customer support functionality including components, hooks, and services
*
* Note: This feature module is currently empty and ready for future implementation
*/
// This feature module is not yet implemented
// Components, hooks, services, types, and utilities will be added as needed
export * from "./views";

View File

@ -0,0 +1,247 @@
"use client";
import { useState, type FormEvent } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
ArrowLeftIcon,
PaperAirplaneIcon,
ExclamationCircleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { logger } from "@customer-portal/logging";
export function NewSupportCaseView() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
subject: "",
category: "Technical",
priority: "Medium",
description: "",
});
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
setIsSubmitting(true);
// Mock submission - would normally send to API
void (async () => {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
// Redirect to cases list with success message
router.push("/support/cases?created=true");
} catch (error) {
logger.error({ error }, "Error creating case");
} finally {
setIsSubmitting(false);
}
})();
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
const isFormValid = formData.subject.trim() && formData.description.trim();
return (
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center space-x-4 mb-4">
<button
onClick={() => router.back()}
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
>
<ArrowLeftIcon className="h-4 w-4 mr-1" />
Back to Support
</button>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Create Support Case</h1>
<p className="mt-1 text-sm text-gray-600">Get help from our support team</p>
</div>
</div>
{/* Help Tips */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon className="h-5 w-5 text-blue-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">Before creating a case</h3>
<div className="mt-2 text-sm text-blue-700">
<ul className="list-disc list-inside space-y-1">
<li>Check our knowledge base for common solutions</li>
<li>Include relevant error messages or screenshots</li>
<li>Provide detailed steps to reproduce the issue</li>
<li>Mention your service or subscription if applicable</li>
</ul>
</div>
</div>
</div>
</div>
{/* Form */}
<div className="bg-white shadow rounded-lg">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Subject */}
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
Subject *
</label>
<input
type="text"
id="subject"
value={formData.subject}
onChange={event => handleInputChange("subject", event.target.value)}
placeholder="Brief description of your issue"
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
{/* Category and Priority */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<select
id="category"
value={formData.category}
onChange={event => handleInputChange("category", event.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="Technical">Technical Support</option>
<option value="Billing">Billing Question</option>
<option value="General">General Inquiry</option>
<option value="Feature Request">Feature Request</option>
<option value="Bug Report">Bug Report</option>
</select>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<select
id="priority"
value={formData.priority}
onChange={event => handleInputChange("priority", event.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="Low">Low - General question</option>
<option value="Medium">Medium - Issue affecting work</option>
<option value="High">High - Service disruption</option>
<option value="Critical">Critical - Complete outage</option>
</select>
</div>
</div>
{/* Description */}
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
Description *
</label>
<textarea
id="description"
rows={6}
value={formData.description}
onChange={event => handleInputChange("description", event.target.value)}
placeholder="Please provide a detailed description of your issue, including any error messages and steps to reproduce the problem..."
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required
/>
<p className="mt-2 text-xs text-gray-500">
The more details you provide, the faster we can help you.
</p>
</div>
{/* Priority Warning */}
{formData.priority === "Critical" && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Critical Priority Selected</h3>
<div className="mt-2 text-sm text-red-700">
<p>
Critical priority should only be used for complete service outages. For
urgent issues that aren&apos;t complete outages, please use High priority.
</p>
</div>
</div>
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-between pt-6 border-t border-gray-200">
<button
type="button"
onClick={() => router.back()}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
disabled={!isFormValid || isSubmitting}
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Creating Case...
</>
) : (
<>
<PaperAirplaneIcon className="h-4 w-4 mr-2" />
Create Case
</>
)}
</button>
</div>
</form>
</div>
{/* Additional Help */}
<div className="mt-8 bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Need immediate help?</h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-gray-900">Phone Support</h4>
<p className="text-sm text-gray-600 mt-1">
9:30-18:00 JST
<br />
<span className="font-medium text-blue-600">0120-660-470</span>
</p>
</div>
<div>
<h4 className="text-sm font-medium text-gray-900">Knowledge Base</h4>
<p className="text-sm text-gray-600 mt-1">
Search our help articles for quick solutions
<br />
<Link href="/support/kb" className="font-medium text-blue-600 hover:text-blue-500">
Browse Knowledge Base
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import {
ChatBubbleLeftRightIcon,
@ -28,7 +28,7 @@ interface SupportCase {
assignedTo?: string;
}
export default function SupportCasesPage() {
export function SupportCasesView() {
const [cases, setCases] = useState<SupportCase[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
@ -99,10 +99,12 @@ export default function SupportCasesPage() {
},
];
setTimeout(() => {
const timeout = setTimeout(() => {
setCases(mockCases);
setLoading(false);
}, 500);
return () => clearTimeout(timeout);
}, []);
// Filter cases based on search, status, and priority
@ -228,8 +230,8 @@ export default function SupportCasesPage() {
<dt className="text-sm font-medium text-gray-500 truncate">Open Cases</dt>
<dd className="text-lg font-medium text-gray-900">
{
cases.filter(c =>
["Open", "In Progress", "Waiting on Customer"].includes(c.status)
cases.filter(caseItem =>
["Open", "In Progress", "Waiting on Customer"].includes(caseItem.status)
).length
}
</dd>
@ -249,7 +251,10 @@ export default function SupportCasesPage() {
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">High Priority</dt>
<dd className="text-lg font-medium text-gray-900">
{cases.filter(c => ["High", "Critical"].includes(c.priority)).length}
{
cases.filter(caseItem => ["High", "Critical"].includes(caseItem.priority))
.length
}
</dd>
</dl>
</div>
@ -267,7 +272,10 @@ export default function SupportCasesPage() {
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Resolved</dt>
<dd className="text-lg font-medium text-gray-900">
{cases.filter(c => ["Resolved", "Closed"].includes(c.status)).length}
{
cases.filter(caseItem => ["Resolved", "Closed"].includes(caseItem.status))
.length
}
</dd>
</dl>
</div>
@ -289,7 +297,7 @@ export default function SupportCasesPage() {
type="text"
placeholder="Search cases..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onChange={event => setSearchTerm(event.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
@ -298,7 +306,7 @@ export default function SupportCasesPage() {
<div className="relative">
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
onChange={event => setStatusFilter(event.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Statuses</option>
@ -314,7 +322,7 @@ export default function SupportCasesPage() {
<div className="relative">
<select
value={priorityFilter}
onChange={e => setPriorityFilter(e.target.value)}
onChange={event => setPriorityFilter(event.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Priorities</option>

View File

@ -0,0 +1,2 @@
export * from "./NewSupportCaseView";
export * from "./SupportCasesView";

View File

@ -15,6 +15,12 @@
// Path mappings
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/core/*": ["./src/core/*"],
"@/features/*": ["./src/features/*"],
"@/shared/*": ["./src/shared/*"],
"@/styles/*": ["./src/styles/*"],
"@/types/*": ["./src/types/*"],
},
// Enforce TS-only in portal and keep strict mode explicit (inherits from root)
"allowJs": false,

View File

@ -30,7 +30,7 @@ apps/portal/src/features/service-management/
## Integration
- Entry point: `apps/portal/src/app/(portal)/subscriptions/[id]/page.tsx` renders `ServiceManagementSection`
- Entry point: `apps/portal/src/app/(authenticated)/subscriptions/[id]/page.tsx` renders `ServiceManagementSection`
- Detection: SIM availability is inferred from `subscription.productName` including `sim` (case-insensitive)
## Future Expansion

View File

@ -99,10 +99,10 @@ export default [
},
},
// Prevent importing the DashboardLayout directly in (portal) pages.
// Pages should rely on the shared route-group layout at (portal)/layout.tsx.
// Prevent importing the DashboardLayout directly in (authenticated) pages.
// Pages should rely on the shared route-group layout at (authenticated)/layout.tsx.
{
files: ["apps/portal/src/app/(portal)/**/*.{ts,tsx}"],
files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
@ -111,7 +111,7 @@ export default [
{
group: ["@/components/layout/dashboard-layout"],
message:
"Use the shared (portal)/layout.tsx instead of importing DashboardLayout in pages.",
"Use the shared (authenticated)/layout.tsx instead of importing DashboardLayout in pages.",
},
],
},
@ -133,14 +133,14 @@ export default [
},
// Allow the shared layout file itself to import the layout component
{
files: ["apps/portal/src/app/(portal)/layout.tsx"],
files: ["apps/portal/src/app/(authenticated)/layout.tsx"],
rules: {
"no-restricted-imports": "off",
},
},
// Allow controlled window.location usage for invoice SSO download
{
files: ["apps/portal/src/app/(portal)/billing/invoices/[id]/page.tsx"],
files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"],
rules: {
"no-restricted-syntax": "off",
},