catalog.php

Please log in to continue

You need to be logged in to request a proposal. Once you’re in, you’ll be brought right back here.

prepare("SELECT user_code, user_type, first_name, last_name, email_address, company_name, phone_number FROM simeya_users WHERE user_code=? LIMIT 1"); $stmt->execute([$current_user_code]); $me = $stmt->fetch(PDO::FETCH_ASSOC); //if (!$me) exit('User not found.'); $isSuper = (strtoupper($me['user_type'] ?? 'S') === 'A'); // Default “target” user = self $target = $me; if (!isset($_COOKIE['SIM_id']) || !($SIMinfo = get_user_code())) { exit("You need to log in."); } $user_code = $SIMinfo['user_code'] ?? ''; /* ---------- helpers ---------- */ if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } } function redirect_entity($entity, $msg='Saved') { $qs = http_build_query(['entity'=>$entity,'msg'=>$msg]); header("Location: ?$qs"); exit; } /* ---------- inputs ---------- */ $entity = $_GET['entity'] ?? 'services'; // 'services'|'addons' $action = $_POST['action'] ?? ($_GET['action'] ?? ''); $id = (int)($_POST['id'] ?? ($_GET['id'] ?? 0)); /* ---------- constants ---------- */ $CURRENCIES = ['USD','INR','AUD','EUR','GBP','CAD','JPY']; /* ---------- save handlers ---------- */ if ($entity === 'services' && $action === 'save') { $row = [ 'category_id' => (int)($_POST['category_id'] ?? 0), 'name' => trim($_POST['name'] ?? ''), 'short_desc' => trim($_POST['short_desc'] ?? ''), 'price_note' => trim($_POST['price_note'] ?? ''), 'is_active' => isset($_POST['is_active']) ? 1 : 0, 'sort_order' => (int)($_POST['sort_order'] ?? 0), 'price_type' => (($_POST['price_type'] ?? 'fixed') === 'hourly') ? 'hourly' : 'fixed', 'currency' => strtoupper(trim($_POST['currency'] ?? 'USD')), 'price_fixed' => $_POST['price_fixed'] !== '' ? (float)$_POST['price_fixed'] : null, 'hourly_rate' => $_POST['hourly_rate'] !== '' ? (float)$_POST['hourly_rate'] : null, 'hourly_hours' => $_POST['hourly_hours'] !== '' ? (float)$_POST['hourly_hours'] : null, ]; // normalize + legacy mirror if ($row['price_type'] === 'fixed') { if ($row['price_fixed'] === null) $row['price_fixed'] = 0.00; $row['hourly_rate'] = $row['hourly_hours'] = null; $base_price = $row['price_fixed']; } else { if ($row['hourly_rate'] === null) $row['hourly_rate'] = 0.00; if ($row['hourly_hours'] === null) $row['hourly_hours'] = 0.00; $base_price = null; } if ($id) { $st=$pdo->prepare(" UPDATE website_services SET category_id=?, name=?, short_desc=?, base_price=?, price_note=?, is_active=?, sort_order=?, price_type=?, currency=?, price_fixed=?, hourly_rate=?, hourly_hours=? WHERE id=? "); $st->execute([ $row['category_id'],$row['name'],$row['short_desc'],$base_price,$row['price_note'],$row['is_active'],$row['sort_order'], $row['price_type'],$row['currency'],$row['price_fixed'],$row['hourly_rate'],$row['hourly_hours'],$id ]); redirect_entity('services','Service updated'); } else { $st=$pdo->prepare(" INSERT INTO website_services (category_id,name,short_desc,base_price,price_note,is_active,sort_order, price_type,currency,price_fixed,hourly_rate,hourly_hours) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) "); $st->execute([ $row['category_id'],$row['name'],$row['short_desc'],$base_price,$row['price_note'],$row['is_active'],$row['sort_order'], $row['price_type'],$row['currency'],$row['price_fixed'],$row['hourly_rate'],$row['hourly_hours'] ]); redirect_entity('services','Service added'); } } if ($entity === 'addons' && $action === 'save') { $row = [ 'name' => trim($_POST['name'] ?? ''), 'short_desc' => trim($_POST['short_desc'] ?? ''), 'is_active' => isset($_POST['is_active']) ? 1 : 0, 'sort_order' => (int)($_POST['sort_order'] ?? 0), 'price_type' => (($_POST['price_type'] ?? 'fixed') === 'hourly') ? 'hourly' : 'fixed', 'currency' => strtoupper(trim($_POST['currency'] ?? 'USD')), 'price_fixed' => $_POST['price_fixed'] !== '' ? (float)$_POST['price_fixed'] : null, 'hourly_rate' => $_POST['hourly_rate'] !== '' ? (float)$_POST['hourly_rate'] : null, 'hourly_hours' => $_POST['hourly_hours'] !== '' ? (float)$_POST['hourly_hours'] : null, ]; // normalize + legacy mirror if ($row['price_type'] === 'fixed') { if ($row['price_fixed'] === null) $row['price_fixed'] = 0.00; $row['hourly_rate'] = $row['hourly_hours'] = null; $price_legacy = $row['price_fixed']; } else { if ($row['hourly_rate'] === null) $row['hourly_rate'] = 0.00; if ($row['hourly_hours'] === null) $row['hourly_hours'] = 0.00; $price_legacy = null; } if ($id) { $st=$pdo->prepare(" UPDATE website_service_addons SET name=?, short_desc=?, price=?, is_active=?, sort_order=?, price_type=?, currency=?, price_fixed=?, hourly_rate=?, hourly_hours=? WHERE id=? "); $st->execute([ $row['name'],$row['short_desc'],$price_legacy,$row['is_active'],$row['sort_order'], $row['price_type'],$row['currency'],$row['price_fixed'],$row['hourly_rate'],$row['hourly_hours'],$id ]); redirect_entity('addons','Add-on updated'); } else { $st=$pdo->prepare(" INSERT INTO website_service_addons (name,short_desc,price,is_active,sort_order, price_type,currency,price_fixed,hourly_rate,hourly_hours) VALUES (?,?,?,?,?,?,?,?,?,?) "); $st->execute([ $row['name'],$row['short_desc'],$price_legacy,$row['is_active'],$row['sort_order'], $row['price_type'],$row['currency'],$row['price_fixed'],$row['hourly_rate'],$row['hourly_hours'] ]); redirect_entity('addons','Add-on added'); } } /* ---------- loads for UI ---------- */ $cats = $pdo->query(" SELECT id, name FROM website_categories ORDER BY sort_order, name ")->fetchAll(PDO::FETCH_ASSOC); $services = $pdo->query(" SELECT s.*, c.name AS cat_name FROM website_services s LEFT JOIN website_categories c ON c.id=s.category_id ORDER BY c.sort_order, s.sort_order, s.name ")->fetchAll(PDO::FETCH_ASSOC); $addons = $pdo->query(" SELECT * FROM website_service_addons ORDER BY sort_order, name ")->fetchAll(PDO::FETCH_ASSOC); /* edit row */ $editRow = null; if ($action === 'edit' && $id) { if ($entity === 'services') { $st=$pdo->prepare("SELECT * FROM website_services WHERE id=?"); $st->execute([$id]); $editRow = $st->fetch(PDO::FETCH_ASSOC); } else { $st=$pdo->prepare("SELECT * FROM website_service_addons WHERE id=?"); $st->execute([$id]); $editRow = $st->fetch(PDO::FETCH_ASSOC); } } ?> Website Catalog

