Compare commits

...

16 Commits

Author SHA1 Message Date
theRAD
0a17fe2fc1 style: apply official stargas favicon to all dashboard layouts 2026-03-06 06:08:16 +02:00
theRAD
4184e5c506 feat: add division task performance placeholder card to landing page 2026-03-06 05:52:32 +02:00
theRAD
02e4b4e53f feat: add sales and marketing placeholder cards to landing page 2026-03-06 05:50:43 +02:00
theRAD
4bef383cf7 added ecommerce data 2026-03-06 05:48:29 +02:00
theRAD
f43448aae9 fix: change date input background to solid dark color 2026-03-06 05:43:38 +02:00
theRAD
91a6cbe4c7 fix: change dropdown background to solid dark color 2026-03-06 05:43:02 +02:00
theRAD
ef97bc4a2e fix: resolve syntax parse error in ecommerce controller 2026-03-06 05:38:03 +02:00
theRAD
38ab842b52 feat: integrate facebook marketing roi on ecommerce dashboard 2026-03-06 05:33:03 +02:00
theRAD
c1e4027db8 feat: replace recent orders with geographic and retention insights 2026-03-06 05:30:06 +02:00
theRAD
ff8570b526 feat: add advanced device and referral analytics charts 2026-03-06 05:24:15 +02:00
theRAD
15d2c5d12e fix: format dates correctly for WooCommerce reports endpoint 2026-03-06 05:18:16 +02:00
theRAD
dbf8118492 feat: complete e-commerce dashboard and woocommerce api integration 2026-03-06 05:14:14 +02:00
theRAD
fd87ac0b93 feat: replace sales/inventory with e-commerce dashboard and settings 2026-03-06 05:04:55 +02:00
theRAD
3fc2f3340b Add backend admin page 2026-03-06 05:01:13 +02:00
theRAD
d404054887 feat: add admin settings panel and dashboard configurations 2026-03-06 04:58:29 +02:00
theRAD
d705160b55 feat: implement centralized dashboard landing page and move procurement dashboard 2026-03-06 04:51:03 +02:00
25 changed files with 4127 additions and 2 deletions

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Setting;
class AdminController extends Controller
{
public function index()
{
return view('admin.index');
}
public function dashboardSettings($dashboard)
{
// Define available settings per dashboard here for simplicity
$schema = [];
if ($dashboard === 'procurement') {
$schema = [
['key' => 'target_margin', 'name' => 'Target Margin (%)', 'type' => 'integer', 'default' => 20, 'description' => 'Target gross margin percentage for alerts.'],
['key' => 'low_stock_threshold', 'name' => 'Low Stock Warning', 'type' => 'integer', 'default' => 50, 'description' => 'Quantity at which a product is considered low stock.'],
['key' => 'default_date_range', 'name' => 'Default Date Filter', 'type' => 'string', 'default' => 'YTD', 'description' => 'Default date range on load (e.g. YTD, All).'],
];
} else if ($dashboard === 'ecommerce') {
$schema = [
['key' => 'woo_store_url', 'name' => 'WooCommerce Store URL', 'type' => 'string', 'default' => 'https://example.com', 'description' => 'The base URL of your WooCommerce store (e.g. https://shop.stargas.co.za).'],
['key' => 'woo_consumer_key', 'name' => 'Consumer Key', 'type' => 'string', 'default' => '', 'description' => 'WooCommerce REST API Consumer Key (ck_...).'],
['key' => 'woo_consumer_secret', 'name' => 'Consumer Secret', 'type' => 'string', 'default' => '', 'description' => 'WooCommerce REST API Consumer Secret (cs_...).'],
['key' => 'fb_app_id', 'name' => 'Facebook App ID', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Application ID.'],
['key' => 'fb_app_secret', 'name' => 'Facebook App Secret', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Application Secret.'],
['key' => 'fb_access_token', 'name' => 'Facebook Access Token', 'type' => 'string', 'default' => '', 'description' => 'Long-lived Facebook Access Token for API calls.'],
['key' => 'fb_ad_account_id', 'name' => 'Facebook Ad Account ID', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Ad Account ID (e.g. act_123456789).'],
];
} else {
abort(404);
}
// Fetch current settings from DB
$settings = Setting::where('dashboard', $dashboard)->get()->keyBy('key');
return view('admin.settings', compact('dashboard', 'schema', 'settings'));
}
public function updateSettings(Request $request, $dashboard)
{
$settingsData = $request->except('_token');
foreach ($settingsData as $key => $value) {
Setting::updateOrCreate(
['dashboard' => $dashboard, 'key' => $key],
['value' => $value]
);
}
return redirect()->back()->with('success', ucfirst($dashboard) . ' settings saved successfully!');
}
}

View File

@@ -0,0 +1,496 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Models\Setting;
use Illuminate\Support\Facades\Log;
class EcommerceController extends Controller
{
/**
* Display the index view.
*/
public function index()
{
return view('ecommerce');
}
/**
* Get a configured WP HTTP client to easily make requests.
*/
private function wooClient()
{
$url = Setting::getValue('ecommerce', 'woo_store_url', '');
$key = Setting::getValue('ecommerce', 'woo_consumer_key', '');
$secret = Setting::getValue('ecommerce', 'woo_consumer_secret', '');
if (!$url || !$key || !$secret) {
Log::error('Missing WooCommerce API Credentials in settings.');
return null;
}
// Clean trailing slashes
$baseUrl = rtrim($url, '/');
return Http::withBasicAuth($key, $secret)->baseUrl($baseUrl . '/wp-json/wc/v3/');
}
/**
* Get start and end dates from the request (YYYY-MM-DD).
*/
private function dateRange(Request $request): array
{
$from = $request->query('from');
$to = $request->query('to');
if (!$from && !$to) {
return [null, null];
}
return [
$from ?: null,
$to ?: null,
];
}
/**
* Summary metrics (Net Sales, Total Orders, Total Customers).
*/
public function summary(Request $request)
{
$client = $this->wooClient();
if (!$client) {
return response()->json(['error' => 'API credentials missing.'], 500);
}
[$from, $to] = $this->dateRange($request);
$params = [];
if ($from) $params['date_min'] = $from;
if ($to) $params['date_max'] = $to;
try {
// Get Sales Report
$salesResp = $client->get('reports/sales', $params);
$salesData = $salesResp->successful() ? $salesResp->json() : [];
$totals = !empty($salesData) ? $salesData[0] : [
'total_sales' => 0,
'net_sales' => 0,
'total_orders' => 0
];
// For total customers, we count users with role customer.
$custParams = ['role' => 'customer', 'per_page' => 1];
$custResp = $client->get('customers', $custParams);
$totalCustomers = 0;
if ($custResp->successful() && $custResp->header('X-WP-Total')) {
$totalCustomers = (int) $custResp->header('X-WP-Total');
}
return response()->json([
'net_sales' => (float) ($totals['net_sales'] ?? 0),
'total_sales' => (float) ($totals['total_sales'] ?? 0),
'total_orders' => (int) ($totals['total_orders'] ?? 0),
'total_customers' => $totalCustomers,
'avg_order_value' => ($totals['total_orders'] ?? 0) > 0 ? ((float) ($totals['net_sales'] ?? 0)) / (int) $totals['total_orders'] : 0,
]);
} catch (\Exception $e) {
Log::error('WooCommerce API Error (summary): ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
}
}
/**
* Recent Orders list.
*/
public function recentOrders(Request $request)
{
$client = $this->wooClient();
if (!$client) {
return response()->json(['error' => 'API credentials missing.'], 500);
}
[$from, $to] = $this->dateRange($request);
$params = [
'per_page' => 10,
'orderby' => 'date',
'order' => 'desc',
];
if ($from) $params['after'] = $from . 'T00:00:00';
if ($to) $params['before'] = $to . 'T23:59:59';
try {
$resp = $client->get('orders', $params);
$data = $resp->successful() ? $resp->json() : [];
$orders = array_map(function($order) {
return [
'id' => $order['id'],
'number' => $order['number'],
'status' => $order['status'],
'total' => (float) $order['total'],
'currency' => $order['currency'],
'date_created' => $order['date_created'],
'customer' => $order['billing']['first_name'] . ' ' . $order['billing']['last_name'],
];
}, $data);
return response()->json($orders);
} catch (\Exception $e) {
Log::error('WooCommerce API Error (orders): ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
}
}
/**
* Top Selling Products list.
*/
public function topProducts(Request $request)
{
$client = $this->wooClient();
if (!$client) {
return response()->json(['error' => 'API credentials missing.'], 500);
}
[$from, $to] = $this->dateRange($request);
$params = [
'per_page' => 10,
];
if ($from) $params['date_min'] = $from;
if ($to) $params['date_max'] = $to;
try {
$resp = $client->get('reports/top_sellers', $params);
$data = $resp->successful() ? $resp->json() : [];
return response()->json($data);
} catch (\Exception $e) {
Log::error('WooCommerce API Error (top_sellers): ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
}
}
/**
* Extensive Analytics (Device Types, Referrals).
*/
public function analytics(Request $request)
{
$client = $this->wooClient();
if (!$client) {
return response()->json(['error' => 'API credentials missing.'], 500);
}
[$from, $to] = $this->dateRange($request);
// Fetch up to 100 recent orders for analytics sampling
$params = [
'per_page' => 100,
'orderby' => 'date',
'order' => 'desc',
];
if ($from) $params['after'] = $from . 'T00:00:00';
if ($to) $params['before'] = $to . 'T23:59:59';
try {
$resp = $client->get('orders', $params);
$orders = $resp->successful() ? $resp->json() : [];
$devices = [];
$sources = [];
foreach ($orders as $order) {
if (!isset($order['meta_data'])) continue;
$device = 'Unknown';
$source = 'Direct / Organic';
foreach ($order['meta_data'] as $meta) {
if ($meta['key'] === '_wc_order_attribution_device_type') {
$device = ucfirst(strtolower($meta['value']));
}
if ($meta['key'] === '_wc_order_attribution_utm_source') {
$source = ucfirst(strtolower($meta['value']));
} elseif ($meta['key'] === '_wc_order_attribution_source_type' && strtolower($meta['value']) === 'organic') {
if ($source === 'Direct / Organic') {
$source = 'Organic Search';
}
}
}
// Aggregate Device
if (!isset($devices[$device])) {
$devices[$device] = 0;
}
$devices[$device]++;
// Aggregate Source
if (!isset($sources[$source])) {
$sources[$source] = 0;
}
$sources[$source]++;
}
// Sort sources by count DESC
arsort($sources);
$topSources = array_slice($sources, 0, 5, true);
return response()->json([
'devices' => [
'labels' => array_keys($devices),
'values' => array_values($devices)
],
'sources' => [
'labels' => array_keys($topSources),
'values' => array_values($topSources)
]
]);
} catch (\Exception $e) {
Log::error('WooCommerce API Error (analytics): ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
}
}
/**
* Customer Insights (Geographic Distribution, Retention).
*/
public function insights(Request $request)
{
$client = $this->wooClient();
if (!$client) {
return response()->json(['error' => 'API credentials missing.'], 500);
}
[$from, $to] = $this->dateRange($request);
// Fetch up to 100 recent orders for insights
$params = [
'per_page' => 100,
'orderby' => 'date',
'order' => 'desc',
'status' => ['completed', 'processing']
];
if ($from) $params['after'] = $from . 'T00:00:00';
if ($to) $params['before'] = $to . 'T23:59:59';
try {
$resp = $client->get('orders', $params);
$orders = $resp->successful() ? $resp->json() : [];
$provinces = [];
$guests = 0;
$newCustomers = 0;
$returningCustomers = 0;
// Simple map for ZA provinces (WooCommerce standard abbreviations)
$zaProvinces = [
'GT' => 'Gauteng',
'WC' => 'Western Cape',
'KZN' => 'KwaZulu-Natal',
'EC' => 'Eastern Cape',
'FS' => 'Free State',
'MP' => 'Mpumalanga',
'NW' => 'North West',
'NC' => 'Northern Cape',
'NL' => 'Limpopo',
'LP' => 'Limpopo' // Sometimes LP is used
];
// Local cache to count orders per returning customer ID
$customerOrderCounts = [];
foreach ($orders as $order) {
// Geographic
if (isset($order['billing']) && !empty($order['billing']['state'])) {
$stateCode = strtoupper($order['billing']['state']);
$stateName = $zaProvinces[$stateCode] ?? $stateCode;
if (!isset($provinces[$stateName])) {
$provinces[$stateName] = 0;
}
$provinces[$stateName] += (float) $order['total'];
}
// Retention
$custId = (int) $order['customer_id'];
if ($custId === 0) {
$guests++;
} else {
if (!isset($customerOrderCounts[$custId])) {
// Optimistic assumption: if we haven't seen them in this batch,
// we can check if they have previous orders (or simplistically
// assume all first occurrences in this 100 order batch are 'New',
// and subsequent are 'Returning' in the timeframe).
// Since we sort by date descending, the FIRST time we process them is their latest order.
$customerOrderCounts[$custId] = 1;
} else {
$customerOrderCounts[$custId]++;
}
}
}
// Refine Returners: Anyone with > 1 order in this batch is considered a returner for this simplified metric.
// Ideally, you'd check WC Customer data to see 'orders_count', but this requires a separate API call per user
// To keep it fast, we estimate based on the batch. Or, better yet, we hit the customer endpoint if we have few unique customers.
// For now, let's use the batch estimation:
foreach ($customerOrderCounts as $id => $count) {
if ($count > 1) {
$returningCustomers++;
} else {
$newCustomers++;
}
}
// Sort provinces by total ZAR DESC
arsort($provinces);
// Cap at top 6 regions
$topProvinces = array_slice($provinces, 0, 6, true);
return response()->json([
'geographic' => [
'labels' => array_keys($topProvinces),
'values' => array_values($topProvinces)
],
'retention' => [
'labels' => ['New Accounts', 'Returning Accounts', 'Guest Checkouts'],
'values' => [$newCustomers, $returningCustomers, $guests]
]
]);
} catch (\Exception $e) {
Log::error('WooCommerce API Error (insights): ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch WooCommerce insights data.'], 500);
}
}
/**
* Marketing & ROI (Facebook Ads + WooCommerce)
*/
public function marketing(Request $request)
{
$fbAppId = Setting::getValue('ecommerce', 'fb_app_id');
$fbToken = Setting::getValue('ecommerce', 'fb_access_token');
$fbAccountId = Setting::getValue('ecommerce', 'fb_ad_account_id');
if (!$fbToken || !$fbAccountId) {
return response()->json(['error' => 'Facebook credentials not configured in Admin settings.', 'configured' => false], 200);
}
// Base WooCommerce Sales calculation to determine ROAS
$client = $this->wooClient();
$netSales = 0;
[$from, $to] = $this->dateRange($request);
if ($client) {
$params = [];
if ($from) $params['date_min'] = $from;
if ($to) $params['date_max'] = $to;
try {
$salesResp = $client->get('reports/sales', $params);
if ($salesResp->successful()) {
$salesData = $salesResp->json();
if (!empty($salesData)) {
$netSales = (float) ($salesData[0]['net_sales'] ?? 0);
}
}
} catch (\Exception $e) {
// Silently drop woo error mapped to marketing
}
}
// Facebook Date Filtering
$fbParams = [
'level' => 'account',
'fields' => 'spend,impressions,clicks,cpc,cpa',
];
if ($from && $to) {
$fbParams['time_range'] = json_encode(['since' => $from, 'until' => $to]);
} else {
// Default to last 30 days if no explicit date range is given
$fbParams['date_preset'] = 'last_30d';
}
try {
// Remove 'act_' prefix if user included it, then add it back to be safe
$accountId = str_replace('act_', '', $fbAccountId);
$fbUrl = "https://graph.facebook.com/v19.0/act_{$accountId}";
// 1. Fetch Account-level Insights
$fbResp = Http::withToken($fbToken)->get("{$fbUrl}/insights", $fbParams);
$spend = 0;
$impressions = 0;
$clicks = 0;
if ($fbResp->successful()) {
$fbData = $fbResp->json();
if (isset($fbData['data']) && count($fbData['data']) > 0) {
$insight = $fbData['data'][0];
$spend = (float) ($insight['spend'] ?? 0);
$impressions = (int) ($insight['impressions'] ?? 0);
$clicks = (int) ($insight['clicks'] ?? 0);
}
}
// 2. Fetch Top Campaigns by Spend
$campParams = [
'level' => 'campaign',
'fields' => 'campaign_name,spend,clicks,impressions',
'sort' => ['spend_descending'],
'limit' => 5
];
if ($from && $to) {
$campParams['time_range'] = json_encode(['since' => $from, 'until' => $to]);
} else {
$campParams['date_preset'] = 'last_30d';
}
$campResp = Http::withToken($fbToken)->get("{$fbUrl}/insights", $campParams);
$campaigns = [];
if ($campResp->successful()) {
$campData = $campResp->json();
if (isset($campData['data'])) {
foreach ($campData['data'] as $cmp) {
$campaigns[] = [
'name' => $cmp['campaign_name'] ?? 'Unknown',
'spend' => (float) ($cmp['spend'] ?? 0),
'clicks' => (int) ($cmp['clicks'] ?? 0),
'impressions' => (int) ($cmp['impressions'] ?? 0),
];
}
}
}
// Calculate ROAS / CPC
$roas = $spend > 0 ? ($netSales / $spend) : 0;
$cpc = $clicks > 0 ? ($spend / $clicks) : 0;
return response()->json([
'configured' => true,
'spend' => $spend,
'impressions' => $impressions,
'clicks' => $clicks,
'cpc' => $cpc,
'roas' => $roas,
'woo_net_sales' => $netSales,
'campaigns' => $campaigns
]);
} catch (\Exception $e) {
Log::error('Facebook API Error: ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch Facebook data.'], 500);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = [
'dashboard',
'key',
'value',
'type',
'name',
'description',
];
/**
* Get a setting value, returning the provided default if not found.
*/
public static function getValue($dashboard, $key, $default = null)
{
$setting = self::where('dashboard', $dashboard)->where('key', $key)->first();
if (!$setting) {
return $default;
}
switch ($setting->type) {
case 'boolean':
return filter_var($setting->value, FILTER_VALIDATE_BOOLEAN);
case 'integer':
return (int) $setting->value;
case 'json':
return json_decode($setting->value, true);
default:
return $setting->value;
}
}
}

26
src/apply_indexes_safe.php Executable file
View File

@@ -0,0 +1,26 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
use Illuminate\Support\Facades\DB;
$sql = file_get_contents('/var/www/gauges.stargas.co.za/add_indexes.sql');
$queries = array_filter(array_map('trim', explode(';', $sql)));
foreach ($queries as $query) {
if (empty($query) || str_starts_with($query, '--')) continue;
echo "Running: " . substr($query, 0, 60) . "...\n";
try {
DB::unprepared($query);
echo "Success.\n";
} catch (\Exception $e) {
if (str_contains($e->getMessage(), 'Duplicate key name')) {
echo "Index already exists, skipping.\n";
} else {
echo "Error: " . $e->getMessage() . "\n";
}
}
}
echo "Done.\n";

0
src/bootstrap/cache/.gitignore vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('dashboard')->index(); // e.g., 'procurement', 'sales', 'global'
$table->string('key');
$table->text('value')->nullable();
$table->string('type')->default('string'); // string, boolean, integer, json
$table->string('name')->nullable(); // Human-readable name
$table->text('description')->nullable();
$table->timestamps();
$table->unique(['dashboard', 'key']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('settings');
}
};

2421
src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

BIN
src/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,28 @@
@extends('admin.layout')
@section('content')
<div class="mb-8">
<h2 class="text-2xl font-bold text-white tracking-tight">System Configuration Overview</h2>
<p class="text-slate-400 mt-2">Manage dashboard settings and globally defined parameters.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<a href="/admin/settings/procurement" class="glass rounded-xl p-6 group transition-all duration-300 hover:border-red-500/50 hover:bg-white/5 block">
<div class="w-12 h-12 rounded-lg bg-red-500/20 text-red-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<h3 class="text-lg font-bold text-white mb-2">Procurement Settings</h3>
<p class="text-sm text-slate-400">Configure thresholds, margins, and default variables for the procurement module.</p>
</a>
<a href="/admin/settings/ecommerce" class="glass rounded-xl p-6 group transition-all duration-300 hover:border-purple-500/50 hover:bg-white/5 block">
<div class="w-12 h-12 rounded-lg bg-purple-500/20 text-purple-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
</div>
<h3 class="text-lg font-bold text-slate-300 mb-2">E-Commerce Settings</h3>
<p class="text-sm text-slate-500">Configure WooCommerce API and Facebook Business credentials for the marketing dashboard.</p>
</a>
</div>
@endsection

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - Stargas Dashboards</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="icon" type="image/png" href="/favicon.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%);
min-height: 100vh;
font-family: 'Inter', sans-serif;
}
.glass {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
border: 1px solid rgba(227, 25, 55, 0.12);
}
.stargas-accent {
background: linear-gradient(90deg, #E31937, #FFD700, #E31937);
height: 2px;
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
@keyframes fadeIn {
to { opacity: 1; transform: translateY(0); }
from { opacity: 0; transform: translateY(10px); }
}
/* Nav links hover glow */
.nav-link.active {
background: rgba(227, 25, 55, 0.15);
border-right: 3px solid #E31937;
color: white;
}
.nav-link:hover:not(.active) {
background: rgba(255, 255, 255, 0.05);
color: #fca5a5;
}
</style>
</head>
<body class="text-white antialiased flex flex-col min-h-screen">
<!-- Stargas Accent Line -->
<div class="stargas-accent"></div>
<!-- Header -->
<header class="glass sticky top-0 z-50 border-b border-white/5">
<div class="max-w-[1600px] mx-auto px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<img src="/images/stargas-logo.svg" alt="Stargas" class="h-10">
<div class="border-l border-white/10 pl-4">
<h1 class="text-lg font-bold tracking-tight text-red-400">Admin Panel</h1>
<p class="text-xs text-slate-400">System Configuration</p>
</div>
</div>
<div class="flex items-center gap-4">
<a href="/" class="px-3 py-2 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10 flex items-center gap-1.5 text-slate-300">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
Hub
</a>
</div>
</div>
</header>
<div class="flex-1 flex max-w-[1600px] w-full mx-auto">
<!-- Sidebar Navigation -->
<aside class="w-64 glass border-x-0 border-y-0 border-r border-white/5 flex-shrink-0 pt-8">
<nav class="space-y-1">
<div class="px-6 mb-4">
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider">Dashboards</h3>
</div>
<a href="/admin" class="nav-link flex items-center gap-3 px-6 py-3 text-sm font-medium text-slate-400 transition-colors {{ request()->is('admin') ? 'active' : '' }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Overview
</a>
<a href="/admin/settings/procurement" class="nav-link flex items-center gap-3 px-6 py-3 text-sm font-medium text-slate-400 transition-colors {{ request()->is('admin/settings/procurement') ? 'active' : '' }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Procurement
</a>
<a href="/admin/settings/ecommerce" class="nav-link flex items-center gap-3 px-6 py-3 text-sm font-medium text-slate-400 transition-colors {{ request()->is('admin/settings/ecommerce') ? 'active' : '' }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
E-Commerce
</a>
</nav>
</aside>
<!-- Main Content Area -->
<main class="flex-1 p-8 fade-in opacity-0">
@if(session('success'))
<div class="mb-6 p-4 rounded-lg bg-green-500/10 border border-green-500/30 flex items-start gap-3 fade-in">
<svg class="w-5 h-5 text-green-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<div>
<h4 class="text-sm font-medium text-green-400">Success</h4>
<div class="mt-1 text-sm text-green-400/80">{{ session('success') }}</div>
</div>
</div>
@endif
@yield('content')
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,54 @@
@extends('admin.layout')
@section('content')
<div class="mb-8 flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-white tracking-tight">{{ ucfirst($dashboard) }} Settings</h2>
<p class="text-slate-400 mt-2">Adjust constants and parameters specific to this dashboard.</p>
</div>
</div>
<form action="/admin/settings/{{ $dashboard }}" method="POST" class="glass rounded-2xl p-8 max-w-3xl border-t-4 border-t-red-700">
@csrf
<div class="space-y-6">
@foreach($schema as $field)
@php
// Retrieve existing value from DB or fallback to default
$currentValue = isset($settings[$field['key']]) ? $settings[$field['key']]->value : $field['default'];
@endphp
<div class="border-b border-white/5 pb-6 last:border-0 last:pb-0">
<label for="{{ $field['key'] }}" class="block text-sm font-semibold text-slate-200 mb-1">
{{ $field['name'] }}
</label>
<p class="text-xs text-slate-500 mb-3">{{ $field['description'] }}</p>
@if($field['type'] === 'boolean')
<div class="flex items-center gap-3">
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="{{ $field['key'] }}" value="0">
<input type="checkbox" name="{{ $field['key'] }}" id="{{ $field['key'] }}" value="1" class="sr-only peer" {{ $currentValue ? 'checked' : '' }}>
<div class="w-11 h-6 bg-white/10 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-red-500/50 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-red-600"></div>
</label>
</div>
@else
<input type="{{ $field['type'] === 'integer' ? 'number' : 'text' }}"
name="{{ $field['key'] }}"
id="{{ $field['key'] }}"
value="{{ $currentValue }}"
class="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm text-white focus:border-red-500 focus:outline-none focus:ring-1 focus:ring-red-500/30 transition-colors"
>
@endif
</div>
@endforeach
</div>
<div class="mt-8 pt-6 border-t border-white/10 flex items-center justify-end">
<button type="submit" class="px-6 py-2.5 text-sm font-semibold rounded-lg bg-red-700 hover:bg-red-600 border border-red-500/50 transition-colors shadow-lg shadow-red-900/20 text-white flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>
Save Changes
</button>
</div>
</form>
@endsection

View File

@@ -9,6 +9,7 @@
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700,800" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=inter:400,500,600,700,800" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<link rel="icon" type="image/png" href="/favicon.png">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<style> <style>
@@ -105,11 +106,15 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<img src="/images/stargas-logo.svg" alt="Stargas Energies" class="h-12"> <img src="/images/stargas-logo.svg" alt="Stargas Energies" class="h-12">
<div class="border-l border-white/10 pl-4"> <div class="border-l border-white/10 pl-4">
<h1 class="text-lg font-bold tracking-tight">Supplier Dashboard</h1> <h1 class="text-lg font-bold tracking-tight">Procurement Dashboard</h1>
<p class="text-xs text-slate-400">Performance Analytics</p> <p class="text-xs text-slate-400">Performance Analytics</p>
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<a href="/" class="px-3 py-2 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10 flex items-center gap-1.5 text-slate-300">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Hub
</a>
<span id="last-updated" class="text-xs text-slate-500"></span> <span id="last-updated" class="text-xs text-slate-500"></span>
<button onclick="loadAllData()" class="px-4 py-2 text-xs font-medium rounded-lg bg-red-700 hover:bg-red-600 transition-colors flex items-center gap-2 border border-red-600/30"> <button onclick="loadAllData()" class="px-4 py-2 text-xs font-medium rounded-lg bg-red-700 hover:bg-red-600 transition-colors flex items-center gap-2 border border-red-600/30">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>

View File

@@ -0,0 +1,630 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Commerce Dashboard - Stargas</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="icon" type="image/png" href="/favicon.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%); min-height: 100vh; font-family: 'Inter', sans-serif; }
.glass { background: rgba(255, 255, 255, 0.04); backdrop-filter: blur(12px); border: 1px solid rgba(227, 25, 55, 0.12); }
.stargas-accent { background: linear-gradient(90deg, #E31937, #FFD700, #E31937); height: 2px; }
/* Shimmer effect for loading states */
.loading-shimmer {
background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 37%, rgba(255,255,255,0.03) 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
@keyframes shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
</style>
</head>
<body class="text-white antialiased">
<!-- Stargas Accent Line -->
<div class="stargas-accent"></div>
<!-- Header -->
<header class="glass sticky top-0 z-50 border-b border-white/5">
<div class="max-w-[1600px] mx-auto px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<img src="/images/stargas-logo.svg" alt="Stargas Energies" class="h-12">
<div class="border-l border-white/10 pl-4">
<h1 class="text-lg font-bold tracking-tight text-purple-400">E-Commerce Dashboard</h1>
<p class="text-xs text-slate-400">WooCommerce Analytics</p>
</div>
</div>
<div class="flex items-center gap-4">
<a href="/" class="px-3 py-2 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10 flex items-center gap-1.5 text-slate-300">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Hub
</a>
<span id="last-updated" class="text-xs text-slate-500"></span>
<button onclick="loadAllData()" class="px-4 py-2 text-xs font-medium rounded-lg bg-purple-700 hover:bg-purple-600 transition-colors flex items-center gap-2 border border-purple-600/30">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Refresh
</button>
</div>
</div>
<!-- Date Range Filter Bar -->
<div class="max-w-[1600px] mx-auto px-6 py-2 flex items-center gap-3 border-t border-white/5">
<svg class="w-4 h-4 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<label class="text-xs text-slate-400">Financial Year</label>
<select id="filter-fy" onchange="applyFinancialYear()" class="bg-slate-900 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30">
<option value="" class="bg-slate-900 text-white">Custom / YTD</option>
<script>
const currentYear = new Date().getFullYear();
const currentFYStart = new Date().getMonth() < 2 ? currentYear - 1 : currentYear;
for (let y = currentFYStart; y >= 2020; y--) {
document.write(`<option value="${y}" class="bg-slate-900 text-white">FY ${y}/${(y+1).toString().substring(2)}</option>`);
}
</script>
</select>
<div class="h-4 border-l border-white/10 mx-1"></div>
<label class="text-xs text-slate-400">From</label>
<input type="date" id="filter-from" class="bg-slate-900 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30" />
<label class="text-xs text-slate-400">To</label>
<input type="date" id="filter-to" class="bg-slate-900 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30" />
<button onclick="loadAllData()" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-purple-700/80 hover:bg-purple-600 transition-colors border border-purple-600/30">Apply</button>
<button onclick="clearDateFilter()" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10">Clear</button>
<span id="filter-status" class="text-xs text-slate-500 ml-auto"></span>
</div>
</header>
<main class="max-w-[1600px] mx-auto px-6 py-8 space-y-8">
<div id="api-error" class="hidden glass border-red-500/50 bg-red-500/10 p-4 rounded-xl text-red-400 text-sm font-medium flex-col gap-2">
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
WooCommerce API Connection Error
</div>
<p class="text-xs text-red-400/80 font-normal">Please check your WooCommerce credentials in the <a href="/admin/settings/ecommerce" class="underline hover:text-white">Admin Panel</a>.</p>
</div>
<!-- KPI Cards -->
<section class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-4">
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Net Sales</span>
<div class="w-8 h-8 rounded-lg bg-purple-500/10 flex items-center justify-center text-purple-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-sales" class="text-2xl font-bold text-white tracking-tight">R 0.00</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Total Orders</span>
<div class="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-orders" class="text-2xl font-bold text-white tracking-tight">0</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Avg Order Value</span>
<div class="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-aov" class="text-2xl font-bold text-white tracking-tight">R 0.00</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Total Customers</span>
<div class="w-8 h-8 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-customers" class="text-2xl font-bold text-white tracking-tight">0</div>
</div>
</div>
</section>
<!-- Tables Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Top Selling Products -->
<div class="glass rounded-2xl flex flex-col h-full overflow-hidden">
<div class="p-5 border-b border-white/5 flex items-center justify-between bg-white/5">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/></svg>
Top Selling Products
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-black/20 text-xs text-slate-400 uppercase tracking-wider">
<th class="p-3 font-medium">Product</th>
<th class="p-3 font-medium text-right">Qty Sold</th>
</tr>
</thead>
<tbody id="table-products" class="text-sm divide-y divide-white/5">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
<!-- Geographic Sales Distribution -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Geographic Sales Distribution
</h3>
</div>
<div class="relative w-full h-[300px]">
<canvas id="geoChart"></canvas>
</div>
</div>
</div>
<!-- Marketing & Growth Row -->
<h2 class="text-xl font-bold text-white mb-4 mt-8 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Marketing & Growth (Meta Ads)
</h2>
<div id="marketing-unconfigured" class="glass rounded-2xl p-8 flex flex-col items-center justify-center text-center mb-8 hidden">
<svg class="w-12 h-12 text-slate-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<h3 class="text-lg font-bold text-white mb-2">Connect Facebook Business</h3>
<p class="text-sm text-slate-400 max-w-md">Unlock powerful Return on Ad Spend (ROAS) analytics by configuring your Facebook Graph API credentials in the Admin Panel.</p>
<a href="/admin/settings/ecommerce" class="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors">Configure Now</a>
</div>
<div id="marketing-dashboard" class="mb-8 hidden">
<!-- Marketing KPIs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<!-- Ad Spend -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-blue-500/5 group-hover:bg-blue-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Ad Spend</p>
<p class="text-2xl font-bold text-white" id="mkt-spend"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- ROAS -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-purple-500/5 group-hover:bg-purple-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">ROAS</p>
<p class="text-2xl font-bold text-emerald-400" id="mkt-roas"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- CPC -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-rose-500/5 group-hover:bg-rose-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Cost Per Click</p>
<p class="text-2xl font-bold text-white" id="mkt-cpc"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- Clicks -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-orange-500/5 group-hover:bg-orange-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Link Clicks</p>
<p class="text-2xl font-bold text-white" id="mkt-clicks"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
</div>
<!-- Top Campaigns Table -->
<div class="glass rounded-2xl flex flex-col overflow-hidden">
<div class="p-5 border-b border-white/5 flex items-center justify-between bg-white/5">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/></svg>
Top Performing Campaigns
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-black/20 text-xs text-slate-400 uppercase tracking-wider">
<th class="p-3 font-medium">Campaign Name</th>
<th class="p-3 font-medium text-right">Spend</th>
<th class="p-3 font-medium text-right">Clicks</th>
<th class="p-3 font-medium text-right">CPC</th>
</tr>
</thead>
<tbody id="table-campaigns" class="text-sm divide-y divide-white/5">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
</div>
<h2 class="text-xl font-bold text-white mb-4 mt-8 flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Advanced Custom Analytics
</h2>
<!-- Advanced Analytics Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Customer Retention -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Customer Retention
</h3>
</div>
<div class="relative w-full max-w-[240px] aspect-square">
<canvas id="retentionChart"></canvas>
</div>
</div>
<!-- Device Breakthrough -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
Device Breakdown
</h3>
</div>
<div class="relative w-full max-w-[240px] aspect-square">
<canvas id="deviceChart"></canvas>
</div>
</div>
<!-- Top Referral Sources -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Top Referral Sources
</h3>
</div>
<div class="relative w-full h-[240px] mt-4">
<canvas id="sourceChart"></canvas>
</div>
</div>
</div>
</main>
<!-- JS Logic -->
<script>
const fmtNum = new Intl.NumberFormat('en-ZA').format;
const fmt = new Intl.NumberFormat('en-ZA', { style: 'currency', currency: 'ZAR', minimumFractionDigits: 0 }).format;
// Date filter helpers
function getDateParams() {
let from = document.getElementById('filter-from').value;
let to = document.getElementById('filter-to').value;
// Default to YTD
if (!from && !to) {
const now = new Date();
from = `${now.getFullYear()}-01-01`;
to = now.toISOString().split('T')[0];
document.getElementById('filter-from').value = from;
document.getElementById('filter-to').value = to;
setTimeout(updateFilterStatus, 50);
}
const params = new URLSearchParams();
if (from) params.set('from', from);
if (to) params.set('to', to);
return params.toString();
}
function clearDateFilter() {
document.getElementById('filter-fy').value = '';
document.getElementById('filter-from').value = '';
document.getElementById('filter-to').value = '';
loadAllData();
}
function applyFinancialYear() {
const fyYear = document.getElementById('filter-fy').value;
if (!fyYear) return;
const startYear = parseInt(fyYear);
const endYear = startYear + 1;
const from = `${startYear}-03-01`;
const isLeapYear = (endYear % 4 === 0 && endYear % 100 !== 0) || (endYear % 400 === 0);
const to = `${endYear}-02-${isLeapYear ? '29' : '28'}`;
document.getElementById('filter-from').value = from;
document.getElementById('filter-to').value = to;
updateFilterStatus();
loadAllData();
}
function updateFilterStatus() {
const from = document.getElementById('filter-from').value;
const to = document.getElementById('filter-to').value;
const el = document.getElementById('filter-status');
if (from || to) {
const parts = [];
if (from) parts.push('from ' + from);
if (to) parts.push('to ' + to);
el.textContent = 'Filtered: ' + parts.join(' ');
el.classList.add('text-purple-400');
el.classList.remove('text-slate-500');
} else {
el.textContent = 'Showing all time';
}
}
// Fetch data wrapper
async function fetchJson(url) {
const qs = getDateParams();
const separator = url.includes('?') ? '&' : '?';
const fullUrl = qs ? url + separator + qs : url;
const res = await fetch(fullUrl);
const data = await res.json();
if (!res.ok) {
document.getElementById('api-error').classList.remove('hidden');
document.getElementById('api-error').classList.add('flex');
throw new Error(data.error || `Failed to fetch ${fullUrl}`);
} else {
document.getElementById('api-error').classList.add('hidden');
document.getElementById('api-error').classList.remove('flex');
}
return data;
}
function setKpisLoading() {
const ids = ['kpi-sales', 'kpi-orders', 'kpi-aov', 'kpi-customers'];
ids.forEach(id => {
document.getElementById(id).innerHTML = '<div class="h-8 w-24 rounded loading-shimmer"></div>';
});
}
async function loadSummary() {
setKpisLoading();
try {
const d = await fetchJson('/api/ecommerce/summary');
document.getElementById('kpi-sales').textContent = fmt(d.net_sales);
document.getElementById('kpi-orders').textContent = fmtNum(d.total_orders);
document.getElementById('kpi-aov').textContent = fmt(d.avg_order_value);
document.getElementById('kpi-customers').textContent = fmtNum(d.total_customers);
} catch (e) {
console.error(e);
}
}
async function loadTopProducts() {
const tb = document.getElementById('table-products');
tb.innerHTML = Array(5).fill('<tr><td colspan="2" class="p-3"><div class="h-4 w-full rounded loading-shimmer"></div></td></tr>').join('');
try {
const products = await fetchJson('/api/ecommerce/top-products');
if (!products || products.length === 0) {
tb.innerHTML = '<tr><td colspan="2" class="p-4 text-center text-slate-500">No products found for this period.</td></tr>';
return;
}
tb.innerHTML = products.map(p => `
<tr class="hover:bg-white/5 transition-colors">
<td class="p-3 font-medium text-white truncate max-w-[200px]" title="${p.name}">${p.name || 'Unknown Product'}</td>
<td class="p-3 text-right font-medium text-blue-400">${fmtNum(p.quantity)}</td>
</tr>
`).join('');
} catch(e) { console.error(e); }
}
let deviceChartInstance = null;
let sourceChartInstance = null;
let geoChartInstance = null;
let retentionChartInstance = null;
// Custom darker Chart JS defaults for dark UI
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.05)';
Chart.defaults.font.family = "'Inter', sans-serif";
async function loadInsights() {
try {
const data = await fetchJson('/api/ecommerce/insights');
// Geo Bar Chart
const ctxGeo = document.getElementById('geoChart').getContext('2d');
if (geoChartInstance) geoChartInstance.destroy();
geoChartInstance = new Chart(ctxGeo, {
type: 'bar',
data: {
labels: data.geographic.labels,
datasets: [{
label: 'Sales (ZAR)',
data: data.geographic.values,
backgroundColor: '#e11d48', // Rose color
borderRadius: 4,
barThickness: 'flex',
maxBarThickness: 40
}]
},
options: {
indexAxis: 'y', // Horizontal bar to accommodate province names
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return fmt(context.raw);
}
}
}
},
scales: {
x: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.05)' } },
y: { grid: { display: false } }
}
}
});
// Retention Doughnut
const ctxRet = document.getElementById('retentionChart').getContext('2d');
if (retentionChartInstance) retentionChartInstance.destroy();
retentionChartInstance = new Chart(ctxRet, {
type: 'doughnut',
data: {
labels: data.retention.labels,
datasets: [{
data: data.retention.values,
backgroundColor: ['#10b981', '#3b82f6', '#475569'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '75%',
plugins: {
legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }
}
}
});
} catch(e) { console.error(e); }
}
async function loadAnalytics() {
try {
const data = await fetchJson('/api/ecommerce/analytics');
// Device Doughnut
const ctxDevice = document.getElementById('deviceChart').getContext('2d');
if (deviceChartInstance) deviceChartInstance.destroy();
deviceChartInstance = new Chart(ctxDevice, {
type: 'doughnut',
data: {
labels: data.devices.labels,
datasets: [{
data: data.devices.values,
backgroundColor: ['#3b82f6', '#8b5cf6', '#10b981', '#64748b'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '75%',
plugins: {
legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }
}
}
});
// Source Bar Chart
const ctxSource = document.getElementById('sourceChart').getContext('2d');
if (sourceChartInstance) sourceChartInstance.destroy();
sourceChartInstance = new Chart(ctxSource, {
type: 'bar',
data: {
labels: data.sources.labels,
datasets: [{
label: 'Orders',
data: data.sources.values,
backgroundColor: '#f97316',
borderRadius: 4,
barThickness: 'flex',
maxBarThickness: 40
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.05)' } },
x: { grid: { display: false } }
}
}
});
} catch(e) { console.error(e); }
}
async function loadMarketing() {
document.querySelectorAll('.mkt-loading').forEach(el => el.classList.remove('hidden'));
const tb = document.getElementById('table-campaigns');
if (tb) tb.innerHTML = Array(5).fill('<tr><td colspan="4" class="p-3"><div class="h-4 w-full rounded loading-shimmer"></div></td></tr>').join('');
try {
const data = await fetchJson('/api/ecommerce/marketing');
if (!data.configured) {
document.getElementById('marketing-unconfigured').classList.remove('hidden');
document.getElementById('marketing-dashboard').classList.add('hidden');
return;
}
document.getElementById('marketing-unconfigured').classList.add('hidden');
document.getElementById('marketing-dashboard').classList.remove('hidden');
document.getElementById('mkt-spend').textContent = fmt(data.spend);
document.getElementById('mkt-roas').textContent = data.roas.toFixed(2) + 'x';
document.getElementById('mkt-cpc').textContent = fmt(data.cpc);
document.getElementById('mkt-clicks').textContent = fmtNum(data.clicks);
if (!data.campaigns || data.campaigns.length === 0) {
tb.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-slate-500">No active campaigns found in this period.</td></tr>';
} else {
tb.innerHTML = data.campaigns.map(c => `
<tr class="hover:bg-white/5 transition-colors">
<td class="p-3 font-medium text-white max-w-[200px] truncate" title="${c.name}">${c.name}</td>
<td class="p-3 text-right font-medium text-blue-400">${fmt(c.spend)}</td>
<td class="p-3 text-right text-slate-300">${fmtNum(c.clicks)}</td>
<td class="p-3 text-right text-slate-300">${fmt(c.clicks > 0 ? (c.spend / c.clicks) : 0)}</td>
</tr>
`).join('');
}
} catch(e) {
console.error(e);
}
}
function loadAllData() {
updateFilterStatus();
loadSummary();
loadTopProducts();
loadAnalytics();
loadInsights();
loadMarketing();
const now = new Date();
document.getElementById('last-updated').textContent = 'Updated ' + now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');
}
// Init
document.addEventListener('DOMContentLoaded', loadAllData);
</script>
</body>
</html>

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stargas Dashboards</title>
<!-- Tailwind via Vite -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="icon" type="image/png" href="/favicon.png">
<!-- Inter Font -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Animated gradient background — Stargas dark theme */
body {
background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%);
min-height: 100vh;
font-family: 'Inter', sans-serif;
}
/* Glass card effect */
.glass {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
border: 1px solid rgba(227, 25, 55, 0.12);
}
/* Dashboard card hover glow */
.dashboard-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(227, 25, 55, 0.25);
border-color: rgba(227, 25, 55, 0.4);
background: rgba(255, 255, 255, 0.06);
}
.disabled-card {
opacity: 0.6;
cursor: not-allowed;
border-style: dashed;
}
/* Stargas accent line */
.stargas-accent {
background: linear-gradient(90deg, #E31937, #FFD700, #E31937);
height: 2px;
}
/* Fade in animation */
.fade-in {
animation: fadeIn 0.8s ease-out forwards;
opacity: 0;
}
@keyframes fadeIn {
to { opacity: 1; transform: translateY(0); }
from { opacity: 0; transform: translateY(20px); }
}
</style>
</head>
<body class="text-white antialiased">
<!-- Stargas Accent Line -->
<div class="stargas-accent"></div>
<!-- Admin Link -->
<div class="absolute top-6 right-6 z-50 fade-in">
<a href="/admin" class="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10 text-xs font-medium text-slate-400 hover:text-white" title="Admin Control Panel">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Settings
</a>
</div>
<main class="max-w-[1200px] mx-auto px-6 py-16">
<!-- Hero Section -->
<div class="flex flex-col items-center justify-center text-center mb-16 fade-in" style="animation-delay: 100ms">
<div class="w-32 h-32 rounded-3xl flex items-center justify-center mb-8 p-4 shadow-[0_0_50px_rgba(227,25,55,0.15)]">
<img src="/images/stargas-logo.svg" alt="Stargas Energies" class="w-full h-auto drop-shadow-lg">
</div>
</div>
<!-- Dashboard Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
<!-- Active: Procurement Dashboard -->
<a href="/procurement" class="dashboard-card glass rounded-2xl p-6 flex flex-col group fade-in relative overflow-hidden block" style="animation-delay: 200ms">
<div class="absolute top-0 right-0 p-4">
<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-400 ring-1 ring-inset ring-green-500/20">Active</span>
</div>
<div class="w-14 h-14 rounded-xl bg-gradient-to-br from-red-600 to-red-900 flex items-center justify-center mb-6 shadow-lg shadow-red-900/40 group-hover:scale-110 transition-transform duration-300">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<h2 class="text-xl font-bold text-white mb-2 group-hover:text-red-400 transition-colors">Procurement</h2>
<p class="text-sm text-slate-400 mb-6 flex-grow">
Track supplier spend, analyze product margins, and monitor purchasing trends across the organization.
</p>
<div class="text-sm font-medium text-red-500 flex items-center gap-1 mt-auto">
Access Dashboard
<svg class="w-4 h-4 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
</div>
</a>
<!-- Active: E-Commerce Dashboard -->
<a href="/ecommerce" class="dashboard-card glass rounded-2xl p-6 flex flex-col group fade-in relative overflow-hidden block" style="animation-delay: 300ms">
<div class="absolute top-0 right-0 p-4">
<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-400 ring-1 ring-inset ring-green-500/20">Active</span>
</div>
<div class="w-14 h-14 rounded-xl bg-gradient-to-br from-purple-600 to-purple-900 flex items-center justify-center mb-6 shadow-lg shadow-purple-900/40 group-hover:scale-110 transition-transform duration-300">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
</div>
<h2 class="text-xl font-bold text-white mb-2 group-hover:text-purple-400 transition-colors">E-Commerce</h2>
<p class="text-sm text-slate-400 mb-6 flex-grow">
Monitor online sales, WooCommerce integration, customer insights, and digital revenue streams.
</p>
<div class="text-sm font-medium text-purple-500 flex items-center gap-1 mt-auto">
Access Dashboard
<svg class="w-4 h-4 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
</div>
</a>
<!-- Coming Soon: Sales Dashboard -->
<div class="dashboard-card glass rounded-2xl p-6 flex flex-col group fade-in relative overflow-hidden block disabled-card" style="animation-delay: 400ms">
<div class="absolute top-0 right-0 p-4">
<span class="inline-flex items-center rounded-md bg-slate-500/10 px-2 py-1 text-xs font-medium text-slate-400 ring-1 ring-inset ring-slate-500/20">Coming Soon</span>
</div>
<div class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center mb-6 border border-white/5 group-hover:bg-slate-700 transition-colors duration-300">
<svg class="w-7 h-7 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<h2 class="text-xl font-bold text-slate-300 mb-2 transition-colors">Sales & CRM</h2>
<p class="text-sm text-slate-500 mb-6 flex-grow">
Track wholesale orders, manage B2B customer relationships, and monitor offline revenue targets.
</p>
<div class="text-sm font-medium text-slate-600 flex items-center gap-1 mt-auto">
In Development
</div>
</div>
<!-- Coming Soon: Marketing Dashboard -->
<div class="dashboard-card glass rounded-2xl p-6 flex flex-col group fade-in relative overflow-hidden block disabled-card" style="animation-delay: 500ms">
<div class="absolute top-0 right-0 p-4">
<span class="inline-flex items-center rounded-md bg-slate-500/10 px-2 py-1 text-xs font-medium text-slate-400 ring-1 ring-inset ring-slate-500/20">Coming Soon</span>
</div>
<div class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center mb-6 border border-white/5 group-hover:bg-slate-700 transition-colors duration-300">
<svg class="w-7 h-7 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"/></svg>
</div>
<h2 class="text-xl font-bold text-slate-300 mb-2 transition-colors">Marketing</h2>
<p class="text-sm text-slate-500 mb-6 flex-grow">
Consolidate campaign performance, SEO insights, and high-level marketing analytics in one view.
</p>
</div>
<!-- Coming Soon: Division Task Performance -->
<div class="dashboard-card glass rounded-2xl p-6 flex flex-col group fade-in relative overflow-hidden block disabled-card" style="animation-delay: 600ms">
<div class="absolute top-0 right-0 p-4">
<span class="inline-flex items-center rounded-md bg-slate-500/10 px-2 py-1 text-xs font-medium text-slate-400 ring-1 ring-inset ring-slate-500/20">Coming Soon</span>
</div>
<div class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center mb-6 border border-white/5 group-hover:bg-slate-700 transition-colors duration-300">
<svg class="w-7 h-7 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
</div>
<h2 class="text-xl font-bold text-slate-300 mb-2 transition-colors">Division Task Performance</h2>
<p class="text-sm text-slate-500 mb-6 flex-grow">
Monitor staff KPIs, division output metrics, and project completion rates across the organization.
</p>
<div class="text-sm font-medium text-slate-600 flex items-center gap-1 mt-auto">
In Development
</div>
</div>
</div>
<div class="mt-24 text-center text-xs text-slate-600 fade-in" style="animation-delay: 500ms">
&copy; <script>document.write(new Date().getFullYear())</script> Stargas Energies. All Rights Reserved.
</div>
</main>
</body>
</html>

View File

@@ -1,9 +1,23 @@
<?php <?php
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\EcommerceController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', [DashboardController::class, 'index']); Route::get('/', function() {
return view('landing');
});
Route::get('/procurement', [DashboardController::class, 'index']);
Route::get('/ecommerce', [EcommerceController::class, 'index']);
// Admin endpoints
Route::prefix('admin')->group(function () {
Route::get('/', [AdminController::class, 'index']);
Route::get('/settings/{dashboard}', [AdminController::class, 'dashboardSettings']);
Route::post('/settings/{dashboard}', [AdminController::class, 'updateSettings']);
});
// Dashboard API endpoints // Dashboard API endpoints
Route::prefix('api/dashboard')->group(function () { Route::prefix('api/dashboard')->group(function () {
@@ -16,3 +30,13 @@
Route::get('/supplier/{code}', [DashboardController::class, 'supplierProducts']); Route::get('/supplier/{code}', [DashboardController::class, 'supplierProducts']);
Route::get('/product-details', [DashboardController::class, 'productDetails']); Route::get('/product-details', [DashboardController::class, 'productDetails']);
}); });
// E-Commerce API endpoints
Route::prefix('api/ecommerce')->group(function () {
Route::get('/summary', [EcommerceController::class, 'summary']);
Route::get('/recent-orders', [EcommerceController::class, 'recentOrders']);
Route::get('/top-products', [EcommerceController::class, 'topProducts']);
Route::get('/analytics', [EcommerceController::class, 'analytics']);
Route::get('/insights', [EcommerceController::class, 'insights']);
Route::get('/marketing', [EcommerceController::class, 'marketing']);
});

0
src/storage/app/.gitignore vendored Normal file → Executable file
View File

0
src/storage/app/private/.gitignore vendored Normal file → Executable file
View File

0
src/storage/app/public/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/cache/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/cache/data/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/sessions/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/testing/.gitignore vendored Normal file → Executable file
View File

0
src/storage/framework/views/.gitignore vendored Normal file → Executable file
View File

0
src/storage/logs/.gitignore vendored Normal file → Executable file
View File