mirror of
https://github.com/getnamingo/registry.git
synced 2025-07-02 00:53:22 +02:00
Almost done work on windows for contact/host create and contact/host search
This commit is contained in:
parent
1af5127cfb
commit
bee6f27188
7 changed files with 1958 additions and 30 deletions
|
@ -8,6 +8,7 @@ use Psr\Container\ContainerInterface;
|
|||
use Selective\XmlDSig\PublicKeyStore;
|
||||
use Selective\XmlDSig\CryptoVerifier;
|
||||
use Selective\XmlDSig\XmlSignatureVerifier;
|
||||
use League\ISO3166\ISO3166;
|
||||
|
||||
class ApplicationsController extends Controller
|
||||
{
|
||||
|
@ -616,11 +617,15 @@ class ApplicationsController extends Controller
|
|||
$registrar = null;
|
||||
}
|
||||
|
||||
$iso3166 = new ISO3166();
|
||||
$countries = $iso3166->all();
|
||||
|
||||
// Default view for GET requests or if POST data is not set
|
||||
$launch_phases = $db->selectValue("SELECT value FROM settings WHERE name = 'launch_phases'");
|
||||
if ($launch_phases == 'on') {
|
||||
return view($response,'admin/domains/createApplication.twig', [
|
||||
'registrars' => $registrars,
|
||||
'countries' => $countries,
|
||||
'registrar' => $registrar,
|
||||
]);
|
||||
} else {
|
||||
|
|
|
@ -399,6 +399,374 @@ class ContactsController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function createContactApi(Request $request, Response $response)
|
||||
{
|
||||
if (envi('MINIMUM_DATA') === 'true') {
|
||||
return $response->withHeader('Location', '/dashboard')->withStatus(302);
|
||||
}
|
||||
|
||||
if ($request->getMethod() === 'POST') {
|
||||
// Retrieve POST data
|
||||
$data = $request->getParsedBody();
|
||||
$db = $this->container->get('db');
|
||||
$iso3166 = new ISO3166();
|
||||
$countries = $iso3166->all();
|
||||
$contactID = $data['contactid'] ?? null;
|
||||
$registrar_id = $data['registrar'] ?? null;
|
||||
$registrars = $db->select("SELECT id, clid, name FROM registrar");
|
||||
if ($_SESSION["auth_roles"] != 0) {
|
||||
$registrar = true;
|
||||
} else {
|
||||
$registrar = null;
|
||||
}
|
||||
|
||||
$postalInfoIntName = $data['intName'] ?? null;
|
||||
$postalInfoIntOrg = $data['org'] ?? null;
|
||||
$postalInfoIntStreet1 = $data['street1'] ?? null;
|
||||
$postalInfoIntStreet2 = $data['street2'] ?? null;
|
||||
$postalInfoIntStreet3 = $data['street3'] ?? null;
|
||||
$postalInfoIntCity = $data['city'] ?? null;
|
||||
$postalInfoIntSp = $data['sp'] ?? null;
|
||||
$postalInfoIntPc = $data['pc'] ?? null;
|
||||
$postalInfoIntCc = $data['cc'] ?? null;
|
||||
|
||||
$postalInfoLocName = $data['locName'] ?? null;
|
||||
$postalInfoLocOrg = $data['locOrg'] ?? null;
|
||||
$postalInfoLocStreet1 = $data['locStreet1'] ?? null;
|
||||
$postalInfoLocStreet2 = $data['locStreet2'] ?? null;
|
||||
$postalInfoLocStreet3 = $data['locStreet3'] ?? null;
|
||||
$postalInfoLocCity = $data['locCity'] ?? null;
|
||||
$postalInfoLocSp = $data['locSP'] ?? null;
|
||||
$postalInfoLocPc = $data['locPC'] ?? null;
|
||||
$postalInfoLocCc = $data['locCC'] ?? null;
|
||||
|
||||
$voice = $data['voice'] ?? null;
|
||||
$fax = $data['fax'] ?? null;
|
||||
$email = strtolower($data['email']) ?? null;
|
||||
$authInfo_pw = $data['authInfoc'] ?? null;
|
||||
|
||||
if (!$contactID) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Please provide a contact ID');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
// Validation for contact ID
|
||||
$invalid_identifier = validate_identifier($contactID);
|
||||
if ($invalid_identifier) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: ' . $invalid_identifier);
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
$contact = $db->select('SELECT * FROM contact WHERE identifier = ?', [$contactID]);
|
||||
if ($contact) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Contact ID already exists');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
$result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]);
|
||||
|
||||
if ($_SESSION["auth_roles"] != 0) {
|
||||
$clid = $result['registrar_id'];
|
||||
} else {
|
||||
$clid = $registrar_id;
|
||||
}
|
||||
|
||||
if ($postalInfoIntName) {
|
||||
if (!$postalInfoIntName) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Missing contact name');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if (preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntName) || !preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntName)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact name');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if ($postalInfoIntOrg) {
|
||||
if (preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntOrg) || !preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntOrg)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact org');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoIntStreet1) {
|
||||
if (preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet1) || !preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet1)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact street');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoIntStreet2) {
|
||||
if (preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet2) || !preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet2)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact street 2');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoIntStreet3) {
|
||||
if (preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet3) || !preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact street 3');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/(^\-)|(^\.)|(\-\-)|(\.\.)|(\.\-)|(\-\.)|(\-$)|(\.$)/', $postalInfoIntCity) || !preg_match('/^[a-z][a-z\-\.\'\s]{2,}$/i', $postalInfoIntCity)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact city');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if ($postalInfoIntSp) {
|
||||
if (preg_match('/(^\-)|(^\.)|(\-\-)|(\.\.)|(\.\-)|(\-\.)|(\-$)|(\.$)/', $postalInfoIntSp) || !preg_match('/^[A-Z][a-zA-Z\-\.\s]{1,}$/', $postalInfoIntSp)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact state/province');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoIntPc) {
|
||||
if (preg_match('/(^\-)|(\-\-)|(\-$)/', $postalInfoIntPc) || !preg_match('/^[A-Z0-9\-\s]{3,}$/', $postalInfoIntPc)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid contact postal code');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($postalInfoLocName) {
|
||||
if (!validateLocField($postalInfoLocName, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact name');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if ($postalInfoLocOrg) {
|
||||
if (!validateLocField($postalInfoLocOrg, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact org');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoLocStreet1) {
|
||||
if (!validateLocField($postalInfoLocStreet1, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact street');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoLocStreet2) {
|
||||
if (!validateLocField($postalInfoLocStreet2, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact street 2');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoLocStreet3) {
|
||||
if (!validateLocField($postalInfoLocStreet3, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact street 3');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validateLocField($postalInfoLocCity, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact city');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if ($postalInfoLocSp) {
|
||||
if (!validateLocField($postalInfoLocSp, 2)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact state/province');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
if ($postalInfoLocPc) {
|
||||
if (!validateLocField($postalInfoLocPc, 3)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Invalid loc contact postal code');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedVoice = normalizePhoneNumber($voice, strtoupper($postalInfoIntCc));
|
||||
if (isset($normalizedVoice['error'])) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: ' . $normalizedVoice['error']);
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
$voice = $normalizedVoice['success'];
|
||||
|
||||
// Validate length of $voice
|
||||
if (strlen($voice) > 17) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Phone number exceeds 17 characters');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if (!empty($fax)) {
|
||||
$normalizedFax = normalizePhoneNumber($fax, strtoupper($postalInfoIntCc));
|
||||
if (isset($normalizedFax['error'])) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: ' . $normalizedFax['error']);
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
// Update the fax number only if normalization was successful.
|
||||
$fax = $normalizedFax['success'];
|
||||
}
|
||||
|
||||
if (!validateUniversalEmail($email)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Email address failed check');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if (!$authInfo_pw) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Email contact authinfo missing');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if ((strlen($authInfo_pw) < 6) || (strlen($authInfo_pw) > 16)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Password needs to be at least 6 and up to 16 characters long');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
if (!preg_match('/[A-Z]/', $authInfo_pw)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Password should have both upper and lower case characters');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
$disclose_voice = isset($data['disclose_voice']) ? 1 : 0;
|
||||
$disclose_fax = isset($data['disclose_fax']) ? 1 : 0;
|
||||
$disclose_email = isset($data['disclose_email']) ? 1 : 0;
|
||||
$disclose_name_int = isset($data['disclose_name_int']) ? 1 : 0;
|
||||
$disclose_name_loc = isset($data['disclose_name_loc']) ? 1 : 0;
|
||||
$disclose_org_int = isset($data['disclose_org_int']) ? 1 : 0;
|
||||
$disclose_org_loc = isset($data['disclose_org_loc']) ? 1 : 0;
|
||||
$disclose_addr_int = isset($data['disclose_addr_int']) ? 1 : 0;
|
||||
$disclose_addr_loc = isset($data['disclose_addr_loc']) ? 1 : 0;
|
||||
|
||||
if ($data['nin']) {
|
||||
$nin = $data['nin'];
|
||||
$nin_type = (isset($data['isBusiness']) && $data['isBusiness'] === 'on') ? 'business' : 'personal';
|
||||
|
||||
if (!preg_match('/\d/', $nin)) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: NIN should contain one or more numbers');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if either postalInfoIntName or postalInfoLocName exists
|
||||
if (!$postalInfoIntName && !$postalInfoLocName) {
|
||||
$this->container->get('flash')->addMessage('error', 'Unable to create contact: At least one of the postal info types (INT or LOC) is required.');
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
$currentDateTime = new \DateTime();
|
||||
$crdate = $currentDateTime->format('Y-m-d H:i:s.v');
|
||||
$db->insert(
|
||||
'contact',
|
||||
[
|
||||
'identifier' => $contactID,
|
||||
'voice' => $voice,
|
||||
'voice_x' => null,
|
||||
'fax' => $fax ?? null,
|
||||
'fax_x' => null,
|
||||
'email' => $email,
|
||||
'nin' => $nin ?? null,
|
||||
'nin_type' => $nin_type ?? null,
|
||||
'clid' => $clid,
|
||||
'crid' => $clid,
|
||||
'crdate' => $crdate,
|
||||
'disclose_voice' => $disclose_voice,
|
||||
'disclose_fax' => $disclose_fax,
|
||||
'disclose_email' => $disclose_email
|
||||
]
|
||||
);
|
||||
$contact_id = $db->getLastInsertId();
|
||||
|
||||
if ($postalInfoIntName) {
|
||||
$db->insert(
|
||||
'contact_postalInfo',
|
||||
[
|
||||
'contact_id' => $contact_id,
|
||||
'type' => 'int',
|
||||
'name' => $postalInfoIntName ?? null,
|
||||
'org' => $postalInfoIntOrg ?? null,
|
||||
'street1' => $postalInfoIntStreet1 ?? null,
|
||||
'street2' => $postalInfoIntStreet2 ?? null,
|
||||
'street3' => $postalInfoIntStreet3 ?? null,
|
||||
'city' => $postalInfoIntCity ?? null,
|
||||
'sp' => $postalInfoIntSp ?? null,
|
||||
'pc' => $postalInfoIntPc ?? null,
|
||||
'cc' => $postalInfoIntCc ?? null,
|
||||
'disclose_name_int' => $disclose_name_int,
|
||||
'disclose_org_int' => $disclose_org_int,
|
||||
'disclose_addr_int' => $disclose_addr_int
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($postalInfoLocName) {
|
||||
$db->insert(
|
||||
'contact_postalInfo',
|
||||
[
|
||||
'contact_id' => $contact_id,
|
||||
'type' => 'loc',
|
||||
'name' => $postalInfoLocName ?? null,
|
||||
'org' => $postalInfoLocOrg ?? null,
|
||||
'street1' => $postalInfoLocStreet1 ?? null,
|
||||
'street2' => $postalInfoLocStreet2 ?? null,
|
||||
'street3' => $postalInfoLocStreet3 ?? null,
|
||||
'city' => $postalInfoLocCity ?? null,
|
||||
'sp' => $postalInfoLocSp ?? null,
|
||||
'pc' => $postalInfoLocPc ?? null,
|
||||
'cc' => $postalInfoLocCc ?? null,
|
||||
'disclose_name_loc' => $disclose_name_loc,
|
||||
'disclose_org_loc' => $disclose_org_loc,
|
||||
'disclose_addr_loc' => $disclose_addr_loc
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$db->insert(
|
||||
'contact_authInfo',
|
||||
[
|
||||
'contact_id' => $contact_id,
|
||||
'authtype' => 'pw',
|
||||
'authinfo' => $authInfo_pw
|
||||
]
|
||||
);
|
||||
|
||||
$db->insert(
|
||||
'contact_status',
|
||||
[
|
||||
'contact_id' => $contact_id,
|
||||
'status' => 'ok'
|
||||
]
|
||||
);
|
||||
|
||||
$db->commit();
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$this->container->get('flash')->addMessage('error', 'Database failure: ' . $e->getMessage());
|
||||
return $response->withHeader('Location', '/contact/create')->withStatus(302);
|
||||
}
|
||||
|
||||
$crdate = $db->selectValue(
|
||||
"SELECT crdate FROM contact WHERE id = ? LIMIT 1",
|
||||
[$contact_id]
|
||||
);
|
||||
|
||||
// Return JSON response
|
||||
$data = [
|
||||
"identifier" => $contactID,
|
||||
"email" => $email,
|
||||
"phone" => $voice,
|
||||
"crdate" => $crdate
|
||||
];
|
||||
|
||||
$response->getBody()->write(json_encode($data));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
|
||||
}
|
||||
|
||||
return $response->withHeader('Location', '/contacts')->withStatus(302);
|
||||
}
|
||||
|
||||
public function viewContact(Request $request, Response $response, $args)
|
||||
{
|
||||
if (envi('MINIMUM_DATA') === 'true') {
|
||||
|
|
|
@ -9,6 +9,7 @@ use Psr\Container\ContainerInterface;
|
|||
use Selective\XmlDSig\PublicKeyStore;
|
||||
use Selective\XmlDSig\CryptoVerifier;
|
||||
use Selective\XmlDSig\XmlSignatureVerifier;
|
||||
use League\ISO3166\ISO3166;
|
||||
|
||||
class DomainsController extends Controller
|
||||
{
|
||||
|
@ -954,12 +955,16 @@ class DomainsController extends Controller
|
|||
|
||||
$launch_phases = $db->selectValue("SELECT value FROM settings WHERE name = 'launch_phases'");
|
||||
|
||||
$iso3166 = new ISO3166();
|
||||
$countries = $iso3166->all();
|
||||
|
||||
// Default view for GET requests or if POST data is not set
|
||||
return view($response,'admin/domains/createDomain.twig', [
|
||||
'registrars' => $registrars,
|
||||
'currencySymbol' => $symbol,
|
||||
'currencyPosition' => $position,
|
||||
'registrar' => $registrar,
|
||||
'countries' => $countries,
|
||||
'launch_phases' => $launch_phases,
|
||||
'currency' => $currency,
|
||||
'registry_currency' => $registry_currency,
|
||||
|
@ -1125,6 +1130,9 @@ class DomainsController extends Controller
|
|||
}
|
||||
$_SESSION['domains_to_update'] = [$domain['punycode']];
|
||||
|
||||
$iso3166 = new ISO3166();
|
||||
$countries = $iso3166->all();
|
||||
|
||||
return view($response,'admin/domains/updateDomain.twig', [
|
||||
'domain' => $domain,
|
||||
'domainStatus' => $domainStatus,
|
||||
|
@ -1135,6 +1143,7 @@ class DomainsController extends Controller
|
|||
'domainContacts' => $domainContacts,
|
||||
'registrar' => $registrars,
|
||||
'currentUri' => $uri,
|
||||
'countries' => $countries,
|
||||
'csrfTokenName' => $csrfTokenName,
|
||||
'csrfTokenValue' => $csrfTokenValue
|
||||
]);
|
||||
|
|
|
@ -56,10 +56,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Registrant Contact') }}" name="contactRegistrant" id="contactRegistrant" required="required">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactRegistrant" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactRegistrant">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -72,12 +72,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Admin Contact') }}" name="contactAdmin">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Admin Contact') }}" name="contactAdmin" id="contactAdmin">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" title="{{ __('Search Contacts') }}" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactAdmin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactAdmin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -90,12 +90,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Tech Contact') }}" name="contactTech">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Tech Contact') }}" name="contactTech" id="contactTech">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactTech" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactTech">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -108,12 +108,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Billing Contact') }}" name="contactBilling">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Billing Contact') }}" name="contactBilling" id="contactBilling">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactBilling" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactBilling">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -222,6 +222,521 @@
|
|||
</div>
|
||||
{% include 'partials/footer.twig' %}
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="contactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Select Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4 text-secondary">
|
||||
{{ __('Search') }}:
|
||||
<div class="ms-2 d-inline-block">
|
||||
<input id="search-input" type="text" class="form-control" aria-label="{{ __('Search contacts') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="contactTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="addContactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Add New Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addContactForm">
|
||||
{{ csrf.field | raw }}
|
||||
<div class="row">
|
||||
<!-- First Column: General & Internationalized Info -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('General & Internationalized Info') }}</h5>
|
||||
|
||||
<!-- Internationalized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="intName" class="form-label required">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="intName" name="intName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameInt" name="disclose_name_int">
|
||||
<span class="form-check-label" for="discloseNameInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if registrars and not registrar %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="registrarDropdown2" class="form-label required">{{ __('Select Registrar') }}</label>
|
||||
<select id="registrarDropdown2" name="registrar" class="form-select" required="required">
|
||||
{% for registrar in registrars %}
|
||||
<option value="{{ registrar.id }}">{{ registrar.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Internationalized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="intOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="intOrg" name="org">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int">
|
||||
<span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="street1" class="form-label required">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="street1" name="street1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="street2" name="street2">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="street3" name="street3">
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="city" class="form-label required">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="city" name="city">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sp" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="sp" name="sp">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pc" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="pc" name="pc">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cc" class="form-label required">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="cc" name="cc">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrInt" name="disclose_addr_int">
|
||||
<span class="form-check-label" for="discloseAddrInt">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Second Column: Voice, Fax, Email, and other details -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Contact Details') }}</h5>
|
||||
|
||||
<!-- Contact ID -->
|
||||
<div class="mb-3">
|
||||
<label for="contactid" class="form-label required">{{ __('Contact ID') }}</label>
|
||||
<input type="text" class="form-control" id="contactid" name="contactid" required="required">
|
||||
<small class="form-text text-muted">{{ __('Auto-generated ID for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- Voice -->
|
||||
<div class="mb-3">
|
||||
<label for="voice" class="form-label required">{{ __('Voice') }}</label>
|
||||
<input type="tel" class="form-control" id="voice" name="voice" required="required">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseVoice" name="disclose_voice">
|
||||
<span class="form-check-label" for="discloseVoice">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Fax -->
|
||||
<div class="mb-3">
|
||||
<label for="fax" class="form-label">{{ __('Fax') }}</label>
|
||||
<input type="tel" class="form-control" id="fax" name="fax">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseFax" name="disclose_fax">
|
||||
<span class="form-check-label" for="discloseFax">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label required">{{ __('Email') }}</label>
|
||||
<input type="text" class="form-control" id="email" name="email" required="required" autocapitalize="none">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseEmail" name="disclose_email">
|
||||
<span class="form-check-label" for="discloseEmail">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- AuthInfo for Contact -->
|
||||
<div class="mb-3">
|
||||
<label for="authInfoc" class="form-label required">{{ __('Contact AuthInfo') }}</label>
|
||||
<input type="text" class="form-control" id="authInfoc" name="authInfoc" readonly>
|
||||
<small class="form-text text-muted">{{ __('Auto-generated authentication information for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- NIN - National Identification Number -->
|
||||
<div class="mb-3">
|
||||
<label for="nin" class="form-label">{{ __('NIN - National Identification Number') }}</label>
|
||||
<input type="text" class="form-control" id="nin" name="nin">
|
||||
</div>
|
||||
|
||||
<!-- Personal or Business Checkbox -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="isBusiness" name="isBusiness">
|
||||
<label class="form-check-label" for="isBusiness">{{ __('This is a Business Contact') }}</label>
|
||||
</div>
|
||||
<!-- You can invert the logic if you prefer the default to be 'Personal' instead of 'Business' -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Info Checkbox -->
|
||||
<div class="mb-3">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="toggleLoc">
|
||||
<span class="form-check-label" for="toggleLoc">{{ __('Include Localized Info') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Localized Info Rows -->
|
||||
<div class="row" id="localizedInfo" style="display: none;">
|
||||
<!-- Localized Postal Info: First Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Personal Details') }}</h5>
|
||||
|
||||
<!-- Localized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="locName" class="form-label">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="locName" name="locName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameLoc" name="disclose_name_loc">
|
||||
<span class="form-check-label" for="discloseNameLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="locOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="locOrg" name="locOrg">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgLoc" name="disclose_org_loc">
|
||||
<span class="form-check-label" for="discloseOrgLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet1" class="form-label">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="locStreet1" name="locStreet1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locStreet2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="locStreet2" name="locStreet2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Postal Info: Second Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Address Details') }}</h5>
|
||||
|
||||
<!-- Continued Localized Street Detail -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="locStreet3" name="locStreet3">
|
||||
</div>
|
||||
|
||||
<!-- Localized City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="locCity" class="form-label">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="locCity" name="locCity">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locSP" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="locSP" name="locSP">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locPC" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="locPC" name="locPC">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locCC" class="form-label">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="locCC" name="locCC">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrLoc" name="disclose_addr_loc">
|
||||
<span class="form-check-label" for="discloseAddrLoc">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary btn-3" data-bs-dismiss="modal">{{ __('Cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">{{ __('Create Contact') }}</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link href="/assets/css/tabulator.min.css" rel="stylesheet">
|
||||
<script src="/assets/js/tabulator.min.js" defer></script>
|
||||
<script>
|
||||
let addContactTargetInputId = null;
|
||||
|
||||
// Capture the target input when opening "Add Contact" modal
|
||||
document.addEventListener("click", function (e) {
|
||||
let openBtn = e.target.closest('.open-add-modal');
|
||||
if (openBtn) {
|
||||
addContactTargetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("addContactForm").addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
let form = this;
|
||||
let formData = new FormData(form);
|
||||
|
||||
fetch("/contact/create-api", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Accept": "application/json" // Ensure JSON response is expected
|
||||
}
|
||||
})
|
||||
.then(response => response.json()) // Convert response to JSON
|
||||
.then(data => {
|
||||
// Ensure the response contains an identifier
|
||||
if (data.identifier && addContactTargetInputId) {
|
||||
let targetInput = document.getElementById(addContactTargetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = data.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("addContactModal");
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
|
||||
// Reset the form
|
||||
form.reset();
|
||||
addContactTargetInputId = null;
|
||||
})
|
||||
.catch(error => console.error("❌ Error adding contact:", error));
|
||||
});
|
||||
|
||||
let targetInputId = null;
|
||||
|
||||
// Ensure target input is set correctly before the modal opens
|
||||
document.addEventListener("mousedown", function(e) {
|
||||
let openBtn = e.target.closest('.open-modal');
|
||||
if (openBtn) {
|
||||
targetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click on "Add to Main" button
|
||||
document.addEventListener("click", function(e) {
|
||||
let addBtn = e.target.closest('.add-to-main');
|
||||
if (!addBtn) return;
|
||||
|
||||
if (!targetInputId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let identifier = addBtn.getAttribute('data-identifier');
|
||||
|
||||
let targetInput = document.getElementById(targetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = identifier;
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("contactModal");
|
||||
if (modalElement) {
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input selection after choosing a contact
|
||||
targetInputId = null;
|
||||
});
|
||||
|
||||
var table;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
|
||||
const toggleLocCheckbox = document.getElementById('toggleLoc');
|
||||
const localizedSection = document.getElementById('localizedInfo');
|
||||
|
||||
toggleLocCheckbox.addEventListener('change', function() {
|
||||
if (toggleLocCheckbox.checked) {
|
||||
localizedSection.style.display = "flex";
|
||||
} else {
|
||||
localizedSection.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Generate ID for Contact
|
||||
const contactidInput = document.getElementById('contactid');
|
||||
contactidInput.value = generateAuthInfoC();
|
||||
|
||||
// Generate authInfo for Contact
|
||||
const authInfoInput = document.getElementById('authInfoc');
|
||||
authInfoInput.value = generateAuthInfoC();
|
||||
|
||||
function generateAuthInfoC() {
|
||||
const length = 16;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let retVal = "";
|
||||
let digitCount = 0;
|
||||
|
||||
// Generate initial random string
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * charset.length);
|
||||
const char = charset.charAt(randomIndex);
|
||||
retVal += char;
|
||||
if (char >= '0' && char <= '9') {
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there are at least two digits in the string
|
||||
while (digitCount < 2) {
|
||||
// Replace a non-digit character at a random position with a digit
|
||||
const replacePosition = Math.floor(Math.random() * length);
|
||||
if (!(retVal[replacePosition] >= '0' && retVal[replacePosition] <= '9')) {
|
||||
const randomDigit = Math.floor(Math.random() * 10); // Generate a digit from 0 to 9
|
||||
retVal = retVal.substring(0, replacePosition) + randomDigit + retVal.substring(replacePosition + 1);
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function contactLinkFormatter(cell){
|
||||
var value = cell.getValue();
|
||||
return `<a href="/contact/view/${value}" style="font-weight:bold;">${value}</a>`;
|
||||
}
|
||||
|
||||
function actionsFormatter(cell, formatterParams, onRendered) {
|
||||
return `
|
||||
<button class="btn btn-outline-success btn-icon add-to-main" data-identifier="${cell.getRow().getData().identifier}" title="Add to Contact">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 5v14m-7 -7h14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
var searchTerm = ""; // global variable to hold the search term
|
||||
|
||||
function updateSearchTerm(term) {
|
||||
searchTerm = term;
|
||||
table.replaceData();
|
||||
}
|
||||
|
||||
table = new Tabulator("#contactTable", {
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
height:400,
|
||||
paginationSizeSelector:[10, 25, 50, 100],
|
||||
clipboard:true,
|
||||
clipboardPasteAction:"replace",
|
||||
sortMode: "remote",
|
||||
ajaxURL: "/api/records/contact",
|
||||
ajaxURLGenerator: function(url, config, params) {
|
||||
var queryParts = [];
|
||||
|
||||
// Handle search term
|
||||
if (searchTerm) {
|
||||
queryParts.push("filter1=identifier,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter2=email,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter3=voice,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter4=crdate,cs," + encodeURIComponent(searchTerm));
|
||||
}
|
||||
|
||||
// Handle sorting from Tabulator
|
||||
if (params.sort && params.sort.length > 0) {
|
||||
var sorter = params.sort[0]; // single-column sorting
|
||||
var sortField = encodeURIComponent(sorter.field);
|
||||
var sortDir = (sorter.dir === "asc" ? "asc" : "desc");
|
||||
queryParts.push("order=" + sortField + "," + sortDir);
|
||||
} else {
|
||||
// fallback default order if no sorters
|
||||
queryParts.push("order=id,desc");
|
||||
}
|
||||
|
||||
// Include pagination parameters
|
||||
if (params.page) {
|
||||
queryParts.push("page=" + params.page + "," + params.size);
|
||||
}
|
||||
|
||||
return url + "?" + queryParts.join("&");
|
||||
},
|
||||
ajaxResponse: function(url, params, response) {
|
||||
if (response && Array.isArray(response.records) && typeof response.results === 'number') {
|
||||
var lastPage = Math.ceil(response.results / this.options.paginationSize);
|
||||
return {
|
||||
last_page: lastPage, // Calculated total number of pages
|
||||
data: response.records, // Data for the current page
|
||||
};
|
||||
} else {
|
||||
console.error('Unexpected response format', response);
|
||||
return { last_page: 1, data: [] };
|
||||
}
|
||||
},
|
||||
layout:"fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseStartOpen:false,
|
||||
resizableColumns:false,
|
||||
placeholder: "{{ __('No Data') }}",
|
||||
columns:[
|
||||
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
|
||||
{title:"{{ __('Identifier') }}", field:"identifier", width:200, minWidth:150, resizable:false, headerSort:true, formatter: contactLinkFormatter, responsive:0},
|
||||
{title:"{{ __('Email') }}", field:"email", minWidth:250, resizable:false, headerSort:true, responsive:2},
|
||||
{title: "{{ __('Actions') }}", formatter: actionsFormatter, minWidth:80, resizable:false, headerSort:false, download:false, hozAlign: "center", responsive:0, cellClick: function(e, cell){
|
||||
if (e.target.closest('.delete-btn')) {
|
||||
e.preventDefault(); // Prevent the default link behavior
|
||||
Swal.fire({
|
||||
title: "{{ __('Are you sure you want to delete this contact?') }}",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "{{ __('Confirm') }}"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
let deleteUrl = e.target.closest('.delete-btn').getAttribute('data-delete-url');
|
||||
window.location.href = deleteUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}},
|
||||
]
|
||||
});
|
||||
var searchInput = document.getElementById("search-input");
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
updateSearchTerm(searchInput.value);
|
||||
}, 300); // 300ms delay
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
|
|
|
@ -68,10 +68,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Registrant Contact') }}" name="contactRegistrant" id="contactRegistrant" required="required">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactRegistrant" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactRegistrant">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -84,12 +84,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Admin Contact') }}" name="contactAdmin">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Admin Contact') }}" name="contactAdmin" id="contactAdmin">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactAdmin" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactAdmin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -102,12 +102,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Tech Contact') }}" name="contactTech">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Tech Contact') }}" name="contactTech" id="contactTech">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactTech" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactTech">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -120,12 +120,12 @@
|
|||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Billing Contact') }}" name="contactBilling">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Billing Contact') }}" name="contactBilling" id="contactBilling">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactBilling" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactBilling">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -346,6 +346,521 @@
|
|||
</div>
|
||||
{% include 'partials/footer.twig' %}
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="contactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Select Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4 text-secondary">
|
||||
{{ __('Search') }}:
|
||||
<div class="ms-2 d-inline-block">
|
||||
<input id="search-input" type="text" class="form-control" aria-label="{{ __('Search contacts') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="contactTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="addContactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Add New Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addContactForm">
|
||||
{{ csrf.field | raw }}
|
||||
<div class="row">
|
||||
<!-- First Column: General & Internationalized Info -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('General & Internationalized Info') }}</h5>
|
||||
|
||||
<!-- Internationalized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="intName" class="form-label required">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="intName" name="intName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameInt" name="disclose_name_int">
|
||||
<span class="form-check-label" for="discloseNameInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if registrars and not registrar %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="registrarDropdown2" class="form-label required">{{ __('Select Registrar') }}</label>
|
||||
<select id="registrarDropdown2" name="registrar" class="form-select" required="required">
|
||||
{% for registrar in registrars %}
|
||||
<option value="{{ registrar.id }}">{{ registrar.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Internationalized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="intOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="intOrg" name="org">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int">
|
||||
<span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="street1" class="form-label required">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="street1" name="street1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="street2" name="street2">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="street3" name="street3">
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="city" class="form-label required">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="city" name="city">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sp" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="sp" name="sp">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pc" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="pc" name="pc">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cc" class="form-label required">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="cc" name="cc">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrInt" name="disclose_addr_int">
|
||||
<span class="form-check-label" for="discloseAddrInt">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Second Column: Voice, Fax, Email, and other details -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Contact Details') }}</h5>
|
||||
|
||||
<!-- Contact ID -->
|
||||
<div class="mb-3">
|
||||
<label for="contactid" class="form-label required">{{ __('Contact ID') }}</label>
|
||||
<input type="text" class="form-control" id="contactid" name="contactid" required="required">
|
||||
<small class="form-text text-muted">{{ __('Auto-generated ID for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- Voice -->
|
||||
<div class="mb-3">
|
||||
<label for="voice" class="form-label required">{{ __('Voice') }}</label>
|
||||
<input type="tel" class="form-control" id="voice" name="voice" required="required">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseVoice" name="disclose_voice">
|
||||
<span class="form-check-label" for="discloseVoice">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Fax -->
|
||||
<div class="mb-3">
|
||||
<label for="fax" class="form-label">{{ __('Fax') }}</label>
|
||||
<input type="tel" class="form-control" id="fax" name="fax">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseFax" name="disclose_fax">
|
||||
<span class="form-check-label" for="discloseFax">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label required">{{ __('Email') }}</label>
|
||||
<input type="text" class="form-control" id="email" name="email" required="required" autocapitalize="none">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseEmail" name="disclose_email">
|
||||
<span class="form-check-label" for="discloseEmail">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- AuthInfo for Contact -->
|
||||
<div class="mb-3">
|
||||
<label for="authInfoc" class="form-label required">{{ __('Contact AuthInfo') }}</label>
|
||||
<input type="text" class="form-control" id="authInfoc" name="authInfoc" readonly>
|
||||
<small class="form-text text-muted">{{ __('Auto-generated authentication information for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- NIN - National Identification Number -->
|
||||
<div class="mb-3">
|
||||
<label for="nin" class="form-label">{{ __('NIN - National Identification Number') }}</label>
|
||||
<input type="text" class="form-control" id="nin" name="nin">
|
||||
</div>
|
||||
|
||||
<!-- Personal or Business Checkbox -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="isBusiness" name="isBusiness">
|
||||
<label class="form-check-label" for="isBusiness">{{ __('This is a Business Contact') }}</label>
|
||||
</div>
|
||||
<!-- You can invert the logic if you prefer the default to be 'Personal' instead of 'Business' -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Info Checkbox -->
|
||||
<div class="mb-3">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="toggleLoc">
|
||||
<span class="form-check-label" for="toggleLoc">{{ __('Include Localized Info') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Localized Info Rows -->
|
||||
<div class="row" id="localizedInfo" style="display: none;">
|
||||
<!-- Localized Postal Info: First Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Personal Details') }}</h5>
|
||||
|
||||
<!-- Localized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="locName" class="form-label">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="locName" name="locName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameLoc" name="disclose_name_loc">
|
||||
<span class="form-check-label" for="discloseNameLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="locOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="locOrg" name="locOrg">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgLoc" name="disclose_org_loc">
|
||||
<span class="form-check-label" for="discloseOrgLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet1" class="form-label">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="locStreet1" name="locStreet1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locStreet2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="locStreet2" name="locStreet2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Postal Info: Second Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Address Details') }}</h5>
|
||||
|
||||
<!-- Continued Localized Street Detail -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="locStreet3" name="locStreet3">
|
||||
</div>
|
||||
|
||||
<!-- Localized City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="locCity" class="form-label">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="locCity" name="locCity">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locSP" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="locSP" name="locSP">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locPC" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="locPC" name="locPC">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locCC" class="form-label">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="locCC" name="locCC">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrLoc" name="disclose_addr_loc">
|
||||
<span class="form-check-label" for="discloseAddrLoc">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary btn-3" data-bs-dismiss="modal">{{ __('Cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">{{ __('Create Contact') }}</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link href="/assets/css/tabulator.min.css" rel="stylesheet">
|
||||
<script src="/assets/js/tabulator.min.js" defer></script>
|
||||
<script>
|
||||
let addContactTargetInputId = null;
|
||||
|
||||
// Capture the target input when opening "Add Contact" modal
|
||||
document.addEventListener("click", function (e) {
|
||||
let openBtn = e.target.closest('.open-add-modal');
|
||||
if (openBtn) {
|
||||
addContactTargetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("addContactForm").addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
let form = this;
|
||||
let formData = new FormData(form);
|
||||
|
||||
fetch("/contact/create-api", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Accept": "application/json" // Ensure JSON response is expected
|
||||
}
|
||||
})
|
||||
.then(response => response.json()) // Convert response to JSON
|
||||
.then(data => {
|
||||
// Ensure the response contains an identifier
|
||||
if (data.identifier && addContactTargetInputId) {
|
||||
let targetInput = document.getElementById(addContactTargetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = data.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("addContactModal");
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
|
||||
// Reset the form
|
||||
form.reset();
|
||||
addContactTargetInputId = null;
|
||||
})
|
||||
.catch(error => console.error("❌ Error adding contact:", error));
|
||||
});
|
||||
|
||||
let targetInputId = null;
|
||||
|
||||
// Ensure target input is set correctly before the modal opens
|
||||
document.addEventListener("mousedown", function(e) {
|
||||
let openBtn = e.target.closest('.open-modal');
|
||||
if (openBtn) {
|
||||
targetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click on "Add to Main" button
|
||||
document.addEventListener("click", function(e) {
|
||||
let addBtn = e.target.closest('.add-to-main');
|
||||
if (!addBtn) return;
|
||||
|
||||
if (!targetInputId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let identifier = addBtn.getAttribute('data-identifier');
|
||||
|
||||
let targetInput = document.getElementById(targetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = identifier;
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("contactModal");
|
||||
if (modalElement) {
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input selection after choosing a contact
|
||||
targetInputId = null;
|
||||
});
|
||||
|
||||
var table;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
|
||||
const toggleLocCheckbox = document.getElementById('toggleLoc');
|
||||
const localizedSection = document.getElementById('localizedInfo');
|
||||
|
||||
toggleLocCheckbox.addEventListener('change', function() {
|
||||
if (toggleLocCheckbox.checked) {
|
||||
localizedSection.style.display = "flex";
|
||||
} else {
|
||||
localizedSection.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Generate ID for Contact
|
||||
const contactidInput = document.getElementById('contactid');
|
||||
contactidInput.value = generateAuthInfoC();
|
||||
|
||||
// Generate authInfo for Contact
|
||||
const authInfoInput = document.getElementById('authInfoc');
|
||||
authInfoInput.value = generateAuthInfoC();
|
||||
|
||||
function generateAuthInfoC() {
|
||||
const length = 16;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let retVal = "";
|
||||
let digitCount = 0;
|
||||
|
||||
// Generate initial random string
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * charset.length);
|
||||
const char = charset.charAt(randomIndex);
|
||||
retVal += char;
|
||||
if (char >= '0' && char <= '9') {
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there are at least two digits in the string
|
||||
while (digitCount < 2) {
|
||||
// Replace a non-digit character at a random position with a digit
|
||||
const replacePosition = Math.floor(Math.random() * length);
|
||||
if (!(retVal[replacePosition] >= '0' && retVal[replacePosition] <= '9')) {
|
||||
const randomDigit = Math.floor(Math.random() * 10); // Generate a digit from 0 to 9
|
||||
retVal = retVal.substring(0, replacePosition) + randomDigit + retVal.substring(replacePosition + 1);
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function contactLinkFormatter(cell){
|
||||
var value = cell.getValue();
|
||||
return `<a href="/contact/view/${value}" style="font-weight:bold;">${value}</a>`;
|
||||
}
|
||||
|
||||
function actionsFormatter(cell, formatterParams, onRendered) {
|
||||
return `
|
||||
<button class="btn btn-outline-success btn-icon add-to-main" data-identifier="${cell.getRow().getData().identifier}" title="Add to Contact">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 5v14m-7 -7h14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
var searchTerm = ""; // global variable to hold the search term
|
||||
|
||||
function updateSearchTerm(term) {
|
||||
searchTerm = term;
|
||||
table.replaceData();
|
||||
}
|
||||
|
||||
table = new Tabulator("#contactTable", {
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
height:400,
|
||||
paginationSizeSelector:[10, 25, 50, 100],
|
||||
clipboard:true,
|
||||
clipboardPasteAction:"replace",
|
||||
sortMode: "remote",
|
||||
ajaxURL: "/api/records/contact",
|
||||
ajaxURLGenerator: function(url, config, params) {
|
||||
var queryParts = [];
|
||||
|
||||
// Handle search term
|
||||
if (searchTerm) {
|
||||
queryParts.push("filter1=identifier,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter2=email,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter3=voice,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter4=crdate,cs," + encodeURIComponent(searchTerm));
|
||||
}
|
||||
|
||||
// Handle sorting from Tabulator
|
||||
if (params.sort && params.sort.length > 0) {
|
||||
var sorter = params.sort[0]; // single-column sorting
|
||||
var sortField = encodeURIComponent(sorter.field);
|
||||
var sortDir = (sorter.dir === "asc" ? "asc" : "desc");
|
||||
queryParts.push("order=" + sortField + "," + sortDir);
|
||||
} else {
|
||||
// fallback default order if no sorters
|
||||
queryParts.push("order=id,desc");
|
||||
}
|
||||
|
||||
// Include pagination parameters
|
||||
if (params.page) {
|
||||
queryParts.push("page=" + params.page + "," + params.size);
|
||||
}
|
||||
|
||||
return url + "?" + queryParts.join("&");
|
||||
},
|
||||
ajaxResponse: function(url, params, response) {
|
||||
if (response && Array.isArray(response.records) && typeof response.results === 'number') {
|
||||
var lastPage = Math.ceil(response.results / this.options.paginationSize);
|
||||
return {
|
||||
last_page: lastPage, // Calculated total number of pages
|
||||
data: response.records, // Data for the current page
|
||||
};
|
||||
} else {
|
||||
console.error('Unexpected response format', response);
|
||||
return { last_page: 1, data: [] };
|
||||
}
|
||||
},
|
||||
layout:"fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseStartOpen:false,
|
||||
resizableColumns:false,
|
||||
placeholder: "{{ __('No Data') }}",
|
||||
columns:[
|
||||
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
|
||||
{title:"{{ __('Identifier') }}", field:"identifier", width:200, minWidth:150, resizable:false, headerSort:true, formatter: contactLinkFormatter, responsive:0},
|
||||
{title:"{{ __('Email') }}", field:"email", minWidth:250, resizable:false, headerSort:true, responsive:2},
|
||||
{title: "{{ __('Actions') }}", formatter: actionsFormatter, minWidth:80, resizable:false, headerSort:false, download:false, hozAlign: "center", responsive:0, cellClick: function(e, cell){
|
||||
if (e.target.closest('.delete-btn')) {
|
||||
e.preventDefault(); // Prevent the default link behavior
|
||||
Swal.fire({
|
||||
title: "{{ __('Are you sure you want to delete this contact?') }}",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "{{ __('Confirm') }}"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
let deleteUrl = e.target.closest('.delete-btn').getAttribute('data-delete-url');
|
||||
window.location.href = deleteUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}},
|
||||
]
|
||||
});
|
||||
var searchInput = document.getElementById("search-input");
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
updateSearchTerm(searchInput.value);
|
||||
}, 300); // 300ms delay
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
|
|
|
@ -84,10 +84,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Registrant Contact') }}" name="contactRegistrant" id="contactRegistrant" value="{{ domainRegistrant.identifier }}" required>
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactRegistrant" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactRegistrant">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -118,10 +118,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Admin Contact') }}" name="contactAdmin" id="contactAdmin" value="{{ contactAdmin }}" required>
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactAdmin" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactAdmin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -137,10 +137,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Tech Contact') }}" name="contactTech" id="contactTech" value="{{ contactTech }}" required>
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactTech" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactTech">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -156,10 +156,10 @@
|
|||
<div class="input-group input-group-flat mb-2">
|
||||
<input type="text" class="form-control" placeholder="{{ __('Billing Contact') }}" name="contactBilling" id="contactBilling" value="{{ contactBilling }}">
|
||||
<span class="input-group-text">
|
||||
<a href="{{route('listContacts')}}" class="link-secondary ms-2" title="{{ __('Search Contacts') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-modal" data-bs-toggle="modal" data-bs-target="#contactModal" data-input="contactBilling" title="{{ __('Search Contacts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
</a>
|
||||
<a href="{{route('createContact')}}" class="link-secondary ms-2" title="{{ __('Create Contact') }}" data-bs-toggle="tooltip" target="_blank">
|
||||
<a href="#" class="link-secondary ms-2 open-add-modal" title="{{ __('Create Contact') }}" data-bs-toggle="modal" data-bs-target="#addContactModal" data-input="contactBilling">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -374,6 +374,521 @@
|
|||
</div>
|
||||
{% include 'partials/footer.twig' %}
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="contactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Select Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4 text-secondary">
|
||||
{{ __('Search') }}:
|
||||
<div class="ms-2 d-inline-block">
|
||||
<input id="search-input" type="text" class="form-control" aria-label="{{ __('Search contacts') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="contactTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="addContactModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Add New Contact') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addContactForm">
|
||||
{{ csrf.field | raw }}
|
||||
<div class="row">
|
||||
<!-- First Column: General & Internationalized Info -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('General & Internationalized Info') }}</h5>
|
||||
|
||||
<!-- Internationalized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="intName" class="form-label required">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="intName" name="intName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameInt" name="disclose_name_int">
|
||||
<span class="form-check-label" for="discloseNameInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if registrars and not registrar %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="registrarDropdown2" class="form-label required">{{ __('Select Registrar') }}</label>
|
||||
<select id="registrarDropdown2" name="registrar" class="form-select" required="required">
|
||||
{% for registrar in registrars %}
|
||||
<option value="{{ registrar.id }}">{{ registrar.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Internationalized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="intOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="intOrg" name="org">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int">
|
||||
<span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="street1" class="form-label required">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="street1" name="street1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="street2" name="street2">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="street3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="street3" name="street3">
|
||||
</div>
|
||||
|
||||
<!-- Internationalized Address: City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="city" class="form-label required">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="city" name="city">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sp" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="sp" name="sp">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pc" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="pc" name="pc">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cc" class="form-label required">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="cc" name="cc">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrInt" name="disclose_addr_int">
|
||||
<span class="form-check-label" for="discloseAddrInt">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Second Column: Voice, Fax, Email, and other details -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Contact Details') }}</h5>
|
||||
|
||||
<!-- Contact ID -->
|
||||
<div class="mb-3">
|
||||
<label for="contactid" class="form-label required">{{ __('Contact ID') }}</label>
|
||||
<input type="text" class="form-control" id="contactid" name="contactid" required="required">
|
||||
<small class="form-text text-muted">{{ __('Auto-generated ID for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- Voice -->
|
||||
<div class="mb-3">
|
||||
<label for="voice" class="form-label required">{{ __('Voice') }}</label>
|
||||
<input type="tel" class="form-control" id="voice" name="voice" required="required">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseVoice" name="disclose_voice">
|
||||
<span class="form-check-label" for="discloseVoice">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Fax -->
|
||||
<div class="mb-3">
|
||||
<label for="fax" class="form-label">{{ __('Fax') }}</label>
|
||||
<input type="tel" class="form-control" id="fax" name="fax">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseFax" name="disclose_fax">
|
||||
<span class="form-check-label" for="discloseFax">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label required">{{ __('Email') }}</label>
|
||||
<input type="text" class="form-control" id="email" name="email" required="required" autocapitalize="none">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseEmail" name="disclose_email">
|
||||
<span class="form-check-label" for="discloseEmail">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- AuthInfo for Contact -->
|
||||
<div class="mb-3">
|
||||
<label for="authInfoc" class="form-label required">{{ __('Contact AuthInfo') }}</label>
|
||||
<input type="text" class="form-control" id="authInfoc" name="authInfoc" readonly>
|
||||
<small class="form-text text-muted">{{ __('Auto-generated authentication information for the contact') }}.</small>
|
||||
</div>
|
||||
|
||||
<!-- NIN - National Identification Number -->
|
||||
<div class="mb-3">
|
||||
<label for="nin" class="form-label">{{ __('NIN - National Identification Number') }}</label>
|
||||
<input type="text" class="form-control" id="nin" name="nin">
|
||||
</div>
|
||||
|
||||
<!-- Personal or Business Checkbox -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="isBusiness" name="isBusiness">
|
||||
<label class="form-check-label" for="isBusiness">{{ __('This is a Business Contact') }}</label>
|
||||
</div>
|
||||
<!-- You can invert the logic if you prefer the default to be 'Personal' instead of 'Business' -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Info Checkbox -->
|
||||
<div class="mb-3">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="toggleLoc">
|
||||
<span class="form-check-label" for="toggleLoc">{{ __('Include Localized Info') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Localized Info Rows -->
|
||||
<div class="row" id="localizedInfo" style="display: none;">
|
||||
<!-- Localized Postal Info: First Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Personal Details') }}</h5>
|
||||
|
||||
<!-- Localized Name -->
|
||||
<div class="mb-3">
|
||||
<label for="locName" class="form-label">{{ __('Name') }}</label>
|
||||
<input type="text" class="form-control" id="locName" name="locName">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseNameLoc" name="disclose_name_loc">
|
||||
<span class="form-check-label" for="discloseNameLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Organization -->
|
||||
<div class="mb-3">
|
||||
<label for="locOrg" class="form-label">{{ __('Organization') }}</label>
|
||||
<input type="text" class="form-control" id="locOrg" name="locOrg">
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseOrgLoc" name="disclose_org_loc">
|
||||
<span class="form-check-label" for="discloseOrgLoc">{{ __('Disclose in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localized Street Details -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet1" class="form-label">{{ __('Street') }} 1</label>
|
||||
<input type="text" class="form-control" id="locStreet1" name="locStreet1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locStreet2" class="form-label">{{ __('Street') }} 2</label>
|
||||
<input type="text" class="form-control" id="locStreet2" name="locStreet2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localized Postal Info: Second Column -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-3">{{ __('Localized Postal Info: Address Details') }}</h5>
|
||||
|
||||
<!-- Continued Localized Street Detail -->
|
||||
<div class="mb-3">
|
||||
<label for="locStreet3" class="form-label">{{ __('Street') }} 3</label>
|
||||
<input type="text" class="form-control" id="locStreet3" name="locStreet3">
|
||||
</div>
|
||||
|
||||
<!-- Localized City, SP, PC, CC -->
|
||||
<div class="mb-3">
|
||||
<label for="locCity" class="form-label">{{ __('City') }}</label>
|
||||
<input type="text" class="form-control" id="locCity" name="locCity">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locSP" class="form-label">{{ __('State/Province') }}</label>
|
||||
<input type="text" class="form-control" id="locSP" name="locSP">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locPC" class="form-label">{{ __('Postal Code') }}</label>
|
||||
<input type="text" class="form-control" id="locPC" name="locPC">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="locCC" class="form-label">{{ __('Country') }}</label>
|
||||
<select class="form-select" id="locCC" name="locCC">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.alpha2|lower }}">{{ country.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="discloseAddrLoc" name="disclose_addr_loc">
|
||||
<span class="form-check-label" for="discloseAddrLoc">{{ __('Disclose Address in WHOIS') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary btn-3" data-bs-dismiss="modal">{{ __('Cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">{{ __('Create Contact') }}</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link href="/assets/css/tabulator.min.css" rel="stylesheet">
|
||||
<script src="/assets/js/tabulator.min.js" defer></script>
|
||||
<script>
|
||||
let addContactTargetInputId = null;
|
||||
|
||||
// Capture the target input when opening "Add Contact" modal
|
||||
document.addEventListener("click", function (e) {
|
||||
let openBtn = e.target.closest('.open-add-modal');
|
||||
if (openBtn) {
|
||||
addContactTargetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("addContactForm").addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
let form = this;
|
||||
let formData = new FormData(form);
|
||||
|
||||
fetch("/contact/create-api", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Accept": "application/json" // Ensure JSON response is expected
|
||||
}
|
||||
})
|
||||
.then(response => response.json()) // Convert response to JSON
|
||||
.then(data => {
|
||||
// Ensure the response contains an identifier
|
||||
if (data.identifier && addContactTargetInputId) {
|
||||
let targetInput = document.getElementById(addContactTargetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = data.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("addContactModal");
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
|
||||
// Reset the form
|
||||
form.reset();
|
||||
addContactTargetInputId = null;
|
||||
})
|
||||
.catch(error => console.error("❌ Error adding contact:", error));
|
||||
});
|
||||
|
||||
let targetInputId = null;
|
||||
|
||||
// Ensure target input is set correctly before the modal opens
|
||||
document.addEventListener("mousedown", function(e) {
|
||||
let openBtn = e.target.closest('.open-modal');
|
||||
if (openBtn) {
|
||||
targetInputId = openBtn.getAttribute('data-input');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click on "Add to Main" button
|
||||
document.addEventListener("click", function(e) {
|
||||
let addBtn = e.target.closest('.add-to-main');
|
||||
if (!addBtn) return;
|
||||
|
||||
if (!targetInputId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let identifier = addBtn.getAttribute('data-identifier');
|
||||
|
||||
let targetInput = document.getElementById(targetInputId);
|
||||
if (targetInput) {
|
||||
targetInput.value = identifier;
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
let modalElement = document.getElementById("contactModal");
|
||||
if (modalElement) {
|
||||
let modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input selection after choosing a contact
|
||||
targetInputId = null;
|
||||
});
|
||||
|
||||
var table;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
|
||||
const toggleLocCheckbox = document.getElementById('toggleLoc');
|
||||
const localizedSection = document.getElementById('localizedInfo');
|
||||
|
||||
toggleLocCheckbox.addEventListener('change', function() {
|
||||
if (toggleLocCheckbox.checked) {
|
||||
localizedSection.style.display = "flex";
|
||||
} else {
|
||||
localizedSection.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Generate ID for Contact
|
||||
const contactidInput = document.getElementById('contactid');
|
||||
contactidInput.value = generateAuthInfoC();
|
||||
|
||||
// Generate authInfo for Contact
|
||||
const authInfoInput = document.getElementById('authInfoc');
|
||||
authInfoInput.value = generateAuthInfoC();
|
||||
|
||||
function generateAuthInfoC() {
|
||||
const length = 16;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let retVal = "";
|
||||
let digitCount = 0;
|
||||
|
||||
// Generate initial random string
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * charset.length);
|
||||
const char = charset.charAt(randomIndex);
|
||||
retVal += char;
|
||||
if (char >= '0' && char <= '9') {
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there are at least two digits in the string
|
||||
while (digitCount < 2) {
|
||||
// Replace a non-digit character at a random position with a digit
|
||||
const replacePosition = Math.floor(Math.random() * length);
|
||||
if (!(retVal[replacePosition] >= '0' && retVal[replacePosition] <= '9')) {
|
||||
const randomDigit = Math.floor(Math.random() * 10); // Generate a digit from 0 to 9
|
||||
retVal = retVal.substring(0, replacePosition) + randomDigit + retVal.substring(replacePosition + 1);
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function contactLinkFormatter(cell){
|
||||
var value = cell.getValue();
|
||||
return `<a href="/contact/view/${value}" style="font-weight:bold;">${value}</a>`;
|
||||
}
|
||||
|
||||
function actionsFormatter(cell, formatterParams, onRendered) {
|
||||
return `
|
||||
<button class="btn btn-outline-success btn-icon add-to-main" data-identifier="${cell.getRow().getData().identifier}" title="Add to Contact">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 5v14m-7 -7h14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
var searchTerm = ""; // global variable to hold the search term
|
||||
|
||||
function updateSearchTerm(term) {
|
||||
searchTerm = term;
|
||||
table.replaceData();
|
||||
}
|
||||
|
||||
table = new Tabulator("#contactTable", {
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
height:400,
|
||||
paginationSizeSelector:[10, 25, 50, 100],
|
||||
clipboard:true,
|
||||
clipboardPasteAction:"replace",
|
||||
sortMode: "remote",
|
||||
ajaxURL: "/api/records/contact",
|
||||
ajaxURLGenerator: function(url, config, params) {
|
||||
var queryParts = [];
|
||||
|
||||
// Handle search term
|
||||
if (searchTerm) {
|
||||
queryParts.push("filter1=identifier,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter2=email,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter3=voice,cs," + encodeURIComponent(searchTerm));
|
||||
queryParts.push("filter4=crdate,cs," + encodeURIComponent(searchTerm));
|
||||
}
|
||||
|
||||
// Handle sorting from Tabulator
|
||||
if (params.sort && params.sort.length > 0) {
|
||||
var sorter = params.sort[0]; // single-column sorting
|
||||
var sortField = encodeURIComponent(sorter.field);
|
||||
var sortDir = (sorter.dir === "asc" ? "asc" : "desc");
|
||||
queryParts.push("order=" + sortField + "," + sortDir);
|
||||
} else {
|
||||
// fallback default order if no sorters
|
||||
queryParts.push("order=id,desc");
|
||||
}
|
||||
|
||||
// Include pagination parameters
|
||||
if (params.page) {
|
||||
queryParts.push("page=" + params.page + "," + params.size);
|
||||
}
|
||||
|
||||
return url + "?" + queryParts.join("&");
|
||||
},
|
||||
ajaxResponse: function(url, params, response) {
|
||||
if (response && Array.isArray(response.records) && typeof response.results === 'number') {
|
||||
var lastPage = Math.ceil(response.results / this.options.paginationSize);
|
||||
return {
|
||||
last_page: lastPage, // Calculated total number of pages
|
||||
data: response.records, // Data for the current page
|
||||
};
|
||||
} else {
|
||||
console.error('Unexpected response format', response);
|
||||
return { last_page: 1, data: [] };
|
||||
}
|
||||
},
|
||||
layout:"fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseStartOpen:false,
|
||||
resizableColumns:false,
|
||||
placeholder: "{{ __('No Data') }}",
|
||||
columns:[
|
||||
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
|
||||
{title:"{{ __('Identifier') }}", field:"identifier", width:200, minWidth:150, resizable:false, headerSort:true, formatter: contactLinkFormatter, responsive:0},
|
||||
{title:"{{ __('Email') }}", field:"email", minWidth:250, resizable:false, headerSort:true, responsive:2},
|
||||
{title: "{{ __('Actions') }}", formatter: actionsFormatter, minWidth:80, resizable:false, headerSort:false, download:false, hozAlign: "center", responsive:0, cellClick: function(e, cell){
|
||||
if (e.target.closest('.delete-btn')) {
|
||||
e.preventDefault(); // Prevent the default link behavior
|
||||
Swal.fire({
|
||||
title: "{{ __('Are you sure you want to delete this contact?') }}",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "{{ __('Confirm') }}"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
let deleteUrl = e.target.closest('.delete-btn').getAttribute('data-delete-url');
|
||||
window.location.href = deleteUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}},
|
||||
]
|
||||
});
|
||||
var searchInput = document.getElementById("search-input");
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
updateSearchTerm(searchInput.value);
|
||||
}, 300); // 300ms delay
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
var csrfTokenName = "{{ csrfTokenName }}";
|
||||
var csrfTokenValue = "{{ csrfTokenValue }}";
|
||||
|
|
|
@ -74,6 +74,7 @@ $app->group('', function ($route) {
|
|||
|
||||
$route->get('/contacts', ContactsController::class .':listContacts')->setName('listContacts');
|
||||
$route->map(['GET', 'POST'], '/contact/create', ContactsController::class . ':createContact')->setName('createContact');
|
||||
$route->map(['GET', 'POST'], '/contact/create-api', ContactsController::class . ':createContactApi')->setName('createContactApi');
|
||||
$route->get('/contact/view/{contact}', ContactsController::class . ':viewContact')->setName('viewContact');
|
||||
$route->get('/contact/update/{contact}', ContactsController::class . ':updateContact')->setName('updateContact');
|
||||
$route->get('/contact/validate/{contact}', ContactsController::class . ':validateContact')->setName('validateContact');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue