White-label multi-domain tuki (Noxus Intra)

- CSS-muuttujat: kaikki kovakoodatut #0f3460/#16213e korvattu var(--primary-color)/var(--primary-dark)
- Uudet API-endpointit: branding (julkinen, domain-pohjainen), company_logo, company_logo_upload
- Domain-pohjainen brändäys: HTTP_HOST → yrityksen domains-arrayn matchaus
- Login: domain asettaa oletusyrityksen sessioon
- check_auth: palauttaa branding-objektin (primary_color, subtitle, logo_url)
- company_create/update: käsittelee domains, primary_color, subtitle, logo_file
- Dynaaminen login-sivu, header ja footer (logo, nimi, alaotsikko, värit)
- JS: loadBranding(), applyBranding(), yritysvaihdon brändäyspäivitys
- Admin-paneeli: brändäysasetukset (logo-upload, väri, alaotsikko, domainit)
- Git-repo siirretty intra.noxus.fi:hin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 13:44:49 +02:00
parent 918a5ff120
commit 095dc90b6f
4 changed files with 381 additions and 54 deletions

184
api.php
View File

@@ -1016,6 +1016,143 @@ switch ($action) {
echo json_encode(['question' => "$a + $b = ?"]);
break;
// ---------- BRANDING (julkinen) ----------
case 'branding':
$host = $_SERVER['HTTP_HOST'] ?? '';
// Stripaa portti pois (localhost:3001 → localhost)
$host = strtolower(explode(':', $host)[0]);
$companies = loadCompanies();
$matchedCompany = null;
foreach ($companies as $comp) {
$domains = $comp['domains'] ?? [];
foreach ($domains as $d) {
if (strtolower(trim($d)) === strtolower($host)) {
$matchedCompany = $comp;
break 2;
}
}
}
if ($matchedCompany) {
$logoUrl = !empty($matchedCompany['logo_file'])
? "api.php?action=company_logo&company_id=" . urlencode($matchedCompany['id'])
: '';
echo json_encode([
'found' => true,
'company_id' => $matchedCompany['id'],
'nimi' => $matchedCompany['nimi'],
'primary_color' => $matchedCompany['primary_color'] ?? '#0f3460',
'subtitle' => $matchedCompany['subtitle'] ?? '',
'logo_url' => $logoUrl,
]);
} else {
// Noxus Intra -oletusbrändäys
echo json_encode([
'found' => false,
'company_id' => '',
'nimi' => 'Noxus Intra',
'primary_color' => '#0f3460',
'subtitle' => 'Hallintapaneeli',
'logo_url' => '',
]);
}
break;
case 'company_logo':
$companyId = $_GET['company_id'] ?? '';
if (empty($companyId) || !preg_match('/^[a-z0-9-]+$/', $companyId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen company_id']);
break;
}
$companies = loadCompanies();
$logoFile = '';
foreach ($companies as $comp) {
if ($comp['id'] === $companyId) {
$logoFile = $comp['logo_file'] ?? '';
break;
}
}
if (empty($logoFile)) {
http_response_code(404);
echo json_encode(['error' => 'Logoa ei löydy']);
break;
}
$logoPath = DATA_DIR . '/companies/' . $companyId . '/' . $logoFile;
if (!file_exists($logoPath)) {
http_response_code(404);
echo json_encode(['error' => 'Logotiedostoa ei löydy']);
break;
}
$ext = strtolower(pathinfo($logoFile, PATHINFO_EXTENSION));
$mimeTypes = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'svg' => 'image/svg+xml', 'webp' => 'image/webp'];
$mime = $mimeTypes[$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Cache-Control: public, max-age=3600');
readfile($logoPath);
exit;
case 'company_logo_upload':
requireAdmin();
if ($method !== 'POST') break;
$companyId = $_POST['company_id'] ?? '';
if (empty($companyId) || !preg_match('/^[a-z0-9-]+$/', $companyId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen company_id']);
break;
}
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Logotiedosto puuttuu tai virhe uploadissa']);
break;
}
$file = $_FILES['logo'];
// Validoi koko (max 2MB)
if ($file['size'] > 2 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Logo on liian suuri (max 2MB)']);
break;
}
// Validoi tyyppi
$allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detectedType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($detectedType, $allowedTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Sallitut tiedostotyypit: PNG, JPG, SVG, WebP']);
break;
}
$extMap = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/svg+xml' => 'svg', 'image/webp' => 'webp'];
$ext = $extMap[$detectedType] ?? 'png';
$newFilename = 'logo.' . $ext;
$compDir = DATA_DIR . '/companies/' . $companyId;
if (!file_exists($compDir)) {
http_response_code(404);
echo json_encode(['error' => 'Yritystä ei löydy']);
break;
}
// Poista vanha logo
$companies = loadCompanies();
foreach ($companies as &$comp) {
if ($comp['id'] === $companyId) {
$oldLogo = $comp['logo_file'] ?? '';
if ($oldLogo && $oldLogo !== $newFilename && file_exists($compDir . '/' . $oldLogo)) {
unlink($compDir . '/' . $oldLogo);
}
$comp['logo_file'] = $newFilename;
break;
}
}
unset($comp);
saveCompanies($companies);
move_uploaded_file($file['tmp_name'], $compDir . '/' . $newFilename);
echo json_encode([
'success' => true,
'logo_file' => $newFilename,
'logo_url' => "api.php?action=company_logo&company_id=" . urlencode($companyId),
]);
break;
// ---------- AUTH ----------
case 'login':
if ($method !== 'POST') break;
@@ -1050,8 +1187,24 @@ switch ($action) {
// Multi-company: aseta käyttäjän yritykset sessioon
$userCompanies = $u['companies'] ?? [];
$_SESSION['companies'] = $userCompanies;
// Valitse ensimmäinen yritys oletukseksi
// Domain-pohjainen oletusyritys
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
$domainCompanyId = '';
$allComps = loadCompanies();
foreach ($allComps as $dc) {
foreach ($dc['domains'] ?? [] as $d) {
if (strtolower(trim($d)) === strtolower($host)) {
$domainCompanyId = $dc['id'];
break 2;
}
}
}
// Jos domain matchaa ja käyttäjällä on oikeus → käytä sitä
if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
$_SESSION['company_id'] = $domainCompanyId;
} else {
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
}
// Hae yritysten nimet
$allCompanies = loadCompanies();
$companyList = [];
@@ -1116,6 +1269,20 @@ switch ($action) {
break;
}
}
// Brändäystiedot aktiivisesta yrityksestä
$branding = ['primary_color' => '#0f3460', 'subtitle' => '', 'logo_url' => '', 'company_nimi' => ''];
$activeCompanyId = $_SESSION['company_id'] ?? '';
foreach ($allCompanies as $bc) {
if ($bc['id'] === $activeCompanyId) {
$branding['primary_color'] = $bc['primary_color'] ?? '#0f3460';
$branding['subtitle'] = $bc['subtitle'] ?? '';
$branding['company_nimi'] = $bc['nimi'] ?? '';
$branding['logo_url'] = !empty($bc['logo_file'])
? "api.php?action=company_logo&company_id=" . urlencode($bc['id'])
: '';
break;
}
}
echo json_encode([
'authenticated' => true,
'user_id' => $_SESSION['user_id'],
@@ -1125,6 +1292,7 @@ switch ($action) {
'companies' => $companyList,
'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures,
'branding' => $branding,
]);
} else {
echo json_encode(['authenticated' => false]);
@@ -2479,9 +2647,18 @@ switch ($action) {
break 2;
}
}
// Brändäyskentät
$domains = [];
if (isset($input['domains']) && is_array($input['domains'])) {
$domains = array_values(array_filter(array_map('trim', $input['domains'])));
}
$company = [
'id' => $id,
'nimi' => $nimi,
'domains' => $domains,
'primary_color' => trim($input['primary_color'] ?? '#0f3460'),
'subtitle' => trim($input['subtitle'] ?? ''),
'logo_file' => '',
'luotu' => date('Y-m-d H:i:s'),
'aktiivinen' => true,
];
@@ -2521,6 +2698,11 @@ switch ($action) {
if ($c['id'] === $id) {
if (isset($input['nimi'])) $c['nimi'] = trim($input['nimi']);
if (isset($input['aktiivinen'])) $c['aktiivinen'] = (bool)$input['aktiivinen'];
if (isset($input['domains']) && is_array($input['domains'])) {
$c['domains'] = array_values(array_filter(array_map('trim', $input['domains'])));
}
if (isset($input['primary_color'])) $c['primary_color'] = trim($input['primary_color']);
if (isset($input['subtitle'])) $c['subtitle'] = trim($input['subtitle']);
$found = true;
echo json_encode($c);
break;

View File

@@ -3,15 +3,16 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CuituNet Intra - Asiakashallinta</title>
<title>Noxus Intra</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Login -->
<div id="login-screen" class="login-screen">
<div class="login-box">
<h1>CuituNet Intra</h1>
<p>Kirjaudu sisään</p>
<img id="login-logo" src="" alt="Logo" style="height:48px;margin-bottom:0.75rem;display:none;">
<h1 id="login-title">Noxus Intra</h1>
<p id="login-subtitle">Kirjaudu sisään</p>
<form id="login-form">
<input type="text" id="login-username" placeholder="Käyttäjätunnus" required autofocus>
<input type="password" id="login-password" placeholder="Salasana" required>
@@ -26,7 +27,7 @@
</div>
<!-- Salasanan palautuspyyntö -->
<div class="login-box" id="forgot-box" style="display:none">
<h1>CuituNet Intra</h1>
<h1 class="login-brand-title">Noxus Intra</h1>
<p>Salasanan palautus</p>
<form id="forgot-form">
<input type="text" id="forgot-username" placeholder="Käyttäjätunnus" required autofocus>
@@ -38,7 +39,7 @@
</div>
<!-- Uusi salasana (reset token) -->
<div class="login-box" id="reset-box" style="display:none">
<h1>CuituNet Intra</h1>
<h1 class="login-brand-title">Noxus Intra</h1>
<p>Aseta uusi salasana</p>
<form id="reset-form">
<input type="password" id="reset-password" placeholder="Uusi salasana" required>
@@ -55,10 +56,11 @@
<header>
<div class="header-left">
<div class="header-brand" id="brand-home" style="cursor:pointer;">
<span class="brand-icon">&#9889;</span>
<img id="header-logo" src="" alt="Logo" style="height:32px;display:none;">
<span class="brand-icon" id="header-brand-icon">&#9889;</span>
<div>
<h1>CuituNet Intra</h1>
<span class="subtitle">Kuituasiakkaiden hallinta</span>
<h1 id="header-title">Noxus Intra</h1>
<span class="subtitle" id="header-subtitle">Hallintapaneeli</span>
</div>
</div>
</div>
@@ -514,16 +516,36 @@
<div id="company-detail-view" style="display:none;">
<button class="btn-secondary" id="btn-company-back" style="color:#555;border-color:#ddd;margin-bottom:1rem;">&#8592; Takaisin yrityslistaan</button>
<div class="table-card" style="padding:1.5rem;">
<h3 style="color:#0f3460;margin-bottom:0.5rem;" id="company-detail-title">Yrityksen asetukset</h3>
<div class="form-grid" style="max-width:400px;margin-bottom:1.5rem;">
<h3 style="margin-bottom:0.5rem;" id="company-detail-title">Yrityksen asetukset</h3>
<div class="form-grid" style="max-width:600px;margin-bottom:1.5rem;">
<div class="form-group">
<label>Yrityksen nimi</label>
<input type="text" id="company-edit-nimi">
</div>
<div class="form-group">
<button class="btn-primary" id="btn-save-company-name" style="font-size:0.85rem;">Tallenna nimi</button>
<label>Alaotsikko</label>
<input type="text" id="company-edit-subtitle" placeholder="esim. Asiakashallinta">
</div>
<div class="form-group">
<label>Pääväri</label>
<div style="display:flex;align-items:center;gap:0.5rem;">
<input type="color" id="company-edit-color" value="#0f3460" style="width:50px;height:36px;border:none;cursor:pointer;">
<input type="text" id="company-edit-color-text" placeholder="#0f3460" style="width:100px;font-family:monospace;font-size:0.85rem;">
</div>
</div>
<div class="form-group">
<label>Logo</label>
<div style="display:flex;align-items:center;gap:0.75rem;">
<img id="company-logo-preview" src="" style="height:36px;display:none;border-radius:4px;">
<input type="file" id="company-logo-upload" accept="image/png,image/jpeg,image/svg+xml,image/webp" style="font-size:0.82rem;">
</div>
</div>
<div class="form-group full-width">
<label>Domainit (yksi per rivi)</label>
<textarea id="company-edit-domains" rows="3" placeholder="intra.yritys.fi&#10;intra.toinen.fi" style="font-family:monospace;font-size:0.85rem;"></textarea>
</div>
</div>
<button class="btn-primary" id="btn-save-company-settings" style="font-size:0.85rem;">Tallenna asetukset</button>
</div>
<!-- Postilaatikot -->
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
@@ -592,7 +614,7 @@
</div>
<footer>
<p>CuituNet Intra &mdash; Asiakashallintajärjestelmä</p>
<p id="footer-text">Noxus Intra &copy; 2026</p>
</footer>
</div>

124
script.js
View File

@@ -136,6 +136,7 @@ async function checkAuth() {
availableCompanies = data.companies || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {};
if (data.branding) applyBranding(data.branding);
showDashboard();
}
} catch (e) { /* not logged in */ }
@@ -171,6 +172,7 @@ document.getElementById('btn-logout').addEventListener('click', async () => {
document.getElementById('login-captcha').value = '';
showLoginView();
loadCaptcha();
loadBranding(); // Domain-pohjainen brändäys uudelleen
});
async function showDashboard() {
@@ -206,6 +208,11 @@ async function switchCompany(companyId) {
try {
await apiCall('company_switch', 'POST', { company_id: companyId });
currentCompany = availableCompanies.find(c => c.id === companyId) || null;
// Päivitä brändäys vaihdetun yrityksen mukaan
try {
const auth = await apiCall('check_auth');
if (auth.branding) applyBranding(auth.branding);
} catch (e2) {}
// Lataa uudelleen aktiivinen tab
const hash = window.location.hash.replace('#', '') || 'customers';
switchToTab(hash);
@@ -1837,6 +1844,20 @@ async function showCompanyDetail(id) {
const comp = companiesTabData.find(c => c.id === id);
document.getElementById('company-detail-title').textContent = (comp ? comp.nimi : id) + ' — Asetukset';
document.getElementById('company-edit-nimi').value = comp ? comp.nimi : '';
// Brändäyskentät
document.getElementById('company-edit-subtitle').value = comp?.subtitle || '';
const color = comp?.primary_color || '#0f3460';
document.getElementById('company-edit-color').value = color;
document.getElementById('company-edit-color-text').value = color;
document.getElementById('company-edit-domains').value = (comp?.domains || []).join('\n');
// Logo-esikatselu
const logoPreview = document.getElementById('company-logo-preview');
if (comp?.logo_file) {
logoPreview.src = 'api.php?action=company_logo&company_id=' + encodeURIComponent(id) + '&t=' + Date.now();
logoPreview.style.display = '';
} else {
logoPreview.style.display = 'none';
}
// Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein
await apiCall('company_switch', 'POST', { company_id: id });
@@ -1853,18 +1874,60 @@ document.getElementById('btn-company-back').addEventListener('click', () => {
renderCompaniesTable();
});
document.getElementById('btn-save-company-name').addEventListener('click', async () => {
// Synkronoi color picker <-> text input
document.getElementById('company-edit-color').addEventListener('input', function() {
document.getElementById('company-edit-color-text').value = this.value;
});
document.getElementById('company-edit-color-text').addEventListener('input', function() {
if (/^#[0-9a-fA-F]{6}$/.test(this.value)) {
document.getElementById('company-edit-color').value = this.value;
}
});
// Logo-upload
document.getElementById('company-logo-upload').addEventListener('change', async function() {
if (!this.files[0] || !currentCompanyDetail) return;
const formData = new FormData();
formData.append('logo', this.files[0]);
formData.append('company_id', currentCompanyDetail);
try {
const res = await fetch('api.php?action=company_logo_upload', { method: 'POST', body: formData, credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Virhe');
// Päivitä preview
const preview = document.getElementById('company-logo-preview');
preview.src = data.logo_url + '&t=' + Date.now();
preview.style.display = '';
// Päivitä paikallinen data
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
if (comp) comp.logo_file = data.logo_file;
} catch (e) { alert(e.message); }
this.value = ''; // Reset file input
});
document.getElementById('btn-save-company-settings').addEventListener('click', async () => {
const nimi = document.getElementById('company-edit-nimi').value.trim();
if (!nimi) return;
const subtitle = document.getElementById('company-edit-subtitle').value.trim();
const primary_color = document.getElementById('company-edit-color').value;
const domainsText = document.getElementById('company-edit-domains').value;
const domains = domainsText.split('\n').map(d => d.trim()).filter(d => d);
try {
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi });
alert('Nimi tallennettu!');
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains });
alert('Asetukset tallennettu!');
// Päivitä paikalliset tiedot
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
if (comp) comp.nimi = nimi;
if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; }
const avail = availableCompanies.find(c => c.id === currentCompanyDetail);
if (avail) avail.nimi = nimi;
populateCompanySelector();
// Jos tämä on aktiivinen yritys → päivitä brändäys heti
if (currentCompany && currentCompany.id === currentCompanyDetail) {
applyBranding({
nimi, subtitle, primary_color,
logo_url: comp?.logo_file ? 'api.php?action=company_logo&company_id=' + encodeURIComponent(currentCompanyDetail) + '&t=' + Date.now() : ''
});
}
} catch (e) { alert(e.message); }
});
@@ -1985,6 +2048,59 @@ async function toggleCompanyUser(userId, companyId, add) {
} catch (e) { alert(e.message); }
}
// ==================== BRANDING ====================
function applyBranding(branding) {
const color = branding.primary_color || '#0f3460';
const nimi = branding.nimi || 'Noxus Intra';
const subtitle = branding.subtitle || '';
const logoUrl = branding.logo_url || '';
// CSS-muuttuja
document.documentElement.style.setProperty('--primary-color', color);
// Laske tumma variantti
document.documentElement.style.setProperty('--primary-dark', color);
// Login-sivu
const loginLogo = document.getElementById('login-logo');
const loginTitle = document.getElementById('login-title');
const loginSubtitle = document.getElementById('login-subtitle');
if (loginLogo) {
if (logoUrl) { loginLogo.src = logoUrl; loginLogo.style.display = ''; }
else { loginLogo.style.display = 'none'; }
}
if (loginTitle) loginTitle.textContent = nimi;
if (loginSubtitle) loginSubtitle.textContent = subtitle || 'Kirjaudu sisään';
// Muut login-boxien otsikot
document.querySelectorAll('.login-brand-title').forEach(el => el.textContent = nimi);
// Header
const headerLogo = document.getElementById('header-logo');
const headerIcon = document.getElementById('header-brand-icon');
const headerTitle = document.getElementById('header-title');
const headerSubtitle = document.getElementById('header-subtitle');
if (headerLogo) {
if (logoUrl) { headerLogo.src = logoUrl; headerLogo.style.display = ''; if (headerIcon) headerIcon.style.display = 'none'; }
else { headerLogo.style.display = 'none'; if (headerIcon) headerIcon.style.display = ''; }
}
if (headerTitle) headerTitle.textContent = nimi;
if (headerSubtitle) headerSubtitle.textContent = subtitle || 'Hallintapaneeli';
// Sivun title
document.title = nimi;
}
async function loadBranding() {
try {
const data = await apiCall('branding');
applyBranding(data);
} catch (e) {
// Oletusbrändäys
applyBranding({ nimi: 'Noxus Intra', primary_color: '#0f3460', subtitle: 'Hallintapaneeli', logo_url: '' });
}
}
// Init
loadBranding();
loadCaptcha();
checkAuth();

View File

@@ -1,3 +1,10 @@
:root {
--primary-color: #0f3460;
--primary-dark: #16213e;
--primary-light: color-mix(in srgb, var(--primary-color) 10%, white);
--primary-hover: color-mix(in srgb, var(--primary-color) 85%, black);
}
* {
margin: 0;
padding: 0;
@@ -17,7 +24,7 @@ body {
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #0f3460, #16213e);
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
}
.login-box {
@@ -34,7 +41,7 @@ body {
.login-box h1 {
font-size: 1.8rem;
margin-bottom: 0.5rem;
color: #0f3460;
color: var(--primary-color);
}
.login-box p {
@@ -54,13 +61,13 @@ body {
.login-box input:focus {
outline: none;
border-color: #0f3460;
border-color: var(--primary-color);
}
.login-box button {
width: 100%;
padding: 12px;
background: #0f3460;
background: var(--primary-color);
color: #fff;
border: none;
border-radius: 8px;
@@ -70,7 +77,7 @@ body {
}
.login-box button:hover {
background: #16213e;
background: var(--primary-dark);
}
.error {
@@ -88,7 +95,7 @@ body {
.forgot-link {
display: inline-block;
margin-top: 1rem;
color: #0f3460;
color: var(--primary-color);
font-size: 0.85rem;
text-decoration: none;
opacity: 0.7;
@@ -110,7 +117,7 @@ body {
.captcha-question {
font-size: 1.1rem;
font-weight: 700;
color: #0f3460;
color: var(--primary-color);
white-space: nowrap;
min-width: 120px;
text-align: center;
@@ -127,7 +134,7 @@ body {
/* Header */
header {
background: linear-gradient(135deg, #0f3460, #16213e);
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: #fff;
padding: 1rem 2rem;
display: flex;
@@ -221,7 +228,7 @@ header {
}
.sidebar-stats .stat-card.highlight {
background: #0f3460;
background: var(--primary-color);
color: #fff;
}
@@ -253,7 +260,7 @@ header {
}
.stat-highlight {
color: #0f3460;
color: var(--primary-color);
}
.sidebar-stats .stat-card.trivia .stat-value {
@@ -285,7 +292,7 @@ header {
.speed-item.top {
font-weight: 700;
color: #0f3460;
color: var(--primary-color);
}
.speed-bar {
@@ -297,7 +304,7 @@ header {
}
.speed-item.top .speed-bar {
background: #0f3460;
background: var(--primary-color);
}
/* Toolbar */
@@ -331,7 +338,7 @@ header {
.search-bar input:focus {
outline: none;
border-color: #0f3460;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(15,52,96,0.1);
}
@@ -350,7 +357,7 @@ table {
}
thead th {
background: #16213e;
background: var(--primary-dark);
color: #fff;
padding: 11px 14px;
text-align: left;
@@ -401,7 +408,7 @@ tbody td {
.price-cell {
font-weight: 700;
color: #0f3460;
color: var(--primary-color);
}
.actions-cell {
@@ -460,7 +467,7 @@ tbody td {
#total-billing {
font-size: 0.95rem;
color: #0f3460;
color: var(--primary-color);
}
/* Footer */
@@ -604,7 +611,7 @@ footer {
.modal-header h2 {
font-size: 1.3rem;
color: #0f3460;
color: var(--primary-color);
}
.modal-close {
@@ -635,7 +642,7 @@ form {
form h3 {
font-size: 0.95rem;
color: #0f3460;
color: var(--primary-color);
margin: 1.25rem 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f2f5;
@@ -688,7 +695,7 @@ form h3:first-of-type {
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #0f3460;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(15,52,96,0.1);
}
@@ -704,7 +711,7 @@ form h3:first-of-type {
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #0f3460;
accent-color: var(--primary-color);
}
.form-actions {
@@ -744,7 +751,7 @@ form h3:first-of-type {
.liittyma-row-title {
font-size: 0.8rem;
font-weight: 600;
color: #0f3460;
color: var(--primary-color);
}
/* Detail view */
@@ -762,7 +769,7 @@ form h3:first-of-type {
.detail-section h3 {
font-size: 0.95rem;
color: #0f3460;
color: var(--primary-color);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f2f5;
@@ -794,7 +801,7 @@ form h3:first-of-type {
}
.detail-value a {
color: #0f3460;
color: var(--primary-color);
text-decoration: none;
}
@@ -820,7 +827,7 @@ span.empty {
.liittyma-num {
font-size: 0.75rem;
font-weight: 700;
color: #0f3460;
color: var(--primary-color);
margin-bottom: 0.4rem;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -830,7 +837,7 @@ span.empty {
text-align: right;
font-weight: 700;
font-size: 0.95rem;
color: #0f3460;
color: var(--primary-color);
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 2px solid #e8ebf0;
@@ -869,7 +876,7 @@ span.empty {
.file-name {
font-size: 0.9rem;
font-weight: 500;
color: #0f3460;
color: var(--primary-color);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
@@ -995,12 +1002,12 @@ span.empty {
}
.tab:hover {
color: #0f3460;
color: var(--primary-color);
}
.tab.active {
color: #0f3460;
border-bottom-color: #0f3460;
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
@@ -1023,7 +1030,7 @@ span.empty {
}
.role-admin {
background: #0f3460;
background: var(--primary-color);
color: #fff;
}
@@ -1257,7 +1264,7 @@ span.empty {
.ticket-reply-form textarea:focus {
outline: none;
border-color: #0f3460;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(15, 52, 96, 0.1);
}
@@ -1279,9 +1286,9 @@ span.empty {
}
.btn-reply-tab.active {
color: #0f3460;
color: var(--primary-color);
background: #fff;
border-color: #0f3460;
border-color: var(--primary-color);
}
/* Ticket tags */
@@ -1339,7 +1346,7 @@ span.empty {
border-radius: 6px;
font-size: 0.82rem;
font-weight: 600;
color: #0f3460;
color: var(--primary-color);
background: #fff;
cursor: pointer;
transition: border-color 0.2s;
@@ -1347,7 +1354,7 @@ span.empty {
.company-selector:hover,
.company-selector:focus {
border-color: #0f3460;
border-color: var(--primary-color);
outline: none;
}
@@ -1357,7 +1364,7 @@ span.empty {
}
.mailbox-item:hover {
border-color: #0f3460 !important;
border-color: var(--primary-color) !important;
}
/* Company badge */