Website Catalog

>
Cancel
Services
0 && $s['hourly_hours'] > 0) $total = $s['hourly_rate'] * $s['hourly_hours']; else $total = $s['price_fixed']; ?>
# Category Name Description Rate Hours Price Active Sort
0) echo number_format((float)$s['hourly_rate'],2); else echo ' '; $k++; ?> 0) echo number_format((float)$s['hourly_hours'],2); else echo ' '; ?> Edit
>
Cancel
Add-ons
0 && $a['hourly_hours'] > 0) $total = $a['hourly_rate'] * $a['hourly_hours']; else $total = $a['price_fixed']; ?>
# Name Description Rate Hours Price Active Sort
0) echo number_format((float)$a['hourly_rate'],2); else echo ' '; $k++; ?> 0) echo number_format((float)$a['hourly_hours'],2); else echo ' '; ?> Edit
proposal_manage.php

Please log in to continue

You need to be logged in to manage proposals. You’ll be brought right back here.

Log In / Join
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"); $current_user_code = $SIMinfo['user_code'] ?? ''; $stmt = $pdo->prepare("SELECT user_code, user_type, first_name, last_name FROM simeya_users WHERE user_code COLLATE utf8mb4_unicode_ci = ? LIMIT 1"); $stmt->execute([$current_user_code]); $me = $stmt->fetch(PDO::FETCH_ASSOC); $isSuper = (strtoupper($me['user_type'] ?? 'S') === 'A'); function formatDateTime($datetime) { $dt = new DateTime($datetime); return $dt->format('M d, Y h:i A'); } function h($s){ return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); } try { $pdo->exec("ALTER TABLE website_proposals ADD COLUMN IF NOT EXISTS status ENUM('draft','submitted','proposed','approved','deleted') NOT NULL DEFAULT 'draft'"); } catch (Throwable $e) {} try { $pdo->exec(" CREATE TABLE IF NOT EXISTS website_proposal_category_notes ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, proposal_id INT NOT NULL, category_id INT NOT NULL, user_code VARCHAR(20) NOT NULL, role ENUM('client','simeya') NOT NULL DEFAULT 'client', note TEXT NOT NULL, created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, INDEX (proposal_id), INDEX (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci "); } catch (Throwable $e) {} $where = ''; $params = []; if ($isSuper) { $where = "WHERE COALESCE(status,'draft') <> 'deleted'"; } else { $where = "WHERE (user_code COLLATE utf8mb4_unicode_ci = ? OR submitted_by COLLATE utf8mb4_unicode_ci = ?) AND COALESCE(status,'draft') <> 'deleted'"; $params = [$current_user_code, $current_user_code]; } $search = trim($_GET['q'] ?? ''); if ($search !== '') { $where .= ($where ? " AND " : "WHERE ") . "(client_name LIKE ? OR email LIKE ? OR company_name LIKE ?)"; $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; } $sql = "SELECT p.* FROM website_proposals p $where ORDER BY p.id DESC"; $st = $pdo->prepare($sql); $st->execute($params); $rows = $st->fetchAll(PDO::FETCH_ASSOC); // ---------- Unread helper ---------- function unread_count_for($pdo, $pid, $viewer_code, $viewer_is_super) { $otherRole = $viewer_is_super ? 'client' : 'simeya'; $st = $pdo->prepare("SELECT last_seen_note_id FROM website_proposal_note_seen WHERE proposal_id=? AND user_code COLLATE utf8mb4_unicode_ci=? LIMIT 1"); $st->execute([$pid, $viewer_code]); $last = (int)($st->fetchColumn() ?: 0); $q = $pdo->prepare("SELECT COUNT(*) FROM website_proposal_category_notes WHERE proposal_id=? AND id > ? AND role = ?"); $q->execute([$pid, $last, $otherRole]); return (int)$q->fetchColumn(); } ?> Proposal Management – Simeya

Simeya Requests / Propposals Manager

Requests / Proposals

New Proposal
Action Keys:
'secondary','submitted'=>'warning', 'proposed'=>'info','approved'=>'success','deleted'=>'dark' ][$status] ?? 'secondary'; $unread = unread_count_for($pdo, $pid, $current_user_code, $isSuper); ?>
# Client Contact Totals (USD) Created Status Actions
No proposals yet.
Services:
Add-ons:
Total:
0): ?> new
Loading…
proposal_submit.php setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* ---------------- Helpers ---------------- */ function ints_from_post_array($key): array { $vals = $_POST[$key] ?? []; if (!is_array($vals)) $vals = []; return array_values(array_unique(array_map('intval', $vals))); } function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function fnum($v): float { return is_numeric($v) ? (float)$v : 0.0; } /** * Try to load USD conversion rates from an optional table: * CREATE TABLE currency_rates (code CHAR(3) PRIMARY KEY, rate_to_usd DECIMAL(18,8), updated_at TIMESTAMP NULL); * where rate_to_usd = USD per 1 unit of 'code'. (USD: 1.0) * * Returns ['USD'=>1.0, 'EUR'=>1.07, ...]. If table missing or empty, falls back to ['USD'=>1.0]. */ function load_rates_to_usd(PDO $pdo): array { try { $rows = $pdo->query("SELECT code, rate_to_usd FROM currency_rates")->fetchAll(PDO::FETCH_KEY_PAIR); if (!$rows) return ['USD'=>1.0]; // normalize codes + coerce numeric $out = []; foreach ($rows as $code => $rate) { $code = strtoupper(trim($code)); $r = (float)$rate; if ($code && $r > 0) $out[$code] = $r; } if (!isset($out['USD'])) $out['USD'] = 1.0; return $out; } catch (Throwable $e) { // Table probably doesn't exist. Safe fallback: return ['USD'=>1.0]; } } /** * Compute a line's ORIGINAL amount & currency from a service/add-on row. * Supports: * - Fixed: uses price_fixed (fallback: legacy base_price/price) * - Hourly: hourly_rate × hours (hours can be overridden via $hoursOverrideIfAny) */ function compute_original_amount(array $row, ?float $hoursOverrideIfAny = null): array { $ptype = strtolower($row['price_type'] ?? 'fixed'); $ccy = strtoupper($row['currency'] ?? 'USD'); if ($ptype === 'hourly') { $rate = fnum($row['hourly_rate'] ?? 0); $hoursDefault = fnum($row['hourly_hours'] ?? 0); $hours = is_null($hoursOverrideIfAny) ? $hoursDefault : max(0, $hoursOverrideIfAny); $amt = $rate * $hours; return [$amt, $ccy, $rate, $hours, 'hourly']; } else { // fixed $fixed = $row['price_fixed'] ?? $row['base_price'] ?? $row['price'] ?? 0; $amt = fnum($fixed); return [$amt, $ccy, $amt, null, 'fixed']; // (for fixed we carry amt as "rate" for display symmetry) } } /** Get rate to USD; default 1.0 if unknown. */ function rate_to_usd(string $ccy, array $rates): float { $ccy = strtoupper($ccy); return $rates[$ccy] ?? 1.0; } /* -------------- Collect header fields -------------- */ $user_code = trim($_POST['user_code'] ?? ''); $client_name = trim($_POST['client_name'] ?? ''); $email = trim($_POST['email'] ?? ''); $for_user_code = trim($_POST['for_user_code'] ?? ''); $submitted_by = trim($_POST['submitted_by'] ?? ''); if ($client_name === '' || $email === '') { http_response_code(400); echo 'Missing required fields. Go back'; exit; } $company = trim($_POST['company_name'] ?? ''); $phone = trim($_POST['phone'] ?? ''); $website = trim($_POST['website'] ?? ''); $desc = trim($_POST['project_desc'] ?? ''); $budget = trim($_POST['budget_range'] ?? ''); $timeline = trim($_POST['timeline'] ?? ''); /* Selected IDs from the form */ $serviceIds = ints_from_post_array('services'); $addonIds = ints_from_post_array('addons'); /* (Optional) Hours overrides posted from proposal.php if you add names there later. Accept any of these shapes if you decide to post them: - $_POST['hours_services'][] - $_POST['hours_addons'][] - $_POST['hours']['svc'][] / $_POST['hours']['add'][] */ $hours_services = isset($_POST['hours_services']) && is_array($_POST['hours_services']) ? $_POST['hours_services'] : []; $hours_addons = i_